Compare commits

..

25 Commits

Author SHA1 Message Date
diced
fe50bebeba feat(v3.7.6): version 2023-11-20 21:31:08 -08:00
Kashall
1f61c56f83 fix: pg int -> bigint (#484) (#459)
* fix: prisma int supports up to 2gb. Bigint supports a lot more than 2gb.

* yes, i ran `yarn migrate:dev`

* fix: cannot assign bigint to number

* fix: 'bigint' is not assignable to type 'number'.

* fix: 'bigint' is not assignable to type 'number'.

* jesus christ

* Well okay then next
2023-11-20 20:59:40 -08:00
neomoth
cabf932ca0 fix: flameshot abort (#490)
Exit script should file be 0B (aborted screenshot)

Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
2023-11-20 20:44:58 -08:00
diced
f6b995c28d feat: update pkgs & fix lint errors 2023-11-20 20:37:52 -08:00
diced
13a19ccd2b fix: build errors 2023-11-19 17:03:23 -08:00
diced
d1dea0cd92 fix: why did this randomly become a promise 2023-11-10 19:31:15 -08:00
diced
b39507b9a8 fix: actually fix dupe files #467 2023-11-10 19:24:45 -08:00
diced
633dfd4712 feat(v3.7.5): version 2023-11-05 22:35:12 -08:00
Digital
e6ed7a36d5 feat: whitelisted discord & redirect uri oauth (#469)
* Allow Redirect URI Configuration

* Prettier

* Add Whitelisted Users

* Update discord.ts

* Whitespace

* Whitespace

---------

Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
2023-11-05 22:01:43 -08:00
diced
93cb9eec4c fix: overwriting existing files #467 2023-11-05 21:53:16 -08:00
diced
4849cd8221 fix: add warning when wordlist missing #478 2023-11-04 15:24:00 -07:00
diced
89c58044a3 fix: imported files incl size #468 2023-11-04 15:20:37 -07:00
diced
40fb11256f fix: non-english characters encoding (#471) 2023-10-08 11:06:16 -07:00
diced
d112c3a509 feat/fix: UPLOADER_RANDOM_WORDS_sEPERATOR 2023-09-29 20:06:18 -07:00
Jayvin Hernandez
23af36563f fix: no size on folders page (#465) 2023-09-29 19:53:06 -07:00
Kashall
28db15eb77 fix: util method to check if variable is not null (#458)
* chore: fix oauth truthyness

* chore: remove unused util function

* chore: lint

---------

Co-authored-by: Jayvin Hernandez <gogojayvin923@gmail.com>
2023-09-09 09:57:00 -07:00
Lucas Reis
e9054bd3e5 fix: og video type (#462) 2023-09-09 09:54:13 -07:00
diced
713f857e28 feat(v3.7.4): version 2023-08-29 15:36:43 -07:00
diced
5d6768029f fix: WEBSITE_SHOW_VERSION=false works now (#450) 2023-08-29 15:33:20 -07:00
Jayvin Hernandez
72e24a8b86 fix: trailing spaces giphy (#449) 2023-08-29 15:25:26 -07:00
diced
86c3e780d1 fix: docker size optimizations 2023-08-12 23:58:19 -07:00
diced
5102620953 fix: letters cut off #448 2023-08-08 23:48:26 -07:00
diced
4d728f9f8b fix: domain 2023-08-08 23:46:46 -07:00
dicedtomato
faf5098357 feat(v3..7.3): version 2023-07-31 19:06:28 -07:00
diced
c4066fc851 fix: change domains 2023-07-31 18:28:21 -07:00
71 changed files with 5019 additions and 3924 deletions

View File

@@ -7,5 +7,5 @@ contact_links:
url: https://discord.gg/EAhCRfGxCF
about: Ask for help with anything related to Zipline!
- name: Zipline Docs
url: https://zipline.diced.tech
url: https://zipline.diced.sh
about: Maybe take a look a the docs?

0
.yarn/releases/yarn-3.3.1.cjs vendored Executable file → Normal file
View File

View File

@@ -26,10 +26,6 @@ ENV PRISMA_QUERY_ENGINE_BINARY=/prisma-engines/query-engine \
ZIPLINE_DOCKER_BUILD=true \
NEXT_TELEMETRY_DISABLED=1
# Install production dependencies then temporarily save
RUN yarn workspaces focus --production --all
RUN cp -RL node_modules /tmp/node_modules
# Install the dependencies
RUN yarn install --immutable
@@ -63,20 +59,20 @@ ENV PRISMA_QUERY_ENGINE_BINARY=/prisma-engines/query-engine \
# Copy only the necessary files from the previous stage
COPY --from=builder /zipline/dist ./dist
COPY --from=builder /zipline/.next ./.next
COPY --from=builder /zipline/package.json ./package.json
COPY --from=builder /zipline/mimes.json ./mimes.json
COPY --from=builder /zipline/next.config.js ./next.config.js
COPY --from=builder /zipline/public ./public
COPY --from=builder /zipline/node_modules ./node_modules
# Copy Startup Script
COPY docker-entrypoint.sh /zipline
# Make Startup Script Executable
RUN chmod a+x /zipline/docker-entrypoint.sh && rm -rf /zipline/src
# Clean up
RUN rm -rf /tmp/* /root/*
RUN yarn cache clean --all
# Set the entrypoint to the startup script
ENTRYPOINT ["tini", "--", "/zipline/docker-entrypoint.sh"]

View File

@@ -1,6 +1,6 @@
{
"name": "zipline",
"version": "3.7.2",
"version": "3.7.6",
"license": "MIT",
"scripts": {
"dev": "npm-run-all build:server dev:run",
@@ -28,73 +28,73 @@
"scripts:clear-temp": "node --enable-source-maps dist/scripts/clear-temp"
},
"dependencies": {
"@emotion/react": "^11.10.6",
"@emotion/server": "^11.10.0",
"@mantine/core": "^6.0.4",
"@mantine/dropzone": "^6.0.4",
"@mantine/form": "^6.0.4",
"@mantine/hooks": "^6.0.4",
"@mantine/modals": "^6.0.4",
"@mantine/next": "^6.0.4",
"@mantine/notifications": "^6.0.4",
"@mantine/prism": "^6.0.4",
"@mantine/spotlight": "^6.0.4",
"@prisma/client": "^4.10.1",
"@prisma/internals": "^4.10.1",
"@prisma/migrate": "^4.10.1",
"@sapphire/shapeshift": "^3.8.1",
"@tabler/icons-react": "^2.11.0",
"@emotion/react": "^11.11.1",
"@emotion/server": "^11.11.0",
"@mantine/core": "^6.0.21",
"@mantine/dropzone": "^6.0.21",
"@mantine/form": "^6.0.21",
"@mantine/hooks": "^6.0.21",
"@mantine/modals": "^6.0.21",
"@mantine/next": "^6.0.21",
"@mantine/notifications": "^6.0.21",
"@mantine/prism": "^6.0.21",
"@mantine/spotlight": "^6.0.21",
"@prisma/client": "^4.16.2",
"@prisma/internals": "^4.16.2",
"@prisma/migrate": "^4.16.2",
"@sapphire/shapeshift": "^3.9.3",
"@tabler/icons-react": "^2.41.0",
"@tanstack/react-query": "^4.28.0",
"argon2": "^0.30.3",
"cookie": "^0.5.0",
"dayjs": "^1.11.7",
"dotenv": "^16.0.3",
"argon2": "^0.31.2",
"cookie": "^0.6.0",
"dayjs": "^1.11.10",
"dotenv": "^16.3.1",
"dotenv-expand": "^10.0.0",
"exiftool-vendored": "^21.2.0",
"fastify": "^4.15.0",
"fastify-plugin": "^4.5.0",
"fflate": "^0.7.4",
"ffmpeg-static": "^5.1.0",
"find-my-way": "^7.6.0",
"katex": "^0.16.4",
"mantine-datatable": "^2.2.6",
"minio": "^7.0.33",
"exiftool-vendored": "^23.4.0",
"fastify": "^4.24.3",
"fastify-plugin": "^4.5.1",
"fflate": "^0.8.1",
"ffmpeg-static": "^5.2.0",
"find-my-way": "^7.7.0",
"katex": "^0.16.9",
"mantine-datatable": "^2.9.14",
"minio": "^7.1.3",
"ms": "canary",
"multer": "^1.4.5-lts.1",
"next": "^13.2.4",
"next": "^14.0.3",
"otplib": "^12.0.1",
"prisma": "^4.10.1",
"prisma": "^4.16.2",
"prismjs": "^1.29.0",
"qrcode": "^1.5.1",
"qrcode": "^1.5.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^8.0.6",
"recharts": "^2.5.0",
"recharts": "^2.10.1",
"recoil": "^0.7.7",
"remark-gfm": "^3.0.1",
"sharp": "^0.32.0"
"remark-gfm": "^4.0.0",
"sharp": "^0.32.6"
},
"devDependencies": {
"@types/cookie": "^0.5.1",
"@types/katex": "^0.16.0",
"@types/minio": "^7.0.17",
"@types/multer": "^1.4.7",
"@types/node": "^18.15.10",
"@types/qrcode": "^1.5.0",
"@types/react": "^18.0.29",
"@types/sharp": "^0.31.1",
"@typescript-eslint/eslint-plugin": "^5.56.0",
"@typescript-eslint/parser": "^5.56.0",
"@types/cookie": "^0.5.4",
"@types/katex": "^0.16.6",
"@types/minio": "^7.1.1",
"@types/multer": "^1.4.10",
"@types/node": "^18.18.10",
"@types/qrcode": "^1.5.5",
"@types/react": "^18.2.37",
"@types/sharp": "^0.32.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"cross-env": "^7.0.3",
"eslint": "^8.36.0",
"eslint-config-next": "^13.2.4",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-unused-imports": "^2.0.0",
"eslint": "^8.54.0",
"eslint-config-next": "^14.0.3",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-unused-imports": "^3.0.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.8.7",
"tsup": "^6.7.0",
"typescript": "^5.0.2"
"prettier": "^3.1.0",
"tsup": "^8.0.0",
"typescript": "^5.2.2"
},
"repository": {
"type": "git",

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "File" ALTER COLUMN "size" SET DATA TYPE BIGINT;

View File

@@ -48,7 +48,7 @@ model File {
originalName String?
mimetype String @default("image/png")
createdAt DateTime @default(now())
size Int @default(0)
size BigInt @default(0)
expiresAt DateTime?
maxViews Int?
views Int @default(0)
@@ -63,7 +63,7 @@ model File {
folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
folderId Int?
thumbnail Thumbnail?
thumbnail Thumbnail?
}
model Thumbnail {

View File

@@ -1498,4 +1498,4 @@ wheat
white
whitesmoke
yellow
yellowgreen
yellowgreen

View File

@@ -1747,4 +1747,4 @@ zigzagsalamander
zonetailedpigeon
zooplankton
zopilote
zorilla
zorilla

View File

@@ -125,7 +125,7 @@ export default function FileModal({
icon: <IconPhotoCancel size='1rem' />,
});
},
}
},
);
};

View File

@@ -280,7 +280,7 @@ export default function Layout({ children, props }) {
component={Link}
href={link}
/>
)
),
)}
</Navbar.Section>
<Navbar.Section>
@@ -360,6 +360,11 @@ export default function Layout({ children, props }) {
compact
size='xl'
p='sm'
styles={{
label: {
overflow: 'unset',
},
}}
>
{user.username}
</Button>
@@ -411,16 +416,20 @@ export default function Layout({ children, props }) {
</Menu.Item>
<Menu.Divider />
<>
{oauth_providers.filter((x) =>
user.oauth?.map(({ provider }) => provider.toLowerCase()).includes(x.name.toLowerCase())
{oauth_providers.filter(
(x) =>
user.oauth
?.map(({ provider }) => provider.toLowerCase())
.includes(x.name.toLowerCase()),
).length ? (
<Menu.Label>Connected Accounts</Menu.Label>
) : null}
{oauth_providers
.filter((x) =>
user.oauth
?.map(({ provider }) => provider.toLowerCase())
.includes(x.name.toLowerCase())
.filter(
(x) =>
user.oauth
?.map(({ provider }) => provider.toLowerCase())
.includes(x.name.toLowerCase()),
)
.map(({ name, Icon }, i) => (
<>
@@ -433,8 +442,11 @@ export default function Layout({ children, props }) {
</Menu.Item>
</>
))}
{oauth_providers.filter((x) =>
user.oauth?.map(({ provider }) => provider.toLowerCase()).includes(x.name.toLowerCase())
{oauth_providers.filter(
(x) =>
user.oauth
?.map(({ provider }) => provider.toLowerCase())
.includes(x.name.toLowerCase()),
).length ? (
<Menu.Divider />
) : null}

View File

@@ -29,7 +29,7 @@ export default function FilePagation({ disableMediaPreview, exifEnabled, queryPa
},
},
undefined,
{ shallow: true }
{ shallow: true },
);
const { count } = await useFetch(`/api/user/paged?count=true${!checked ? '&filter=media' : ''}`);

View File

@@ -75,7 +75,7 @@ export default function Flameshot({ user, open, setOpen }) {
let shell;
if (values.type === 'upload-file') {
shell = `#!/bin/bash${values.wlCompositorNotSupported ? '\nexport XDG_CURRENT_DESKTOP=sway\n' : ''}
flameshot gui -r > /tmp/ss.png;
flameshot gui -r > /tmp/ss.png;if [ ! -s /tmp/ss.png ]; then\n exit 1\nfi
${curl.join(' ')}${values.noJSON ? '' : " | jq -r '.files[0]'"} | tr -d '\\n' | ${
values.wlCompatibility ? 'wl-copy' : 'xsel -ib'
};

View File

@@ -87,7 +87,7 @@ export default function ShareX({ user, open, setOpen }) {
const pseudoElement = document.createElement('a');
pseudoElement.setAttribute(
'href',
'data:application/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(config, null, '\t'))
'data:application/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(config, null, '\t')),
);
pseudoElement.setAttribute('download', `zipline${values.type === 'upload-file' ? '' : '-url'}.sxcu`);
pseudoElement.style.display = 'none';

View File

@@ -268,7 +268,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
size: s.size,
full: s.name,
}))
.sort((a, b) => a.date.getTime() - b.date.getTime())
.sort((a, b) => a.date.getTime() - b.date.getTime()),
);
};
@@ -367,8 +367,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
<Title>Manage User</Title>
<MutedText size='md'>
Want to use variables in embed text? Visit{' '}
<AnchorNext href='https://zipline.diced.tech/docs/guides/variables'>the docs</AnchorNext> for
variables
<AnchorNext href='https://zipline.diced.sh/docs/guides/variables'>the docs</AnchorNext> for variables
</MutedText>
<TextInput
@@ -488,7 +487,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
{oauth_providers
.filter(
(x) =>
!user.oauth?.map(({ provider }) => provider.toLowerCase()).includes(x.name.toLowerCase())
!user.oauth?.map(({ provider }) => provider.toLowerCase()).includes(x.name.toLowerCase()),
)
.map(({ link_url, name, Icon }, i) => (
<Button key={i} size='lg' leftIcon={<Icon />} component={Link} href={link_url} my='sm'>

View File

@@ -43,7 +43,7 @@ export default function File({ chunks: chunks_config }) {
return e.returnValue;
}
},
[loading]
[loading],
);
const beforeRouteChange = useCallback(
@@ -56,7 +56,7 @@ export default function File({ chunks: chunks_config }) {
}
}
},
[loading]
[loading],
);
useEffect(() => {
@@ -191,7 +191,7 @@ export default function File({ chunks: chunks_config }) {
ready = false;
}
},
false
false,
);
req.open('POST', '/api/upload');
@@ -233,7 +233,7 @@ export default function File({ chunks: chunks_config }) {
if (chunks_config.enabled && file.size >= chunks_config.max_size) {
toChunkFiles.push(file);
} else {
body.append('file', files[i]);
body.append('file', files[i], encodeURIComponent(files[i].name));
}
}
@@ -307,7 +307,7 @@ export default function File({ chunks: chunks_config }) {
}
setProgress(0);
},
false
false,
);
if (bodyLength !== 0) {

View File

@@ -213,7 +213,7 @@ export function OptionsModal({
export default function useUploadOptions(): [
UploadOptionsState,
Dispatch<SetStateAction<boolean>>,
ReactNode
ReactNode,
] {
const [state, setState] = useReducer((state, newState) => ({ ...state, ...newState }), {
expires: 'never',

View File

@@ -57,6 +57,7 @@ export interface ConfigUploader {
format_date: string;
default_expiration: string;
assume_mimetypes: boolean;
random_words_separator: string;
}
export interface ConfigUrls {
@@ -135,9 +136,12 @@ export interface ConfigOAuth {
discord_client_id?: string;
discord_client_secret?: string;
discord_redirect_uri?: string;
discord_whitelisted_users?: string[];
google_client_id?: string;
google_client_secret?: string;
google_redirect_uri?: string;
}
export interface ConfigChunks {

View File

@@ -98,6 +98,7 @@ export default function readConfig() {
map('UPLOADER_FORMAT_DATE', 'string', 'uploader.format_date'),
map('UPLOADER_DEFAULT_EXPIRATION', 'string', 'uploader.default_expiration'),
map('UPLOADER_ASSUME_MIMETYPES', 'boolean', 'uploader.assume_mimetypes'),
map('UPLOADER_RANDOM_WORDS_SEPARATOR', 'string', 'uploader.random_words_separator'),
map('URLS_ROUTE', 'string', 'urls.route'),
map('URLS_LENGTH', 'number', 'urls.length'),
@@ -146,9 +147,12 @@ export default function readConfig() {
map('OAUTH_DISCORD_CLIENT_ID', 'string', 'oauth.discord_client_id'),
map('OAUTH_DISCORD_CLIENT_SECRET', 'string', 'oauth.discord_client_secret'),
map('OAUTH_DISCORD_REDIRECT_URI', 'string', 'oauth.discord_redirect_uri'),
map('OAUTH_DISCORD_WHITELISTED_USERS', 'array', 'oauth.discord_whitelisted_users'),
map('OAUTH_GOOGLE_CLIENT_ID', 'string', 'oauth.google_client_id'),
map('OAUTH_GOOGLE_CLIENT_SECRET', 'string', 'oauth.google_client_secret'),
map('OAUTH_GOOGLE_REDIRECT_URI', 'string', 'oauth.google_redirect_uri'),
map('FEATURES_INVITES', 'boolean', 'features.invites'),
map('FEATURES_INVITES_LENGTH', 'number', 'features.invites_length'),

View File

@@ -97,6 +97,7 @@ const validator = s.object({
format_date: s.string.default('YYYY-MM-DD_HH:mm:ss'),
default_expiration: s.string.optional.default(null),
assume_mimetypes: s.boolean.default(false),
random_words_separator: s.string.default('-'),
})
.default({
default_format: 'RANDOM',
@@ -140,11 +141,11 @@ const validator = s.object({
s.object({
label: s.string,
link: s.string,
})
}),
)
.default([
{ label: 'Zipline', link: 'https://github.com/diced/zipline' },
{ label: 'Documentation', link: 'https://zipline.diced.tech/' },
{ label: 'Documentation', link: 'https://zipline.diced.sh/' },
]),
})
.default({
@@ -155,7 +156,7 @@ const validator = s.object({
external_links: [
{ label: 'Zipline', link: 'https://github.com/diced/zipline' },
{ label: 'Documentation', link: 'https://zipline.diced.tech/' },
{ label: 'Documentation', link: 'https://zipline.diced.sh/' },
],
}),
discord: s
@@ -176,9 +177,12 @@ const validator = s.object({
discord_client_id: s.string.nullable.default(null),
discord_client_secret: s.string.nullable.default(null),
discord_redirect_uri: s.string.nullable.default(null),
discord_whitelisted_users: s.string.array.default([]),
google_client_id: s.string.nullable.default(null),
google_client_secret: s.string.nullable.default(null),
google_redirect_uri: s.string.nullable.default(null),
})
.nullish.default(null),
features: s

View File

@@ -49,13 +49,10 @@ export class S3 extends Datasource {
});
}
public size(file: string): Promise<number> {
return new Promise((res) => {
this.s3.statObject(this.config.bucket, file, (err, stat) => {
if (err) res(0);
else res(stat.size);
});
});
public async size(file: string): Promise<number> {
const stat = await this.s3.statObject(this.config.bucket, file);
return stat.size;
}
public async fullSize(): Promise<number> {

View File

@@ -8,7 +8,7 @@ const logger = Logger.get('discord');
export function parseContent(
content: ConfigDiscordContent,
args: ParseValue
args: ParseValue,
): ConfigDiscordContent & { url: string } {
return {
content: content.content ? parseString(content.content, args) : null,

View File

@@ -1,26 +1,41 @@
import { readFile } from 'fs/promises';
import config from 'lib/config';
import Logger from 'lib/logger';
const logger = Logger.get('random_words');
export type GfyCatWords = {
adjectives: string[];
animals: string[];
};
export async function importWords(): Promise<GfyCatWords> {
const adjectives = (await readFile('public/adjectives.txt', 'utf-8')).split('\n');
const animals = (await readFile('public/animals.txt', 'utf-8')).split('\n');
export async function importWords(): Promise<GfyCatWords | null> {
try {
const adjectives = (await readFile('public/adjectives.txt', 'utf-8')).split('\n').map((x) => x.trim());
const animals = (await readFile('public/animals.txt', 'utf-8')).split('\n').map((x) => x.trim());
return {
adjectives,
animals,
};
return {
adjectives,
animals,
};
} catch {
logger.error('public/adjectives.txt or public/animals.txt do not exist, to fix this please retrieve.');
logger.error('to prevent this from happening again, remember to not delete your public/ directory.');
logger.error('file names will use the RANDOM format instead until fixed');
return null;
}
}
function randomWord(words: string[]) {
return words[Math.floor(Math.random() * words.length)];
}
export default async function gfycat() {
export default async function gfycat(): Promise<string | null> {
const words = await importWords();
return `${randomWord(words.adjectives)}${randomWord(words.adjectives)}${randomWord(words.animals)}`;
if (!words) return null;
return `${randomWord(words.adjectives)}${config.uploader.random_words_separator}${randomWord(
words.adjectives,
)}${config.uploader.random_words_separator}${randomWord(words.animals)}`;
}

View File

@@ -19,7 +19,7 @@ export default async function formatFileName(nameFormat: NameFormat, originalNam
return name;
case 'gfycat':
return gfycat();
return gfycat() ?? random();
default:
return random();
}

View File

@@ -7,7 +7,7 @@ export type ApiError = {
export default async function useFetch(
url: string,
method: 'GET' | 'POST' | 'PATCH' | 'DELETE' = 'GET',
body: ApiError | Record<string, unknown> = null
body: ApiError | Record<string, unknown> = null,
) {
const headers = {};
if (body) headers['content-type'] = 'application/json';

View File

@@ -60,8 +60,8 @@ export default class Logger {
this.formatMessage(
LoggerLevel.ERROR,
this.name,
args.map((error) => (typeof error === 'string' ? error : (error as Error).stack)).join(' ')
)
args.map((error) => (typeof error === 'string' ? error : (error as Error).stack)).join(' '),
),
);
return this;

View File

@@ -1,5 +1,5 @@
import config from 'lib/config';
import { notNull } from 'lib/util';
import { isNotNullOrUndefined } from 'lib/util';
import { GetServerSideProps } from 'next';
export type OauthProvider = {
@@ -27,9 +27,15 @@ export type ServerSideProps = {
};
export const getServerSideProps: GetServerSideProps<ServerSideProps> = async (ctx) => {
const ghEnabled = notNull(config.oauth?.github_client_id, config.oauth?.github_client_secret);
const discEnabled = notNull(config.oauth?.discord_client_id, config.oauth?.discord_client_secret);
const googleEnabled = notNull(config.oauth?.google_client_id, config.oauth?.google_client_secret);
const ghEnabled =
isNotNullOrUndefined(config.oauth?.github_client_id) &&
isNotNullOrUndefined(config.oauth?.github_client_secret);
const discEnabled =
isNotNullOrUndefined(config.oauth?.discord_client_id) &&
isNotNullOrUndefined(config.oauth?.discord_client_secret);
const googleEnabled =
isNotNullOrUndefined(config.oauth?.google_client_id) &&
isNotNullOrUndefined(config.oauth?.google_client_secret);
const oauth_providers: OauthProvider[] = [];

View File

@@ -26,7 +26,7 @@ export interface OAuthResponse {
export const withOAuth =
(
provider: 'discord' | 'github' | 'google',
oauth: (query: OAuthQuery, logger: Logger) => Promise<OAuthResponse>
oauth: (query: OAuthQuery, logger: Logger) => Promise<OAuthResponse>,
) =>
async (req: NextApiReq, res: NextApiRes) => {
const logger = Logger.get(`oauth::${provider}`);
@@ -172,7 +172,7 @@ export const withOAuth =
res.setUserCookie(existingOauth.userId);
Logger.get('user').info(
`User ${existingOauth.username} (${existingOauth.id}) logged in via oauth(${provider})`
`User ${existingOauth.username} (${existingOauth.id}) logged in via oauth(${provider})`,
);
return res.redirect('/dashboard');

View File

@@ -66,7 +66,7 @@ export type ZiplineApiConfig = {
export const withZipline =
(
handler: (req: NextApiRequest, res: NextApiResponse, user?: UserExtended) => Promise<unknown>,
api_config: ZiplineApiConfig = { methods: ['GET', 'OPTIONS'] }
api_config: ZiplineApiConfig = { methods: ['GET', 'OPTIONS'] },
) =>
(req: NextApiReq, res: NextApiRes) => {
if (!api_config.methods.includes('OPTIONS')) api_config.methods.push('OPTIONS');
@@ -87,7 +87,7 @@ export const withZipline =
code: 400,
...extra,
},
400
400,
);
};
@@ -99,7 +99,7 @@ export const withZipline =
code: 401,
...extra,
},
401
401,
);
};
@@ -111,7 +111,7 @@ export const withZipline =
code: 403,
...extra,
},
403
403,
);
};
@@ -122,7 +122,7 @@ export const withZipline =
code: 404,
...extra,
},
404
404,
);
};
@@ -136,7 +136,7 @@ export const withZipline =
code: 429,
...extra,
},
429
429,
);
};
@@ -161,7 +161,7 @@ export const withZipline =
path: '/',
expires: new Date(1),
maxAge: undefined,
})
}),
);
};
@@ -230,7 +230,7 @@ export const withZipline =
error: 'method not allowed',
code: 405,
},
405
405,
);
}

View File

@@ -16,9 +16,9 @@ export const github_auth = {
};
export const discord_auth = {
oauth_url: (clientId: string, origin: string, state?: string) =>
oauth_url: (clientId: string, origin: string, state?: string, redirect_uri?: string) =>
`https://discord.com/api/oauth2/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(
`${origin}/api/auth/oauth/discord`
redirect_uri || `${origin}/api/auth/oauth/discord`,
)}&response_type=code&scope=identify${state ? `&state=${state}` : ''}`,
oauth_user: async (access_token: string) => {
const res = await fetch('https://discord.com/api/users/@me', {
@@ -33,15 +33,15 @@ export const discord_auth = {
};
export const google_auth = {
oauth_url: (clientId: string, origin: string, state?: string) =>
oauth_url: (clientId: string, origin: string, state?: string, redirect_uri?: string) =>
`https://accounts.google.com/o/oauth2/auth?client_id=${clientId}&redirect_uri=${encodeURIComponent(
`${origin}/api/auth/oauth/google`
redirect_uri || `${origin}/api/auth/oauth/google`,
)}&response_type=code&access_type=offline&scope=https://www.googleapis.com/auth/userinfo.profile${
state ? `&state=${state}` : ''
}`,
oauth_user: async (access_token: string) => {
const res = await fetch(
`https://people.googleapis.com/v1/people/me?access_token=${access_token}&personFields=names,photos`
`https://people.googleapis.com/v1/people/me?access_token=${access_token}&personFields=names,photos`,
);
if (!res.ok) return null;

View File

@@ -29,7 +29,7 @@ export const useFiles = (query: { [key: string]: string } = {}) => {
...x,
createdAt: new Date(x.createdAt),
expiresAt: x.expiresAt ? new Date(x.expiresAt) : null,
}))
})),
);
});
};
@@ -59,7 +59,7 @@ export const usePaginatedFiles = (page?: number, options?: Partial<PaginatedFile
...x,
createdAt: new Date(x.createdAt),
expiresAt: x.expiresAt ? new Date(x.expiresAt) : null,
}))
})),
);
});
};
@@ -73,7 +73,7 @@ export const useRecent = (filter?: string) => {
...x,
createdAt: new Date(x.createdAt),
expiresAt: x.expiresAt ? new Date(x.expiresAt) : null,
}))
})),
);
});
};
@@ -94,7 +94,7 @@ export function useFileDelete() {
onSuccess: () => {
queryClient.refetchQueries(['files']);
},
}
},
);
}
@@ -114,7 +114,7 @@ export function useFileFavorite() {
onSuccess: () => {
queryClient.refetchQueries(['files']);
},
}
},
);
}

View File

@@ -18,7 +18,7 @@ export const useFolders = (query: { [key: string]: string } = {}) => {
return useQuery<UserFoldersResponse[]>(['folders', queryString], async () => {
return fetch('/api/user/folders?' + queryString).then(
(res) => res.json() as Promise<UserFoldersResponse[]>
(res) => res.json() as Promise<UserFoldersResponse[]>,
);
});
};
@@ -26,7 +26,7 @@ export const useFolders = (query: { [key: string]: string } = {}) => {
export const useFolder = (id: string, withFiles = false) => {
return useQuery<UserFoldersResponse>(['folder', id], async () => {
return fetch('/api/user/folders/' + id + (withFiles ? '?files=true' : '')).then(
(res) => res.json() as Promise<UserFoldersResponse>
(res) => res.json() as Promise<UserFoldersResponse>,
);
});
};

View File

@@ -27,6 +27,6 @@ export const useStats = (amount = 2) => {
},
{
staleTime: 1000 * 60 * 5, // 5 minutes
}
},
);
};

View File

@@ -36,6 +36,6 @@ export function useURLDelete() {
?.filter((u) => u.id !== variables);
queryClient.setQueryData(['urls'], dataWithoutDeleted);
},
}
},
);
}

View File

@@ -15,10 +15,12 @@ export const useVersion = () => {
return useQuery<VersionResponse>(
['version'],
async () => {
return fetch('/api/version').then((res) => res.json());
return fetch('/api/version').then((res) => (res.ok ? res.json() : Promise.reject('')));
},
{
staleTime: Infinity,
}
refetchInterval: false,
refetchOnMount: false,
retry: false,
},
);
};

View File

@@ -36,7 +36,7 @@ export const createSpotlightActions = (router: NextRouter): SpotlightAction[] =>
title: string,
description: string,
link: string,
icon: ReactNode
icon: ReactNode,
): SpotlightAction => {
return actionDo(group, title, description, icon, () => linkTo(link));
};
@@ -46,7 +46,7 @@ export const createSpotlightActions = (router: NextRouter): SpotlightAction[] =>
title: string,
description: string,
icon: ReactNode,
action: () => void
action: () => void,
): SpotlightAction => {
return {
group,
@@ -70,7 +70,7 @@ export const createSpotlightActions = (router: NextRouter): SpotlightAction[] =>
'Manage Account',
'Manage your account settings',
'/dashboard/manage',
<IconUser />
<IconUser />,
),
// Actions
@@ -80,14 +80,14 @@ export const createSpotlightActions = (router: NextRouter): SpotlightAction[] =>
'Upload Files',
'Upload files of any kind',
'/dashboard/upload/file',
<IconFileUpload />
<IconFileUpload />,
),
actionLink(
'Actions',
'Upload Text',
'Upload code, or any other kind of text file',
'/dashboard/upload/text',
<IconFileText />
<IconFileText />,
),
actionDo('Actions', 'Copy Token', 'Copy your API token to your clipboard', <IconClipboardCopy />, () => {
clipboard.copy(user.token);
@@ -99,7 +99,7 @@ export const createSpotlightActions = (router: NextRouter): SpotlightAction[] =>
});
}),
actionLink('Help', 'Documentation', 'View the documentation', 'https://zipline.diced.tech', <IconHelp />),
actionLink('Help', 'Documentation', 'View the documentation', 'https://zipline.diced.sh', <IconHelp />),
// the list of actions here is very incomplete, and will be expanded in the future
];

View File

@@ -120,6 +120,6 @@ export async function getBase64URLFromURL(url: string) {
return `data:${res.headers.get('content-type')};base64,${base64}`;
}
export function notNull(a: unknown, b: unknown) {
return a !== null && b !== null;
export function isNotNullOrUndefined(value: unknown) {
return value !== null && value !== undefined;
}

View File

@@ -125,7 +125,7 @@ export function expireReadToDate(expires: string): Date {
'6m': Date.now() + 6 * 30 * 24 * 60 * 60 * 1000,
'8m': Date.now() + 8 * 30 * 24 * 60 * 60 * 1000,
'1y': Date.now() + 365 * 24 * 60 * 60 * 1000,
}[expires]
}[expires],
);
}

View File

@@ -39,7 +39,7 @@ export function parseString(str: string, value: ParseValue) {
str,
decodeURIComponent(escape(getV[matches.groups.prop])),
matches.index,
re.lastIndex
re.lastIndex,
);
re.lastIndex = matches.index;
continue;

View File

@@ -69,7 +69,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
logger.info(
`Created user ${newUser.username} (${newUser.id}) ${
code ? `from invite code ${code}` : 'via registration'
}`
}`,
);
return res.json({ success: true });

View File

@@ -37,7 +37,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
logger.info(
`${user.username} (${user.id}) created ${data.length} invites with codes ${data
.map((invite) => invite.code)
.join(', ')}`
.join(', ')}`,
);
return res.json(data);

View File

@@ -51,7 +51,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
const success = verify_totp_code(user.totpSecret, code);
logger.debug(
`body(${JSON.stringify(req.body)}): verify_totp_code(${user.totpSecret}, ${code}) => ${success}`
`body(${JSON.stringify(req.body)}): verify_totp_code(${user.totpSecret}, ${code}) => ${success}`,
);
if (!success) return res.badRequest('Invalid code', { totp: true });
}

View File

@@ -3,7 +3,7 @@ import Logger from 'lib/logger';
import { OAuthQuery, OAuthResponse, withOAuth } from 'lib/middleware/withOAuth';
import { withZipline } from 'lib/middleware/withZipline';
import { discord_auth } from 'lib/oauth';
import { getBase64URLFromURL, notNull } from 'lib/util';
import { getBase64URLFromURL, isNotNullOrUndefined } from 'lib/util';
async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
if (!config.features.oauth_registration)
@@ -12,7 +12,10 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi
error: 'oauth registration is disabled',
};
if (!notNull(config.oauth.discord_client_id, config.oauth.discord_client_secret)) {
if (
!isNotNullOrUndefined(config.oauth.discord_client_id) &&
!isNotNullOrUndefined(config.oauth.discord_client_secret)
) {
logger.error('Discord OAuth is not configured');
return {
@@ -26,7 +29,8 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi
redirect: discord_auth.oauth_url(
config.oauth.discord_client_id,
`${config.core.return_https ? 'https' : 'http'}://${host}`,
state
state,
config.oauth.discord_redirect_uri,
),
};
@@ -35,7 +39,9 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi
client_secret: config.oauth.discord_client_secret,
code,
grant_type: 'authorization_code',
redirect_uri: `${config.core.return_https ? 'https' : 'http'}://${host}/api/auth/oauth/discord`,
redirect_uri:
config.oauth.discord_redirect_uri ||
`${config.core.return_https ? 'https' : 'http'}://${host}/api/auth/oauth/discord`,
scope: 'identify',
});
@@ -67,6 +73,12 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi
: `https://cdn.discordapp.com/embed/avatars/${userJson.discriminator % 5}.png`;
const avatarBase64 = await getBase64URLFromURL(avatar);
if (
config.oauth.discord_whitelisted_users?.length &&
!config.oauth.discord_whitelisted_users.includes(userJson.id)
)
return { error: 'user is not whitelisted' };
return {
username: userJson.username,
user_id: userJson.id,

View File

@@ -3,7 +3,7 @@ import Logger from 'lib/logger';
import { OAuthQuery, OAuthResponse, withOAuth } from 'lib/middleware/withOAuth';
import { withZipline } from 'lib/middleware/withZipline';
import { github_auth } from 'lib/oauth';
import { getBase64URLFromURL, notNull } from 'lib/util';
import { getBase64URLFromURL, isNotNullOrUndefined } from 'lib/util';
async function handler({ code, state }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
if (!config.features.oauth_registration)
@@ -12,7 +12,10 @@ async function handler({ code, state }: OAuthQuery, logger: Logger): Promise<OAu
error: 'oauth registration is disabled',
};
if (!notNull(config.oauth.github_client_id, config.oauth.github_client_secret)) {
if (
!isNotNullOrUndefined(config.oauth.github_client_id) &&
!isNotNullOrUndefined(config.oauth.github_client_secret)
) {
logger.error('GitHub OAuth is not configured');
return {
error_code: 401,

View File

@@ -3,7 +3,7 @@ import Logger from 'lib/logger';
import { OAuthQuery, OAuthResponse, withOAuth } from 'lib/middleware/withOAuth';
import { withZipline } from 'lib/middleware/withZipline';
import { google_auth } from 'lib/oauth';
import { getBase64URLFromURL, notNull } from 'lib/util';
import { getBase64URLFromURL, isNotNullOrUndefined } from 'lib/util';
async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
if (!config.features.oauth_registration)
@@ -12,7 +12,10 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi
error: 'oauth registration is disabled',
};
if (!notNull(config.oauth.google_client_id, config.oauth.google_client_secret)) {
if (
!isNotNullOrUndefined(config.oauth.google_client_id) &&
!isNotNullOrUndefined(config.oauth.google_client_secret)
) {
logger.error('Google OAuth is not configured');
return {
error_code: 401,
@@ -25,7 +28,8 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi
redirect: google_auth.oauth_url(
config.oauth.google_client_id,
`${config.core.return_https ? 'https' : 'http'}://${host}`,
state
state,
config.oauth.google_redirect_uri,
),
};
@@ -33,7 +37,9 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi
code,
client_id: config.oauth.google_client_id,
client_secret: config.oauth.google_client_secret,
redirect_uri: `${config.core.return_https ? 'https' : 'http'}://${host}/api/auth/oauth/google`,
redirect_uri:
config.oauth.google_redirect_uri ||
`${config.core.return_https ? 'https' : 'http'}://${host}/api/auth/oauth/google`,
grant_type: 'authorization_code',
});

View File

@@ -26,7 +26,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
if (!image) return res.notFound('image not found');
logger.info(
`${user.username} (${user.id}) requested to read exif metadata for image ${image.name} (${image.id})`
`${user.username} (${user.id}) requested to read exif metadata for image ${image.name} (${image.id})`,
);
if (config.datasource.type === 'local') {

View File

@@ -80,6 +80,16 @@ async function handler(req: NextApiReq, res: NextApiRes) {
// handle partial uploads before ratelimits
if (req.headers['content-range'] && zconfig.chunks.enabled) {
if (format === 'name') {
const existing = await prisma.file.findFirst({
where: {
name: req.headers['x-zipline-partial-filename'] as string,
},
});
if (existing) return res.badRequest('filename already exists (conflict: NAME format)');
}
// parses content-range header (bytes start-end/total)
const [start, end, total] = req.headers['content-range']
.replace('bytes ', '')
@@ -101,7 +111,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
start,
end,
total,
})}`
})}`,
);
const tempFile = join(zconfig.core.temp_directory, `zipline_partial_${identifier}_${start}_${end}`);
@@ -193,29 +203,32 @@ async function handler(req: NextApiReq, res: NextApiRes) {
mimetype: x.mimetype,
size: x.size,
encoding: x.encoding,
}))
)}`
})),
)}`,
);
for (let i = 0; i !== req.files.length; ++i) {
const file = req.files[i];
if (file.size > zconfig.uploader[user.administrator ? 'admin_limit' : 'user_limit'])
return res.badRequest(`file[${i}]: size too big`);
if (!file.originalname) return res.badRequest(`file[${i}]: no filename`);
const ext = file.originalname.split('.').length === 1 ? '' : file.originalname.split('.').pop();
const decodedName = decodeURI(file.originalname);
const ext = decodedName.split('.').length === 1 ? '' : decodedName.split('.').pop();
if (zconfig.uploader.disabled_extensions.includes(ext))
return res.badRequest(`file[${i}]: disabled extension recieved: ${ext}`);
let fileName = await formatFileName(format, file.originalname);
const fileName = await formatFileName(format, decodedName);
if (req.headers['x-zipline-filename']) {
fileName = req.headers['x-zipline-filename'] as string;
if (format === 'name' || req.headers['x-zipline-filename']) {
const exist = (req.headers['x-zipline-filename'] as string) || decodedName;
const existing = await prisma.file.findFirst({
where: {
name: fileName,
name: exist,
},
});
if (existing) return res.badRequest(`file[${i}]: filename already exists: '${fileName}'`);
if (existing) return res.badRequest(`file[${i}]: filename already exists: '${decodedName}'`);
}
let password = null;
@@ -226,7 +239,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
let mimetype = file.mimetype;
if (file.mimetype === 'application/octet-stream' && zconfig.uploader.assume_mimetypes) {
const ext = parse(file.originalname).ext.replace('.', '');
const ext = parse(decodedName).ext.replace('.', '');
const mime = await guess(ext);
if (!mime) response.assumed_mimetype = false;
@@ -247,7 +260,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
password,
expiresAt: expiry,
maxViews: fileMaxViews,
originalName: req.headers['original-name'] ? file.originalname ?? null : null,
originalName: req.headers['original-name'] ? decodedName ?? null : null,
size: file.size,
},
});
@@ -259,7 +272,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
const buffer = await sharp(file.buffer).jpeg({ quality: imageCompressionPercent }).toBuffer();
await datasource.save(fileUpload.name, buffer);
logger.info(
`User ${user.username} (${user.id}) compressed image from ${file.buffer.length} -> ${buffer.length} bytes`
`User ${user.username} (${user.id}) compressed image from ${file.buffer.length} -> ${buffer.length} bytes`,
);
} else {
await datasource.save(fileUpload.name, file.buffer);
@@ -286,7 +299,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
user,
fileUpload,
`${domain}/r/${invis ? invis.invis : encodeURI(fileUpload.name)}`,
responseUrl
responseUrl,
);
}

View File

@@ -36,7 +36,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
promises.push(
prisma.user.delete({
where: { id: target.id },
})
}),
);
if (req.body.delete_files) {
@@ -61,7 +61,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
where: {
userId: target.id,
},
})
}),
);
}
Promise.all(promises).then((promised) => {
@@ -71,10 +71,10 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
req.body.delete_files
? logger.info(
`User ${user.username} (${user.id}) deleted ${count} files of user ${newTarget.username} (${newTarget.id})`
`User ${user.username} (${user.id}) deleted ${count} files of user ${newTarget.username} (${newTarget.id})`,
)
: logger.info(
`User ${user.username} (${user.id}) deleted user ${newTarget.username} (${newTarget.id})`
`User ${user.username} (${user.id}) deleted user ${newTarget.username} (${newTarget.id})`,
);
delete newTarget.password;
@@ -177,7 +177,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
logger.debug(`updated user ${id} with ${JSON.stringify(newUser, jsonUserReplacer)}`);
logger.info(
`User ${user.username} (${user.id}) updated ${target.username} (${newUser.username}) (${newUser.id})`
`User ${user.username} (${user.id}) updated ${target.username} (${newUser.username}) (${newUser.id})`,
);
delete newUser.password;

View File

@@ -77,7 +77,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
write_stream.close();
logger.debug(`finished writing zip to ${path} at ${data.length} bytes written`);
logger.info(
`Export for ${user.username} (${user.id}) has completed and is available at ${export_name}`
`Export for ${user.username} (${user.id}) has completed and is available at ${export_name}`,
);
}
} else {

View File

@@ -76,7 +76,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
if (file.thumbnail?.name) await datasource.delete(file.thumbnail.name);
logger.info(
`User ${user.username} (${user.id}) deleted an image ${file.name} (${file.id}) owned by ${file.user.username} (${file.user.id})`
`User ${user.username} (${user.id}) deleted an image ${file.name} (${file.id}) owned by ${file.user.username} (${file.user.id})`,
);
// @ts-ignore
@@ -139,7 +139,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
expiresAt: Date;
maxViews: number;
views: number;
size: number;
size: bigint;
originalName: string;
thumbnail?: { name: string };
}[] = await prisma.file.findMany({

View File

@@ -83,7 +83,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
logger.debug(`added file ${fileIdParsed} to folder ${idParsed}`);
logger.info(
`Added file "${file.name}" to folder "${folder.name}" for user ${user.username} (${user.id})`
`Added file "${file.name}" to folder "${folder.name}" for user ${user.username} (${user.id})`,
);
if (req.query.files) {
@@ -94,7 +94,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
(folder.files[i] as unknown as { url: string }).url = formatRootUrl(
config.uploader.route,
folder.files[i].name
folder.files[i].name,
);
}
}
@@ -129,7 +129,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
(folder.files[i] as unknown as { url: string }).url = formatRootUrl(
config.uploader.route,
folder.files[i].name
folder.files[i].name,
);
}
}
@@ -213,7 +213,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
logger.debug(`removed file ${fileIdParsed} from folder ${idParsed}`);
logger.info(
`Removed file "${file.name}" from folder "${folder.name}" for user ${user.username} (${user.id})`
`Removed file "${file.name}" from folder "${folder.name}" for user ${user.username} (${user.id})`,
);
if (req.query.files) {
@@ -224,7 +224,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
(folder.files[i] as unknown as { url: string }).url = formatRootUrl(
config.uploader.route,
folder.files[i].name
folder.files[i].name,
);
}
}
@@ -240,7 +240,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
(folder.files[i] as unknown as { url: string }).url = formatRootUrl(
config.uploader.route,
folder.files[i].name
folder.files[i].name,
);
}
}

View File

@@ -25,7 +25,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
if (files.length !== add.length)
return res.badRequest(
`files ${add.filter((id) => !files.find((file) => file.id === Number(id))).join(', ')} not found`
`files ${add.filter((id) => !files.find((file) => file.id === Number(id))).join(', ')} not found`,
);
const folder = await prisma.folder.create({
@@ -87,7 +87,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
(folder.files[j] as unknown as { url: string }).url = formatRootUrl(
config.uploader.route,
folder.files[j].name
folder.files[j].name,
);
}
}

View File

@@ -36,7 +36,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
error: 'oauth token expired',
redirect_uri: discord_auth.oauth_url(
zconfig.oauth.discord_client_id,
`${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}`
`${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}`,
),
});
}
@@ -60,7 +60,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
error: 'oauth token expired',
redirect_uri: discord_auth.oauth_url(
zconfig.oauth.discord_client_id,
`${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}`
`${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}`,
),
});
}
@@ -80,7 +80,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
const resp = await fetch(
`https://people.googleapis.com/v1/people/me?access_token=${
user.oauth.find((o) => o.provider === 'GOOGLE').token
}&personFields=names,photos`
}&personFields=names,photos`,
);
if (!resp.ok) {
const provider = user.oauth.find((o) => o.provider === 'GOOGLE');
@@ -91,7 +91,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
error: 'oauth token expired',
redirect_uri: google_auth.oauth_url(
zconfig.oauth.google_client_id,
`${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}`
`${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}`,
),
});
}
@@ -114,7 +114,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
error: 'oauth token expired',
redirect_uri: google_auth.oauth_url(
zconfig.oauth.google_client_id,
`${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}`
`${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}`,
),
});
}

View File

@@ -44,7 +44,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
logger.debug(
`body(${JSON.stringify(req.body)}): verify_totp_code(${user.totpSecret}, ${
req.body.code
}) => ${success}`
}) => ${success}`,
);
if (!success) return res.badRequest('Invalid code');

View File

@@ -15,7 +15,7 @@ const sortByValidator = s.enum(
'size',
'name',
'mimetype',
] satisfies (keyof Prisma.FileOrderByWithRelationInput)[])
] satisfies (keyof Prisma.FileOrderByWithRelationInput)[]),
);
const orderValidator = s.enum('asc', 'desc');
@@ -83,7 +83,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
maxViews: number;
views: number;
folderId: number;
size: number;
size: bigint;
password: string | boolean;
thumbnail?: { name: string };
}[] = await prisma.file.findMany({

View File

@@ -38,7 +38,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
for (let i = 0; i !== urls.length; ++i) {
(urls[i] as unknown as { url: string }).url = formatRootUrl(
config.urls.route,
urls[i].vanity ?? urls[i].id
urls[i].vanity ?? urls[i].id,
);
}
return res.json(urls);

View File

@@ -7,7 +7,7 @@ async function handler(_: NextApiReq, res: NextApiRes) {
const pkg = JSON.parse(await readFile('package.json', 'utf8'));
const re = await fetch('https://zipline.diced.tech/api/version?c=' + pkg.version);
const re = await fetch('https://zipline.diced.sh/api/version?c=' + pkg.version);
const json = await re.json();
let updateToType = 'stable';

View File

@@ -12,6 +12,7 @@ type LimitedFolder = {
createdAt: Date | string;
mimetype: string;
views: number;
size: bigint;
}[];
user: {
username: string;
@@ -83,6 +84,7 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
views: true,
createdAt: true,
password: true,
size: true,
},
},
user: {
@@ -101,7 +103,7 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
for (let j = 0; j !== folder.files.length; ++j) {
(folder.files[j] as unknown as { url: string }).url = formatRootUrl(
config.uploader.route,
folder.files[j].name
folder.files[j].name,
);
// @ts-ignore

View File

@@ -57,7 +57,7 @@ export default function EmbeddedFile({
img.addEventListener('load', function () {
if (this.naturalWidth > innerWidth)
imageEl.width = Math.floor(
this.naturalWidth * Math.min(innerHeight / this.naturalHeight, innerWidth / this.naturalWidth)
this.naturalWidth * Math.min(innerHeight / this.naturalHeight, innerWidth / this.naturalWidth),
);
else imageEl.width = this.naturalWidth;
});
@@ -128,6 +128,7 @@ export default function EmbeddedFile({
</>
)}
<meta property='og:type' content={'video.other'} />
<meta property='og:url' content={`${host}/r/${file.name}`} />
<meta property='og:video' content={`${host}/r/${file.name}`} />
<meta property='og:video:url' content={`${host}/r/${file.name}`} />

View File

@@ -12,7 +12,7 @@ async function main() {
}
const files = (await readdir(temp)).filter(
(x) => x.startsWith('zipline_partial_') || x.startsWith('zipline_thumb_')
(x) => x.startsWith('zipline_partial_') || x.startsWith('zipline_thumb_'),
);
if (files.length === 0) {
console.log('No partial files found, exiting..');

View File

@@ -1,10 +1,12 @@
import { PrismaClient } from '@prisma/client';
import { readdir, readFile } from 'fs/promises';
import { statSync } from 'fs';
import { join } from 'path';
import config from 'lib/config';
import datasource from 'lib/datasource';
import { guess } from 'lib/mimes';
import { migrations } from 'server/util';
import { bytesToHuman } from 'lib/utils/bytes';
async function main() {
const directory = process.argv[2];
@@ -25,13 +27,16 @@ async function main() {
for (let i = 0; i !== files.length; ++i) {
const mime = await guess(files[i].split('.').pop());
const { size } = statSync(join(directory, files[i]));
data.push({
name: files[i],
mimetype: mime,
userId,
size,
});
console.log(`Imported ${files[i]} (${mime} mimetype) to user ${userId}`);
console.log(`Imported ${files[i]} (${bytesToHuman(size)}) (${mime} mimetype) to user ${userId}`);
}
process.env.DATABASE_URL = config.core.database_url;

View File

@@ -64,7 +64,7 @@ async function main() {
notFound
? console.log(
'At least one file has been found to not exist in the datasource but was on the database. To remove these files, run the script with the --force-delete flag.'
'At least one file has been found to not exist in the datasource but was on the database. To remove these files, run the script with the --force-delete flag.',
)
: console.log('Done.');

View File

@@ -20,7 +20,7 @@ function dbFileDecorator(fastify: FastifyInstance, _, done) {
this.header('Content-Length', size);
this.header('Content-Type', download ? 'application/octet-stream' : file.mimetype);
this.header('Content-Disposition', `inline; filename="${file.originalName || file.name}"`);
this.header('Content-Disposition', `inline; filename="${encodeURI(file.originalName || file.name)}"`);
return this.send(data);
}

View File

@@ -3,7 +3,7 @@ import { FastifyInstance, FastifyReply } from 'fastify';
import fastifyPlugin from 'fastify-plugin';
function postUrlDecorator(fastify: FastifyInstance, _, done) {
fastify.decorateReply('postUrl', postUrl);
fastify.decorateReply('postUrl', postUrl.bind(fastify));
done();
async function postUrl(this: FastifyReply, url: Url) {

View File

@@ -28,6 +28,12 @@ const logger = Logger.get('server');
const server = fastify(genFastifyOpts());
// Normally I would never condone this, but I lack the patience to deal with this correctly.
// This is just to get JSON.stringify to globally serialize BigInt's
BigInt.prototype['toJSON'] = function () {
return Number(this);
};
if (dev) {
server.addHook('onRoute', (opts) => {
logger.child('route').debug(JSON.stringify(opts));
@@ -87,7 +93,7 @@ async function start() {
url: req.url,
headers: req.headers,
body: req.headers['content-type']?.startsWith('application/json') ? req.body : undefined,
})
}),
);
}
@@ -179,7 +185,7 @@ Disallow: ${config.urls.route}
.info(
`started ${dev ? 'development' : 'production'} zipline@${version} server${
config.features.headless ? ' (headless)' : ''
}`
}`,
);
await clearInvites.bind(server)();

View File

@@ -11,14 +11,14 @@ async function configPlugin(fastify: FastifyInstance, config: Config) {
fastify.logger
.error('Secret is not set!')
.error(
'Running Zipline as is, without a randomized secret is not recommended and leaves your instance at risk!'
'Running Zipline as is, without a randomized secret is not recommended and leaves your instance at risk!',
)
.error('Please change your secret in the config file or environment variables.')
.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.'
'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. If you include special characters, surround the secret with quotes.'
'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.');
@@ -41,7 +41,7 @@ async function configPlugin(fastify: FastifyInstance, config: Config) {
.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.'
'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.');

View File

@@ -3,15 +3,13 @@ import { FastifyInstance } from 'fastify';
import fastifyPlugin from 'fastify-plugin';
import { migrations } from 'server/util';
async function prismaPlugin(fastify: FastifyInstance, _, done) {
async function prismaPlugin(fastify: FastifyInstance) {
process.env.DATABASE_URL = fastify.config.core?.database_url;
await migrations();
const prisma = new PrismaClient();
fastify.decorate('prisma', prisma);
done();
}
export default fastifyPlugin(prismaPlugin, {

View File

@@ -9,7 +9,7 @@ export default async function uploadsRoute(this: FastifyInstance, req: FastifyRe
const image = await this.prisma.file.findFirst({
where: {
OR: [{ name: id }, { invisible: { invis: decodeURI(encodeURI(id)) } }],
OR: [{ name: id }, { name: decodeURI(id) }, { invisible: { invis: decodeURI(encodeURI(id)) } }],
},
});
if (!image) return reply.rawFile(id);
@@ -28,7 +28,7 @@ export async function uploadsRouteOnResponse(
this: FastifyInstance,
req: FastifyRequest,
reply: FastifyReply,
done: () => void
done: () => void,
) {
if (reply.statusCode === 200) {
const { id } = req.params as { id: string };

View File

@@ -22,7 +22,7 @@ export async function urlsRouteOnResponse(
this: FastifyInstance,
req: FastifyRequest,
reply: FastifyReply,
done: () => void
done: () => void,
) {
if (reply.statusCode === 200) {
const { id } = req.params as { id: string };

View File

@@ -59,7 +59,7 @@ export async function migrations() {
} catch (error) {
if (error.message.startsWith('P1001')) {
logger.error(
`Unable to connect to database \`${process.env.DATABASE_URL}\`, check your database connection`
`Unable to connect to database \`${process.env.DATABASE_URL}\`, check your database connection`,
);
logger.debug(error);
} else {

View File

@@ -59,7 +59,7 @@ async function start() {
logger.debug('starting worker');
const partials = await readdir(config.core.temp_directory).then((files) =>
files.filter((x) => x.startsWith(`zipline_partial_${file.identifier}`))
files.filter((x) => x.startsWith(`zipline_partial_${file.identifier}`)),
);
const readChunks = partials.map((x) => {

8398
yarn.lock

File diff suppressed because it is too large Load Diff