feat: multiple stuffs
This commit is contained in:
parent
9c5b3f60d5
commit
5b9b454330
29 changed files with 185 additions and 41 deletions
1
.npmrc
Normal file
1
.npmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
package-lock=false
|
|
@ -55,7 +55,7 @@
|
||||||
"fflate": "^0.7.4",
|
"fflate": "^0.7.4",
|
||||||
"find-my-way": "^7.5.0",
|
"find-my-way": "^7.5.0",
|
||||||
"katex": "^0.16.4",
|
"katex": "^0.16.4",
|
||||||
"mantine-datatable": "^1.8.6",
|
"mantine-datatable": "2.0.1",
|
||||||
"minio": "^7.0.32",
|
"minio": "^7.0.32",
|
||||||
"ms": "canary",
|
"ms": "canary",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
|
|
@ -50,6 +50,7 @@ export default function FileModal({
|
||||||
refresh,
|
refresh,
|
||||||
reducedActions = false,
|
reducedActions = false,
|
||||||
exifEnabled,
|
exifEnabled,
|
||||||
|
compress,
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
|
@ -58,6 +59,7 @@ export default function FileModal({
|
||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
reducedActions?: boolean;
|
reducedActions?: boolean;
|
||||||
exifEnabled?: boolean;
|
exifEnabled?: boolean;
|
||||||
|
compress: boolean;
|
||||||
}) {
|
}) {
|
||||||
const deleteFile = useFileDelete();
|
const deleteFile = useFileDelete();
|
||||||
const favoriteFile = useFileFavorite();
|
const favoriteFile = useFileFavorite();
|
||||||
|
@ -215,11 +217,11 @@ export default function FileModal({
|
||||||
<Stack>
|
<Stack>
|
||||||
<Type
|
<Type
|
||||||
file={file}
|
file={file}
|
||||||
src={`/r/${encodeURI(file.name)}`}
|
src={`/r/${encodeURI(file.name)}?compress=${compress}`}
|
||||||
alt={file.name}
|
alt={file.name}
|
||||||
popup
|
popup
|
||||||
sx={{ minHeight: 200 }}
|
sx={{ minHeight: 200, overflowY: 'scroll', maxHeight: 500 }}
|
||||||
style={{ minHeight: 200 }}
|
style={{ minHeight: 200, overflowY: 'scroll', maxHeight: 500 }}
|
||||||
disableMediaPreview={false}
|
disableMediaPreview={false}
|
||||||
overrideRender={overrideRender}
|
overrideRender={overrideRender}
|
||||||
setOverrideRender={setOverrideRender}
|
setOverrideRender={setOverrideRender}
|
||||||
|
|
|
@ -34,6 +34,7 @@ export default function File({
|
||||||
exifEnabled,
|
exifEnabled,
|
||||||
refreshImages,
|
refreshImages,
|
||||||
reducedActions = false,
|
reducedActions = false,
|
||||||
|
onDash,
|
||||||
}) {
|
}) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const deleteFile = useFileDelete();
|
const deleteFile = useFileDelete();
|
||||||
|
@ -57,9 +58,10 @@ export default function File({
|
||||||
refresh={refresh}
|
refresh={refresh}
|
||||||
reducedActions={reducedActions}
|
reducedActions={reducedActions}
|
||||||
exifEnabled={exifEnabled}
|
exifEnabled={exifEnabled}
|
||||||
|
compress={onDash}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Card sx={{ maxWidth: '100%', height: '100%' }} shadow='md'>
|
<Card sx={{ maxWidth: '100%', height: '100%' }} shadow='md' onClick={() => setOpen(true)}>
|
||||||
<Card.Section>
|
<Card.Section>
|
||||||
<LoadingOverlay visible={loading} />
|
<LoadingOverlay visible={loading} />
|
||||||
<Type
|
<Type
|
||||||
|
@ -78,9 +80,8 @@ export default function File({
|
||||||
width: '100%',
|
width: '100%',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
}}
|
}}
|
||||||
src={`/r/${encodeURI(image.name)}`}
|
src={`/r/${encodeURI(image.name)}?compress=${onDash}`}
|
||||||
alt={image.name}
|
alt={image.name}
|
||||||
onClick={() => setOpen(true)}
|
|
||||||
disableMediaPreview={disableMediaPreview}
|
disableMediaPreview={disableMediaPreview}
|
||||||
/>
|
/>
|
||||||
</Card.Section>
|
</Card.Section>
|
||||||
|
|
|
@ -99,6 +99,7 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
|
||||||
},
|
},
|
||||||
Modal: {
|
Modal: {
|
||||||
defaultProps: {
|
defaultProps: {
|
||||||
|
closeButtonProps: { size: 'lg' },
|
||||||
centered: true,
|
centered: true,
|
||||||
transitionProps: {
|
transitionProps: {
|
||||||
exitDuration: 100,
|
exitDuration: 100,
|
||||||
|
@ -118,7 +119,7 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
|
||||||
},
|
},
|
||||||
LoadingOverlay: {
|
LoadingOverlay: {
|
||||||
defaultProps: {
|
defaultProps: {
|
||||||
overlayProps: {
|
overlayprops: {
|
||||||
blur: 3,
|
blur: 3,
|
||||||
color: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
|
color: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
|
||||||
},
|
},
|
||||||
|
|
|
@ -45,8 +45,8 @@ function Placeholder({ text, Icon, ...props }) {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ height: 200 }} {...props}>
|
<Box sx={{ height: 320 }} {...props}>
|
||||||
<Center sx={{ height: 200 }}>
|
<Center sx={{ height: 320 }}>
|
||||||
<PlaceholderContent text={text} Icon={Icon} />
|
<PlaceholderContent text={text} Icon={Icon} />
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -158,6 +158,8 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop
|
||||||
image: (
|
image: (
|
||||||
<Image
|
<Image
|
||||||
placeholder={<PlaceholderContent Icon={IconPhotoCancel} text={'Image failed to load...'} />}
|
placeholder={<PlaceholderContent Icon={IconPhotoCancel} text={'Image failed to load...'} />}
|
||||||
|
height={320}
|
||||||
|
fit='contain'
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
|
|
@ -5,7 +5,7 @@ import File from 'components/File';
|
||||||
import MutedText from 'components/MutedText';
|
import MutedText from 'components/MutedText';
|
||||||
import { useRecent } from 'lib/queries/files';
|
import { useRecent } from 'lib/queries/files';
|
||||||
|
|
||||||
export default function RecentFiles({ disableMediaPreview, exifEnabled }) {
|
export default function RecentFiles({ disableMediaPreview, exifEnabled, compress }) {
|
||||||
const recent = useRecent('media');
|
const recent = useRecent('media');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -25,6 +25,7 @@ export default function RecentFiles({ disableMediaPreview, exifEnabled }) {
|
||||||
disableMediaPreview={disableMediaPreview}
|
disableMediaPreview={disableMediaPreview}
|
||||||
exifEnabled={exifEnabled}
|
exifEnabled={exifEnabled}
|
||||||
refreshImages={recent.refetch}
|
refreshImages={recent.refetch}
|
||||||
|
onDash={compress}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -22,7 +22,7 @@ import { useRecoilValue } from 'recoil';
|
||||||
import RecentFiles from './RecentFiles';
|
import RecentFiles from './RecentFiles';
|
||||||
import { StatCards } from './StatCards';
|
import { StatCards } from './StatCards';
|
||||||
|
|
||||||
export default function Dashboard({ disableMediaPreview, exifEnabled }) {
|
export default function Dashboard({ disableMediaPreview, exifEnabled, compress }) {
|
||||||
const user = useRecoilValue(userSelector);
|
const user = useRecoilValue(userSelector);
|
||||||
|
|
||||||
const recent = useRecent('media');
|
const recent = useRecent('media');
|
||||||
|
@ -138,6 +138,7 @@ export default function Dashboard({ disableMediaPreview, exifEnabled }) {
|
||||||
refresh={() => files.refetch()}
|
refresh={() => files.refetch()}
|
||||||
reducedActions={false}
|
reducedActions={false}
|
||||||
exifEnabled={exifEnabled}
|
exifEnabled={exifEnabled}
|
||||||
|
compress={compress}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -148,7 +149,7 @@ export default function Dashboard({ disableMediaPreview, exifEnabled }) {
|
||||||
|
|
||||||
<StatCards />
|
<StatCards />
|
||||||
|
|
||||||
<RecentFiles disableMediaPreview={disableMediaPreview} exifEnabled={exifEnabled} />
|
<RecentFiles disableMediaPreview={disableMediaPreview} exifEnabled={exifEnabled} compress={compress} />
|
||||||
|
|
||||||
<Box my='sm'>
|
<Box my='sm'>
|
||||||
<Title>Files</Title>
|
<Title>Files</Title>
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { useRouter } from 'next/router';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
export default function FilePagation({ disableMediaPreview, exifEnabled, queryPage }) {
|
export default function FilePagation({ disableMediaPreview, exifEnabled, queryPage, compress }) {
|
||||||
const [checked, setChecked] = useRecoilState(showNonMediaSelector);
|
const [checked, setChecked] = useRecoilState(showNonMediaSelector);
|
||||||
const [numPages, setNumPages] = useState(Number(queryPage)); // just set it to the queryPage, since the req may have not loaded yet
|
const [numPages, setNumPages] = useState(Number(queryPage)); // just set it to the queryPage, since the req may have not loaded yet
|
||||||
const [page, setPage] = useState(Number(queryPage));
|
const [page, setPage] = useState(Number(queryPage));
|
||||||
|
@ -75,6 +75,7 @@ export default function FilePagation({ disableMediaPreview, exifEnabled, queryPa
|
||||||
disableMediaPreview={disableMediaPreview}
|
disableMediaPreview={disableMediaPreview}
|
||||||
exifEnabled={exifEnabled}
|
exifEnabled={exifEnabled}
|
||||||
refreshImages={pages.refetch}
|
refreshImages={pages.refetch}
|
||||||
|
onDash={compress}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
|
|
@ -7,7 +7,7 @@ import Link from 'next/link';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import FilePagation from './FilePagation';
|
import FilePagation from './FilePagation';
|
||||||
|
|
||||||
export default function Files({ disableMediaPreview, exifEnabled, queryPage }) {
|
export default function Files({ disableMediaPreview, exifEnabled, queryPage, compress }) {
|
||||||
const [favoritePage, setFavoritePage] = useState(1);
|
const [favoritePage, setFavoritePage] = useState(1);
|
||||||
const [favoriteNumPages, setFavoriteNumPages] = useState(0);
|
const [favoriteNumPages, setFavoriteNumPages] = useState(0);
|
||||||
const favoritePages = usePaginatedFiles(favoritePage, 'media', true);
|
const favoritePages = usePaginatedFiles(favoritePage, 'media', true);
|
||||||
|
@ -50,6 +50,7 @@ export default function Files({ disableMediaPreview, exifEnabled, queryPage }) {
|
||||||
disableMediaPreview={disableMediaPreview}
|
disableMediaPreview={disableMediaPreview}
|
||||||
exifEnabled={exifEnabled}
|
exifEnabled={exifEnabled}
|
||||||
refreshImages={favoritePages.refetch}
|
refreshImages={favoritePages.refetch}
|
||||||
|
onDash={compress}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
@ -75,6 +76,7 @@ export default function Files({ disableMediaPreview, exifEnabled, queryPage }) {
|
||||||
disableMediaPreview={disableMediaPreview}
|
disableMediaPreview={disableMediaPreview}
|
||||||
exifEnabled={exifEnabled}
|
exifEnabled={exifEnabled}
|
||||||
queryPage={queryPage}
|
queryPage={queryPage}
|
||||||
|
compress={compress}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,7 +3,14 @@ import File from 'components/File';
|
||||||
import MutedText from 'components/MutedText';
|
import MutedText from 'components/MutedText';
|
||||||
import { useFolder } from 'lib/queries/folders';
|
import { useFolder } from 'lib/queries/folders';
|
||||||
|
|
||||||
export default function ViewFolderFilesModal({ open, setOpen, folderId, disableMediaPreview, exifEnabled }) {
|
export default function ViewFolderFilesModal({
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
folderId,
|
||||||
|
disableMediaPreview,
|
||||||
|
exifEnabled,
|
||||||
|
compress,
|
||||||
|
}) {
|
||||||
if (!folderId) return null;
|
if (!folderId) return null;
|
||||||
|
|
||||||
const folder = useFolder(folderId, true);
|
const folder = useFolder(folderId, true);
|
||||||
|
@ -26,6 +33,7 @@ export default function ViewFolderFilesModal({ open, setOpen, folderId, disableM
|
||||||
image={file}
|
image={file}
|
||||||
exifEnabled={exifEnabled}
|
exifEnabled={exifEnabled}
|
||||||
refreshImages={folder.refetch}
|
refreshImages={folder.refetch}
|
||||||
|
onDash={compress}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
|
@ -22,7 +22,7 @@ import { useEffect, useState } from 'react';
|
||||||
import CreateFolderModal from './CreateFolderModal';
|
import CreateFolderModal from './CreateFolderModal';
|
||||||
import ViewFolderFilesModal from './ViewFolderFilesModal';
|
import ViewFolderFilesModal from './ViewFolderFilesModal';
|
||||||
|
|
||||||
export default function Folders({ disableMediaPreview, exifEnabled }) {
|
export default function Folders({ disableMediaPreview, exifEnabled, compress }) {
|
||||||
const folders = useFolders();
|
const folders = useFolders();
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
const [createWithFile, setCreateWithFile] = useState(null);
|
const [createWithFile, setCreateWithFile] = useState(null);
|
||||||
|
@ -113,6 +113,7 @@ export default function Folders({ disableMediaPreview, exifEnabled }) {
|
||||||
folderId={activeFolderId}
|
folderId={activeFolderId}
|
||||||
disableMediaPreview={disableMediaPreview}
|
disableMediaPreview={disableMediaPreview}
|
||||||
exifEnabled={exifEnabled}
|
exifEnabled={exifEnabled}
|
||||||
|
compress={compress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Group mb='md'>
|
<Group mb='md'>
|
||||||
|
|
|
@ -5,11 +5,18 @@ export interface ConfigCore {
|
||||||
port: number;
|
port: number;
|
||||||
database_url: string;
|
database_url: string;
|
||||||
logger: boolean;
|
logger: boolean;
|
||||||
|
compression: ConfigCompression;
|
||||||
|
|
||||||
stats_interval: number;
|
stats_interval: number;
|
||||||
invites_interval: number;
|
invites_interval: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConfigCompression {
|
||||||
|
enabled: boolean;
|
||||||
|
threshold: number;
|
||||||
|
on_dashboard: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ConfigDatasource {
|
export interface ConfigDatasource {
|
||||||
type: 'local' | 's3' | 'supabase';
|
type: 'local' | 's3' | 'supabase';
|
||||||
local: ConfigLocalDatasource;
|
local: ConfigLocalDatasource;
|
||||||
|
|
|
@ -64,6 +64,9 @@ export default function readConfig() {
|
||||||
map('CORE_LOGGER', 'boolean', 'core.logger'),
|
map('CORE_LOGGER', 'boolean', 'core.logger'),
|
||||||
map('CORE_STATS_INTERVAL', 'number', 'core.stats_interval'),
|
map('CORE_STATS_INTERVAL', 'number', 'core.stats_interval'),
|
||||||
map('CORE_INVITES_INTERVAL', 'number', 'core.invites_interval'),
|
map('CORE_INVITES_INTERVAL', 'number', 'core.invites_interval'),
|
||||||
|
map('CORE_COMPRESSION_ENABLED', 'boolean', 'core.compression.enabled'),
|
||||||
|
map('CORE_COMPRESSION_THRESHOLD', 'human-to-byte', 'core.compression.threshold'),
|
||||||
|
map('CORE_COMPRESSION_ON_DASHBOARD', 'boolean', 'core.compression.on_dashboard'),
|
||||||
|
|
||||||
map('DATASOURCE_TYPE', 'string', 'datasource.type'),
|
map('DATASOURCE_TYPE', 'string', 'datasource.type'),
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,16 @@ const validator = s.object({
|
||||||
logger: s.boolean.default(false),
|
logger: s.boolean.default(false),
|
||||||
stats_interval: s.number.default(1800),
|
stats_interval: s.number.default(1800),
|
||||||
invites_interval: s.number.default(1800),
|
invites_interval: s.number.default(1800),
|
||||||
|
compression: s
|
||||||
|
.object({
|
||||||
|
enabled: s.boolean.default(false),
|
||||||
|
threshold: s.number.default(2048),
|
||||||
|
on_dashboard: s.boolean.default(true),
|
||||||
|
})
|
||||||
|
.default({
|
||||||
|
enabled: false,
|
||||||
|
on_dashboard: false,
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
datasource: s
|
datasource: s
|
||||||
.object({
|
.object({
|
||||||
|
|
|
@ -23,7 +23,7 @@ export function parseContent(
|
||||||
image: content.embed.image,
|
image: content.embed.image,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
url: args[3],
|
url: args.link,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,7 @@ export const getServerSideProps: GetServerSideProps<ServerSideProps> = async (ct
|
||||||
max_size: config.chunks.max_size,
|
max_size: config.chunks.max_size,
|
||||||
totp_enabled: config.mfa.totp_enabled,
|
totp_enabled: config.mfa.totp_enabled,
|
||||||
exif_enabled: config.exif.enabled,
|
exif_enabled: config.exif.enabled,
|
||||||
|
compress: config.core.compression.on_dashboard,
|
||||||
} as ServerSideProps,
|
} as ServerSideProps,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -24,11 +24,21 @@ export function parseString(str: string, value: ParseValue) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matches.groups.prop in ['password', 'avatar']) {
|
if (['password', 'avatar'].includes(matches.groups.prop)) {
|
||||||
str = replaceCharsFromString(str, '{unknown_property}', matches.index, re.lastIndex);
|
str = replaceCharsFromString(str, '{unknown_property}', matches.index, re.lastIndex);
|
||||||
re.lastIndex = matches.index;
|
re.lastIndex = matches.index;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (['originalName', 'name'].includes(matches.groups.prop)) {
|
||||||
|
str = replaceCharsFromString(
|
||||||
|
str,
|
||||||
|
decodeURIComponent(escape(getV[matches.groups.prop])),
|
||||||
|
matches.index,
|
||||||
|
re.lastIndex
|
||||||
|
);
|
||||||
|
re.lastIndex = matches.index;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const v = getV[matches.groups.prop];
|
const v = getV[matches.groups.prop];
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
username: 'administrator',
|
username: 'administrator',
|
||||||
password: await hashPassword('password'),
|
password: await hashPassword('password'),
|
||||||
token: createToken(),
|
token: createToken(),
|
||||||
|
superAdmin: true,
|
||||||
administrator: true,
|
administrator: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -72,9 +72,14 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||||
return res.json(newTarget);
|
return res.json(newTarget);
|
||||||
});
|
});
|
||||||
} else if (req.method === 'PATCH') {
|
} else if (req.method === 'PATCH') {
|
||||||
if (target.administrator && !user.superAdmin) return res.forbidden('cannot modify administrator');
|
if (
|
||||||
|
(target.administrator && !user.superAdmin) ||
|
||||||
|
(target.administrator && user.administrator && !user.superAdmin)
|
||||||
|
)
|
||||||
|
return res.forbidden('cannot modify administrator');
|
||||||
|
|
||||||
logger.debug(`attempting to update user ${id} with ${JSON.stringify(req.body)}`);
|
logger.debug(`attempting to update user ${id} with ${JSON.stringify(req.body)}`);
|
||||||
|
logger.debug(`user ${id} has ${!req.body.administrator} in administrator`);
|
||||||
|
|
||||||
if (req.body.password) {
|
if (req.body.password) {
|
||||||
const hashed = await hashPassword(req.body.password);
|
const hashed = await hashPassword(req.body.password);
|
||||||
|
@ -84,7 +89,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.body.administrator) {
|
if (typeof req.body.administrator != 'undefined') {
|
||||||
await prisma.user.update({
|
await prisma.user.update({
|
||||||
where: { id: target.id },
|
where: { id: target.id },
|
||||||
data: { administrator: req.body.administrator },
|
data: { administrator: req.body.administrator },
|
||||||
|
|
|
@ -99,6 +99,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||||
id: true,
|
id: true,
|
||||||
favorite: true,
|
favorite: true,
|
||||||
views: true,
|
views: true,
|
||||||
|
folderId: true,
|
||||||
maxViews: true,
|
maxViews: true,
|
||||||
size: true,
|
size: true,
|
||||||
},
|
},
|
||||||
|
|
|
@ -26,6 +26,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||||
maxViews: true,
|
maxViews: true,
|
||||||
folderId: true,
|
folderId: true,
|
||||||
size: true,
|
size: true,
|
||||||
|
favorite: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ export default function FilesPage(props) {
|
||||||
disableMediaPreview={props.disable_media_preview}
|
disableMediaPreview={props.disable_media_preview}
|
||||||
exifEnabled={props.exif_enabled}
|
exifEnabled={props.exif_enabled}
|
||||||
queryPage={props.queryPage}
|
queryPage={props.queryPage}
|
||||||
|
compress={props.compress}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -18,7 +18,11 @@ export default function FilesPage(props) {
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<Layout props={props}>
|
<Layout props={props}>
|
||||||
<Folders disableMediaPreview={props.disable_media_preview} exifEnabled={props.exif_enabled} />
|
<Folders
|
||||||
|
disableMediaPreview={props.disable_media_preview}
|
||||||
|
exifEnabled={props.exif_enabled}
|
||||||
|
compress={props.compress}
|
||||||
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -16,7 +16,11 @@ export default function DashboardPage(props) {
|
||||||
<title>{props.title}</title>
|
<title>{props.title}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<Layout props={props}>
|
<Layout props={props}>
|
||||||
<Dashboard disableMediaPreview={props.disable_media_preview} exifEnabled={props.exif_enabled} />
|
<Dashboard
|
||||||
|
disableMediaPreview={props.disable_media_preview}
|
||||||
|
exifEnabled={props.exif_enabled}
|
||||||
|
compress={props.compress}
|
||||||
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -25,9 +25,10 @@ type Props = {
|
||||||
folder: LimitedFolder;
|
folder: LimitedFolder;
|
||||||
uploadRoute: string;
|
uploadRoute: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
compress: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Folder({ title, folder }: Props) {
|
export default function Folder({ title, folder, compress }: Props) {
|
||||||
const full_title = `${title} - ${folder.name}`;
|
const full_title = `${title} - ${folder.name}`;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -55,6 +56,7 @@ export default function Folder({ title, folder }: Props) {
|
||||||
exifEnabled={false}
|
exifEnabled={false}
|
||||||
refreshImages={null}
|
refreshImages={null}
|
||||||
reducedActions={true}
|
reducedActions={true}
|
||||||
|
onDash={compress}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
@ -113,6 +115,7 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
|
||||||
folder,
|
folder,
|
||||||
uploadRoute: config.uploader.route,
|
uploadRoute: config.uploader.route,
|
||||||
title: config.website.title,
|
title: config.website.title,
|
||||||
|
compress: config.core.compression.on_dashboard,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Box, Button, Modal, PasswordInput } from '@mantine/core';
|
import { Box, Button, Modal, PasswordInput } from '@mantine/core';
|
||||||
import type { File } from '@prisma/client';
|
import type { File } from '@prisma/client';
|
||||||
|
import config from 'lib/config';
|
||||||
import Link from 'components/Link';
|
import Link from 'components/Link';
|
||||||
import exts from 'lib/exts';
|
import exts from 'lib/exts';
|
||||||
import prisma from 'lib/prisma';
|
import prisma from 'lib/prisma';
|
||||||
|
@ -15,13 +16,18 @@ export default function EmbeddedFile({
|
||||||
user,
|
user,
|
||||||
pass,
|
pass,
|
||||||
prismRender,
|
prismRender,
|
||||||
|
onDash,
|
||||||
|
compress,
|
||||||
}: {
|
}: {
|
||||||
file: File;
|
file: File & { imageProps?: HTMLImageElement };
|
||||||
user: UserExtended;
|
user: UserExtended;
|
||||||
pass: boolean;
|
pass: boolean;
|
||||||
prismRender: boolean;
|
prismRender: boolean;
|
||||||
|
onDash: boolean;
|
||||||
|
compress?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const dataURL = (route: string) => `${route}/${encodeURI(file.name)}`;
|
const dataURL = (route: string) =>
|
||||||
|
`${route}/${encodeURI(file.name)}?compress=${compress == null ? onDash : compress}`;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [opened, setOpened] = useState(pass);
|
const [opened, setOpened] = useState(pass);
|
||||||
|
@ -60,6 +66,7 @@ export default function EmbeddedFile({
|
||||||
if (url) {
|
if (url) {
|
||||||
imageEl.src = url;
|
imageEl.src = url;
|
||||||
}
|
}
|
||||||
|
file.imageProps = img;
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -94,7 +101,11 @@ export default function EmbeddedFile({
|
||||||
|
|
||||||
{file.mimetype.startsWith('image') && (
|
{file.mimetype.startsWith('image') && (
|
||||||
<>
|
<>
|
||||||
<meta property='og:image' content={`/r/${file.name}`} />
|
<meta property='og:type' content='image' />
|
||||||
|
<meta property='og:image' itemProp='image' content={`/r/${file.name}`} />
|
||||||
|
<meta property='og:url' content={`/r/${file.name}`} />
|
||||||
|
<meta property='og:image:width' content={file.imageProps?.naturalWidth.toString()} />
|
||||||
|
<meta property='og:image:height' content={file.imageProps?.naturalHeight.toString()} />
|
||||||
<meta property='twitter:card' content='summary_large_image' />
|
<meta property='twitter:card' content='summary_large_image' />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -116,6 +127,20 @@ export default function EmbeddedFile({
|
||||||
<meta property='og:video:height' content='480' />
|
<meta property='og:video:height' content='480' />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{file.mimetype.startsWith('audio') && (
|
||||||
|
<>
|
||||||
|
<meta name='twitter:card' content='player' />
|
||||||
|
<meta name='twitter:player:stream' content={`/r/${file.name}`} />
|
||||||
|
<meta name='twitter:player:stream:content_type' content={file.mimetype} />
|
||||||
|
<meta name='twitter:title' content={file.name} />
|
||||||
|
|
||||||
|
<meta property='og:type' content='music.song' />
|
||||||
|
<meta property='og:url' content={`/r/${file.name}`} />
|
||||||
|
<meta property='og:audio' content={`/r/${file.name}`} />
|
||||||
|
<meta property='og:audio:secure_url' content={`/r/${file.name}`} />
|
||||||
|
<meta property='og:audio:type' content={file.mimetype} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{!file.mimetype.startsWith('video') && !file.mimetype.startsWith('image') && (
|
{!file.mimetype.startsWith('video') && !file.mimetype.startsWith('image') && (
|
||||||
<meta property='og:url' content={`/r/${file.name}`} />
|
<meta property='og:url' content={`/r/${file.name}`} />
|
||||||
)}
|
)}
|
||||||
|
@ -155,10 +180,16 @@ export default function EmbeddedFile({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{file.mimetype.startsWith('video') && (
|
{file.mimetype.startsWith('video') && (
|
||||||
<video src={dataURL('/r')} controls={true} autoPlay={true} id='image_content' />
|
<video src={dataURL('/r')} controls autoPlay muted id='video_content' />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!file.mimetype.startsWith('video') && !file.mimetype.startsWith('image') && (
|
{file.mimetype.startsWith('audio') && (
|
||||||
|
<audio src={dataURL('/r')} controls autoPlay muted id='audio_content' />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!file.mimetype.startsWith('video') &&
|
||||||
|
!file.mimetype.startsWith('image') &&
|
||||||
|
!file.mimetype.startsWith('audio') && (
|
||||||
<Link href={dataURL('/r')}>Can't preview this file. Click here to download it.</Link>
|
<Link href={dataURL('/r')}>Can't preview this file. Click here to download it.</Link>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -168,7 +199,7 @@ export default function EmbeddedFile({
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||||
const { id } = context.params as { id: string };
|
const { id } = context.params as { id: string };
|
||||||
|
const { compress = null } = context.query as unknown as { compress?: boolean };
|
||||||
const file = await prisma.file.findFirst({
|
const file = await prisma.file.findFirst({
|
||||||
where: {
|
where: {
|
||||||
OR: [{ name: id }, { invisible: { invis: decodeURI(encodeURI(id)) } }],
|
OR: [{ name: id }, { invisible: { invis: decodeURI(encodeURI(id)) } }],
|
||||||
|
@ -233,6 +264,8 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||||
file,
|
file,
|
||||||
user,
|
user,
|
||||||
pass: file.password ? true : false,
|
pass: file.password ? true : false,
|
||||||
|
onDash: config.core.compression.on_dashboard,
|
||||||
|
compress,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,26 +2,65 @@ import { FastifyInstance, FastifyReply } from 'fastify';
|
||||||
import { guess } from 'lib/mimes';
|
import { guess } from 'lib/mimes';
|
||||||
import { extname } from 'path';
|
import { extname } from 'path';
|
||||||
import fastifyPlugin from 'fastify-plugin';
|
import fastifyPlugin from 'fastify-plugin';
|
||||||
|
import { createBrotliCompress, createDeflate, createGzip } from 'zlib';
|
||||||
|
import pump from 'pump';
|
||||||
|
import { Transform } from 'stream';
|
||||||
|
|
||||||
function rawFileDecorator(fastify: FastifyInstance, _, done) {
|
function rawFileDecorator(fastify: FastifyInstance, _, done) {
|
||||||
fastify.decorateReply('rawFile', rawFile);
|
fastify.decorateReply('rawFile', rawFile);
|
||||||
done();
|
done();
|
||||||
|
|
||||||
async function rawFile(this: FastifyReply, id: string) {
|
async function rawFile(this: FastifyReply, id: string) {
|
||||||
const { download } = this.request.query as { download?: string };
|
const { download, compress } = this.request.query as { download?: string; compress?: boolean };
|
||||||
|
|
||||||
const data = await this.server.datasource.get(id);
|
const data = await this.server.datasource.get(id);
|
||||||
if (!data) return this.notFound();
|
if (!data) return this.notFound();
|
||||||
|
|
||||||
const mimetype = await guess(extname(id).slice(1));
|
const mimetype = await guess(extname(id).slice(1));
|
||||||
const size = await this.server.datasource.size(id);
|
const size = await this.server.datasource.size(id);
|
||||||
|
|
||||||
this.header('Content-Length', size);
|
|
||||||
this.header('Content-Type', download ? 'application/octet-stream' : mimetype);
|
this.header('Content-Type', download ? 'application/octet-stream' : mimetype);
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.server.config.core.compression.enabled &&
|
||||||
|
compress &&
|
||||||
|
!this.request.headers['X-Zipline-NoCompress'] &&
|
||||||
|
!!this.request.headers['accept-encoding']
|
||||||
|
)
|
||||||
|
if (size > this.server.config.core.compression.threshold)
|
||||||
|
return this.send(useCompress.call(this, data));
|
||||||
|
this.header('Content-Length', size);
|
||||||
return this.send(data);
|
return this.send(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useCompress(this: FastifyReply, data: NodeJS.ReadableStream) {
|
||||||
|
let compress: Transform;
|
||||||
|
|
||||||
|
switch ((this.request.headers['accept-encoding'] as string).split(', ')[0]) {
|
||||||
|
case 'gzip':
|
||||||
|
case 'x-gzip':
|
||||||
|
compress = createGzip();
|
||||||
|
this.header('Content-Encoding', 'gzip');
|
||||||
|
break;
|
||||||
|
case 'deflate':
|
||||||
|
compress = createDeflate();
|
||||||
|
this.header('Content-Encoding', 'deflate');
|
||||||
|
break;
|
||||||
|
case 'br':
|
||||||
|
compress = createBrotliCompress();
|
||||||
|
this.header('Content-Encoding', 'br');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.server.logger
|
||||||
|
.child('response')
|
||||||
|
.error(`Unsupported encoding: ${this.request.headers['accept-encoding']}}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!compress) return data;
|
||||||
|
setTimeout(() => compress.destroy(), 2000);
|
||||||
|
return pump(data, compress, (err) => (err ? this.server.logger.error(err) : null));
|
||||||
|
}
|
||||||
|
|
||||||
export default fastifyPlugin(rawFileDecorator, {
|
export default fastifyPlugin(rawFileDecorator, {
|
||||||
name: 'rawFile',
|
name: 'rawFile',
|
||||||
decorators: {
|
decorators: {
|
||||||
|
|
14
yarn.lock
14
yarn.lock
|
@ -7167,14 +7167,14 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"mantine-datatable@npm:^1.8.6":
|
"mantine-datatable@npm:2.0.1":
|
||||||
version: 1.8.6
|
version: 2.0.1
|
||||||
resolution: "mantine-datatable@npm:1.8.6"
|
resolution: "mantine-datatable@npm:2.0.1"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
"@mantine/core": ^5.10.4
|
"@mantine/core": ^6.0.0
|
||||||
"@mantine/hooks": ^5.10.4
|
"@mantine/hooks": ^6.0.0
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
checksum: a4558418067ada3e269e3d9dc0153b613b031ecd5fccd484b45a2b13e403084971870515d51c888d933eb2f4d85f6df2ad221b0c84a2bf2cd602d27938bb9029
|
checksum: aa39a662619373a82ed0408ffbe4ade71449abf281495af6820bfe9113d6de22005128d8ac1ff7d6a89f4919d58c1c2ceb0fdfa4e9be37050350d2635f595f6b
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
@ -11729,7 +11729,7 @@ __metadata:
|
||||||
fflate: ^0.7.4
|
fflate: ^0.7.4
|
||||||
find-my-way: ^7.5.0
|
find-my-way: ^7.5.0
|
||||||
katex: ^0.16.4
|
katex: ^0.16.4
|
||||||
mantine-datatable: ^1.8.6
|
mantine-datatable: 2.0.1
|
||||||
minio: ^7.0.32
|
minio: ^7.0.32
|
||||||
ms: canary
|
ms: canary
|
||||||
multer: ^1.4.5-lts.1
|
multer: ^1.4.5-lts.1
|
||||||
|
|
Loading…
Reference in a new issue