mirror of
https://github.com/diced/zipline.git
synced 2025-12-05 20:40:12 -08:00
Compare commits
6 Commits
c8c4faef2a
...
d49afe60c8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d49afe60c8 | ||
|
|
3370d4b663 | ||
|
|
1f1bcd3a47 | ||
|
|
d9df04bac5 | ||
|
|
2bf2809269 | ||
|
|
9bb9e7e399 |
@@ -157,7 +157,6 @@ export default function Login() {
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log({ willRedirect, config });
|
||||
if (willRedirect && config) {
|
||||
const provider = Object.keys(config.oauthEnabled).find(
|
||||
(x) => config.oauthEnabled[x as keyof typeof config.oauthEnabled] === true,
|
||||
|
||||
@@ -29,6 +29,7 @@ import { showNotification } from '@mantine/notifications';
|
||||
import {
|
||||
Icon,
|
||||
IconBombFilled,
|
||||
IconClipboardTypography,
|
||||
IconCopy,
|
||||
IconDeviceSdCard,
|
||||
IconDownload,
|
||||
@@ -403,6 +404,11 @@ export default function FileModal({
|
||||
tooltip='View file in a new tab'
|
||||
color='blue'
|
||||
/>
|
||||
<ActionButton
|
||||
Icon={IconClipboardTypography}
|
||||
onClick={() => copyFile(file, clipboard, true)}
|
||||
tooltip='Copy raw file link'
|
||||
/>
|
||||
<ActionButton
|
||||
Icon={IconCopy}
|
||||
onClick={() => copyFile(file, clipboard)}
|
||||
|
||||
@@ -27,10 +27,14 @@ export function downloadFile(file: File) {
|
||||
window.open(`/raw/${file.name}?download=true`, '_blank');
|
||||
}
|
||||
|
||||
export function copyFile(file: File, clipboard: ReturnType<typeof useClipboard>) {
|
||||
export function copyFile(file: File, clipboard: ReturnType<typeof useClipboard>, raw: boolean = false) {
|
||||
const domain = `${window.location.protocol}//${window.location.host}`;
|
||||
|
||||
const url = file.url ? `${domain}${file.url}` : `${domain}/view/${file.name}`;
|
||||
const url = raw
|
||||
? `${domain}/raw/${file.name}`
|
||||
: file.url
|
||||
? `${domain}${file.url}`
|
||||
: `${domain}/view/${file.name}`;
|
||||
|
||||
clipboard.copy(url);
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
IconShare,
|
||||
IconShareOff,
|
||||
IconTrashFilled,
|
||||
IconZip,
|
||||
} from '@tabler/icons-react';
|
||||
import ViewFilesModal from '../ViewFilesModal';
|
||||
import EditFolderNameModal from '../EditFolderNameModal';
|
||||
@@ -169,6 +170,14 @@ export default function FolderTableView() {
|
||||
<IconPencil size='1rem' />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label='Export folder as ZIP'>
|
||||
<ActionIcon
|
||||
color='blue'
|
||||
onClick={() => window.open(`/api/user/folders/${folder.id}/export`, '_blank')}
|
||||
>
|
||||
<IconZip size='1rem' />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label='Delete Folder'>
|
||||
<ActionIcon
|
||||
color='red'
|
||||
|
||||
@@ -99,8 +99,7 @@ export default function StatsTables({ data }: { data: Metric[] }) {
|
||||
|
||||
const recent = data[0]; // it is sorted by desc so 0 is the first one.
|
||||
|
||||
if (recent.data.filesUsers.length === 0) return null;
|
||||
if (recent.data.urlsUsers.length === 0) return null;
|
||||
if (recent.data.filesUsers.length === 0 || recent.data.urlsUsers.length === 0) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -121,7 +120,7 @@ export default function StatsTables({ data }: { data: Metric[] }) {
|
||||
.sort((a, b) => b.sum - a.sum)
|
||||
.map((count, i) => (
|
||||
<Table.Tr key={i}>
|
||||
<Table.Td>{count.username}</Table.Td>
|
||||
<Table.Td>{count.username ?? '[unknown]'}</Table.Td>
|
||||
<Table.Td>{count.sum}</Table.Td>
|
||||
<Table.Td>{bytes(count.storage)}</Table.Td>
|
||||
<Table.Td>{count.views}</Table.Td>
|
||||
@@ -147,7 +146,7 @@ export default function StatsTables({ data }: { data: Metric[] }) {
|
||||
.sort((a, b) => b.sum - a.sum)
|
||||
.map((count, i) => (
|
||||
<Table.Tr key={i}>
|
||||
<Table.Td>{count.username}</Table.Td>
|
||||
<Table.Td>{count.username ?? '[unknown]'}</Table.Td>
|
||||
<Table.Td>{count.sum}</Table.Td>
|
||||
<Table.Td>{count.views}</Table.Td>
|
||||
</Table.Tr>
|
||||
|
||||
@@ -32,7 +32,6 @@ export default function DashboardServerSettings() {
|
||||
|
||||
const scrollToSetting = useMemo(() => {
|
||||
return (setting: string) => {
|
||||
console.log('scrolling to setting:', setting);
|
||||
const input = document.querySelector<HTMLInputElement>(`[data-path="${setting}"]`);
|
||||
if (input) {
|
||||
const observer = new IntersectionObserver(
|
||||
|
||||
@@ -20,8 +20,6 @@ export default function DashboardSettings() {
|
||||
const config = useConfig();
|
||||
const user = useUserStore((state) => state.user);
|
||||
|
||||
console.log(config.oauthEnabled);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Group gap='sm'>
|
||||
|
||||
@@ -232,7 +232,7 @@ export default function GeneratorButton({
|
||||
{name === 'ShareX' && (
|
||||
<Switch
|
||||
label='Xshare Compatibility'
|
||||
description='If you choose to use the Xshare app on Android, enable this option for compatibility. The genereated config will not work with ShareX.'
|
||||
description='If you choose to use the Xshare app on Android, enable this option for compatibility. The generated config will not work with ShareX.'
|
||||
checked={options.sharex_xshareCompatibility ?? false}
|
||||
onChange={(event) => setOption({ sharex_xshareCompatibility: event.currentTarget.checked })}
|
||||
disabled={!onlyFile}
|
||||
|
||||
@@ -19,7 +19,7 @@ export const metricDataSchema = z.object({
|
||||
|
||||
filesUsers: z.array(
|
||||
z.object({
|
||||
username: z.string(),
|
||||
username: z.string().nullable(),
|
||||
sum: z.number(),
|
||||
storage: z.number(),
|
||||
views: z.number(),
|
||||
@@ -27,7 +27,7 @@ export const metricDataSchema = z.object({
|
||||
),
|
||||
urlsUsers: z.array(
|
||||
z.object({
|
||||
username: z.string(),
|
||||
username: z.string().nullable(),
|
||||
sum: z.number(),
|
||||
views: z.number(),
|
||||
}),
|
||||
|
||||
@@ -45,7 +45,9 @@ async function vitePlugin(fastify: FastifyInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
reply.hijack();
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
vite!.middlewares(req.raw, reply.raw, (err: any) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
|
||||
@@ -24,11 +24,16 @@ type Body = {
|
||||
|
||||
const logger = log('api').c('user').c('files').c('transaction');
|
||||
|
||||
function checkInteraction(current: Role, roles: Role[]) {
|
||||
function checkInteraction(
|
||||
current: { id: string; role: Role },
|
||||
roles: { id: string; role: Role }[],
|
||||
): number[] {
|
||||
const indices: number[] = [];
|
||||
|
||||
for (let i = 0; i !== roles.length; ++i) {
|
||||
if (!canInteract(current, roles[i])) {
|
||||
if (roles[i].id === current.id) continue;
|
||||
|
||||
if (!canInteract(current.role, roles[i].role)) {
|
||||
indices.push(i);
|
||||
}
|
||||
}
|
||||
@@ -58,8 +63,8 @@ export default fastifyPlugin(
|
||||
});
|
||||
|
||||
const invalids = checkInteraction(
|
||||
req.user.role,
|
||||
toFavoriteFiles.map((f) => f.User?.role ?? 'USER'),
|
||||
{ id: req.user.id, role: req.user.role },
|
||||
toFavoriteFiles.map((f) => ({ id: f.userId ?? '', role: f.User?.role ?? 'USER' })),
|
||||
);
|
||||
if (invalids.length > 0)
|
||||
return res.forbidden(`You don't have the permission to modify files[${invalids.join(', ')}]`);
|
||||
@@ -147,8 +152,8 @@ export default fastifyPlugin(
|
||||
});
|
||||
|
||||
const invalids = checkInteraction(
|
||||
req.user.role,
|
||||
toDeleteFiles.map((f) => f.User?.role ?? 'USER'),
|
||||
{ id: req.user.id, role: req.user.role },
|
||||
toDeleteFiles.map((f) => ({ id: f.userId ?? '', role: f.User?.role ?? 'USER' })),
|
||||
);
|
||||
if (invalids.length > 0)
|
||||
return res.forbidden(`You don't have the permission to delete files[${invalids.join(', ')}]`);
|
||||
|
||||
68
src/server/routes/api/user/folders/[id]/export.ts
Normal file
68
src/server/routes/api/user/folders/[id]/export.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { datasource } from '@/lib/datasource';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { log } from '@/lib/logger';
|
||||
import { userMiddleware } from '@/server/middleware/user';
|
||||
import archiver from 'archiver';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
|
||||
export type ApiUserFoldersIdExportResponse = null;
|
||||
|
||||
type Params = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const logger = log('api').c('user').c('folders').c('[id]').c('export');
|
||||
|
||||
export const PATH = '/api/user/folders/:id/export';
|
||||
export default fastifyPlugin(
|
||||
(server, _, done) => {
|
||||
server.get<{ Params: Params }>(PATH, { preHandler: [userMiddleware] }, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
const folder = await prisma.folder.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
files: true,
|
||||
},
|
||||
});
|
||||
if (!folder) return res.notFound('Folder not found');
|
||||
if (req.user.id !== folder.userId) return res.forbidden('You do not own this folder');
|
||||
|
||||
if (!folder.files.length) return res.badRequest("Can't export an empty folder.");
|
||||
|
||||
logger.info(`folder export requested: ${folder.name}`, { user: req.user.id, folder: folder.id });
|
||||
|
||||
res.hijack();
|
||||
|
||||
const zip = archiver('zip', {
|
||||
zlib: { level: 9 },
|
||||
});
|
||||
|
||||
zip.pipe(res.raw);
|
||||
|
||||
for (const file of folder.files) {
|
||||
const stream = await datasource.get(file.name);
|
||||
if (!stream) {
|
||||
logger.warn('failed to get file stream for folder export', { file: file.id, folder: folder.id });
|
||||
continue;
|
||||
}
|
||||
|
||||
zip.append(stream, { name: file.name });
|
||||
}
|
||||
|
||||
zip.on('error', (err) => {
|
||||
logger.error('error during folder export zip creation', { folder: folder.id }).error(err as Error);
|
||||
});
|
||||
|
||||
zip.on('finish', () => {
|
||||
logger.info(`folder export completed: ${folder.name}`, { user: req.user.id, folder: folder.id });
|
||||
});
|
||||
|
||||
await zip.finalize();
|
||||
});
|
||||
done();
|
||||
},
|
||||
{ name: PATH },
|
||||
);
|
||||
Reference in New Issue
Block a user