Compare commits

..

15 Commits

Author SHA1 Message Date
dicedtomato
5e6d28deac Merge branch 'trunk' into drizzle 2025-09-19 20:29:44 -07:00
dicedtomato
590e46f18e Merge branch 'trunk' into drizzle 2025-09-18 12:42:29 -07:00
dicedtomato
2eaee1a92e Merge branch 'trunk' into drizzle 2025-09-18 12:35:59 -07:00
diced
4758bd145e fix: accidental force push lmaoo (#875)
PR: #875
2025-09-18 12:35:00 -07:00
dicedtomato
8487e07006 Merge branch 'trunk' into drizzle 2025-09-09 17:06:05 -07:00
TheShadowEevee
83246d6a4b fix: increase TLD length regex (#886)
30 matches the Second Level Domain length limit. This should allow for TLDs longer than 2 or 3 characters, such as .solutions

Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
2025-09-09 16:50:46 -07:00
dicedtomato
9df12e141f Merge branch 'trunk' into drizzle 2025-09-08 23:08:50 -07:00
dicedtomato
02e25aa608 Merge branch 'trunk' into drizzle 2025-09-08 23:07:03 -07:00
dicedtomato
cf570af0a8 Merge branch 'trunk' into drizzle 2025-09-08 15:59:39 -07:00
dicedtomato
6644eac0ed Merge branch 'trunk' into drizzle 2025-09-08 12:13:09 -07:00
diced
a14337bdd4 fix: lock resolution 2025-09-06 12:56:58 -07:00
dicedtomato
ab4b9c4ac1 Merge branch 'trunk' into drizzle 2025-09-06 12:55:57 -07:00
dicedtomato
40f7d39426 Merge branch 'trunk' into drizzle 2025-09-05 21:07:54 -07:00
diced
34f27d4da4 feat: add drizzle packages 2025-09-05 19:41:47 -07:00
diced
7b2af8b8c5 feat: initial drizzle setup 2025-09-04 22:05:21 -07:00
24 changed files with 3589 additions and 504 deletions

13
drizzle.config.ts Normal file
View File

@@ -0,0 +1,13 @@
import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
dialect: 'postgresql',
out: './src/drizzle',
schema: './src/drizzle/schema.ts',
dbCredentials: {
url: process.env.DATABASE_URL as string,
},
verbose: true,
strict: true,
});

View File

@@ -2,7 +2,7 @@
"name": "zipline",
"private": true,
"license": "MIT",
"version": "4.3.2",
"version": "4.3.1",
"scripts": {
"build": "tsx scripts/build.ts",
"dev": "cross-env NODE_ENV=development DEBUG=zipline tsx --require dotenv/config --enable-source-maps ./src/server",
@@ -23,9 +23,6 @@
"dependencies": {
"@aws-sdk/client-s3": "3.726.1",
"@aws-sdk/lib-storage": "3.726.1",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@fastify/cookie": "^11.0.2",
"@fastify/cors": "^11.1.0",
"@fastify/multipart": "^9.0.3",
@@ -59,6 +56,7 @@
"cross-env": "^10.0.0",
"dayjs": "^1.11.18",
"dotenv": "^17.2.2",
"drizzle-orm": "^0.44.5",
"fast-glob": "^3.3.3",
"fastify": "^5.5.0",
"fastify-plugin": "^5.0.1",
@@ -72,6 +70,7 @@
"ms": "^2.1.3",
"multer": "2.0.2",
"otplib": "^12.0.1",
"pg": "^8.16.3",
"prisma": "6.13.0",
"qrcode": "^1.5.4",
"react": "^19.1.1",
@@ -93,10 +92,12 @@
"@types/ms": "^2.1.0",
"@types/multer": "^2.0.0",
"@types/node": "^24.3.0",
"@types/pg": "^8.15.5",
"@types/qrcode": "^1.5.5",
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^5.0.2",
"drizzle-kit": "^0.31.4",
"eslint": "^9.34.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-jsx-a11y": "^6.10.2",

462
pnpm-lock.yaml generated
View File

@@ -14,15 +14,6 @@ importers:
'@aws-sdk/lib-storage':
specifier: 3.726.1
version: 3.726.1(@aws-sdk/client-s3@3.726.1)
'@dnd-kit/core':
specifier: ^6.3.1
version: 6.3.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@dnd-kit/sortable':
specifier: ^10.0.0
version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)
'@dnd-kit/utilities':
specifier: ^3.2.2
version: 3.2.2(react@19.1.1)
'@fastify/cookie':
specifier: ^11.0.2
version: 11.0.2
@@ -122,6 +113,9 @@ importers:
dotenv:
specifier: ^17.2.2
version: 17.2.2
drizzle-orm:
specifier: ^0.44.5
version: 0.44.5(@prisma/client@6.13.0(prisma@6.13.0(typescript@5.9.2))(typescript@5.9.2))(@types/pg@8.15.5)(pg@8.16.3)(prisma@6.13.0(typescript@5.9.2))
fast-glob:
specifier: ^3.3.3
version: 3.3.3
@@ -161,6 +155,9 @@ importers:
otplib:
specifier: ^12.0.1
version: 12.0.1
pg:
specifier: ^8.16.3
version: 8.16.3
prisma:
specifier: 6.13.0
version: 6.13.0(typescript@5.9.2)
@@ -219,6 +216,9 @@ importers:
'@types/node':
specifier: ^24.3.0
version: 24.3.0
'@types/pg':
specifier: ^8.15.5
version: 8.15.5
'@types/qrcode':
specifier: ^1.5.5
version: 1.5.5
@@ -231,6 +231,9 @@ importers:
'@vitejs/plugin-react':
specifier: ^5.0.2
version: 5.0.2(vite@7.1.4(@types/node@24.3.0)(jiti@2.5.1)(sass@1.92.0)(sugarss@5.0.1(postcss@8.5.6))(tsx@4.20.5))
drizzle-kit:
specifier: ^0.31.4
version: 0.31.4
eslint:
specifier: ^9.34.0
version: 9.34.0(jiti@2.5.1)
@@ -588,27 +591,8 @@ packages:
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
engines: {node: '>=18'}
'@dnd-kit/accessibility@3.1.1':
resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==}
peerDependencies:
react: '>=16.8.0'
'@dnd-kit/core@6.3.1':
resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@dnd-kit/sortable@10.0.0':
resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==}
peerDependencies:
'@dnd-kit/core': ^6.3.0
react: '>=16.8.0'
'@dnd-kit/utilities@3.2.2':
resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==}
peerDependencies:
react: '>=16.8.0'
'@drizzle-team/brocli@0.10.2':
resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
'@emnapi/runtime@1.4.5':
resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==}
@@ -616,6 +600,14 @@ packages:
'@epic-web/invariant@1.0.0':
resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==}
'@esbuild-kit/core-utils@3.3.2':
resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==}
deprecated: 'Merged into tsx: https://tsx.is'
'@esbuild-kit/esm-loader@2.6.5':
resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==}
deprecated: 'Merged into tsx: https://tsx.is'
'@esbuild/aix-ppc64@0.25.5':
resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==}
engines: {node: '>=18'}
@@ -628,6 +620,12 @@ packages:
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.18.20':
resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
engines: {node: '>=12'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm64@0.25.5':
resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==}
engines: {node: '>=18'}
@@ -640,6 +638,12 @@ packages:
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.18.20':
resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
engines: {node: '>=12'}
cpu: [arm]
os: [android]
'@esbuild/android-arm@0.25.5':
resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==}
engines: {node: '>=18'}
@@ -652,6 +656,12 @@ packages:
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.18.20':
resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
engines: {node: '>=12'}
cpu: [x64]
os: [android]
'@esbuild/android-x64@0.25.5':
resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==}
engines: {node: '>=18'}
@@ -664,6 +674,12 @@ packages:
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.18.20':
resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-arm64@0.25.5':
resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==}
engines: {node: '>=18'}
@@ -676,6 +692,12 @@ packages:
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.18.20':
resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [darwin]
'@esbuild/darwin-x64@0.25.5':
resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==}
engines: {node: '>=18'}
@@ -688,6 +710,12 @@ packages:
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.18.20':
resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
engines: {node: '>=12'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-arm64@0.25.5':
resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==}
engines: {node: '>=18'}
@@ -700,6 +728,12 @@ packages:
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.18.20':
resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [freebsd]
'@esbuild/freebsd-x64@0.25.5':
resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==}
engines: {node: '>=18'}
@@ -712,6 +746,12 @@ packages:
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.18.20':
resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm64@0.25.5':
resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==}
engines: {node: '>=18'}
@@ -724,6 +764,12 @@ packages:
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.18.20':
resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
engines: {node: '>=12'}
cpu: [arm]
os: [linux]
'@esbuild/linux-arm@0.25.5':
resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==}
engines: {node: '>=18'}
@@ -736,6 +782,12 @@ packages:
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.18.20':
resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
engines: {node: '>=12'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-ia32@0.25.5':
resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==}
engines: {node: '>=18'}
@@ -748,6 +800,12 @@ packages:
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.18.20':
resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
engines: {node: '>=12'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-loong64@0.25.5':
resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==}
engines: {node: '>=18'}
@@ -760,6 +818,12 @@ packages:
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.18.20':
resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
engines: {node: '>=12'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-mips64el@0.25.5':
resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==}
engines: {node: '>=18'}
@@ -772,6 +836,12 @@ packages:
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.18.20':
resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-ppc64@0.25.5':
resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==}
engines: {node: '>=18'}
@@ -784,6 +854,12 @@ packages:
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.18.20':
resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
engines: {node: '>=12'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-riscv64@0.25.5':
resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==}
engines: {node: '>=18'}
@@ -796,6 +872,12 @@ packages:
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.18.20':
resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
engines: {node: '>=12'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-s390x@0.25.5':
resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==}
engines: {node: '>=18'}
@@ -808,6 +890,12 @@ packages:
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.18.20':
resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
engines: {node: '>=12'}
cpu: [x64]
os: [linux]
'@esbuild/linux-x64@0.25.5':
resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==}
engines: {node: '>=18'}
@@ -832,6 +920,12 @@ packages:
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.18.20':
resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
engines: {node: '>=12'}
cpu: [x64]
os: [netbsd]
'@esbuild/netbsd-x64@0.25.5':
resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==}
engines: {node: '>=18'}
@@ -856,6 +950,12 @@ packages:
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.18.20':
resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
engines: {node: '>=12'}
cpu: [x64]
os: [openbsd]
'@esbuild/openbsd-x64@0.25.5':
resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==}
engines: {node: '>=18'}
@@ -874,6 +974,12 @@ packages:
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.18.20':
resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [sunos]
'@esbuild/sunos-x64@0.25.5':
resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==}
engines: {node: '>=18'}
@@ -886,6 +992,12 @@ packages:
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.18.20':
resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
engines: {node: '>=12'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-arm64@0.25.5':
resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==}
engines: {node: '>=18'}
@@ -898,6 +1010,12 @@ packages:
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.18.20':
resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
engines: {node: '>=12'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-ia32@0.25.5':
resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==}
engines: {node: '>=18'}
@@ -910,6 +1028,12 @@ packages:
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.18.20':
resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [win32]
'@esbuild/win32-x64@0.25.5':
resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==}
engines: {node: '>=18'}
@@ -2044,6 +2168,9 @@ packages:
'@types/normalize-package-data@2.4.4':
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
'@types/pg@8.15.5':
resolution: {integrity: sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==}
'@types/qrcode@1.5.5':
resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==}
@@ -2663,6 +2790,102 @@ packages:
resolution: {integrity: sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==}
engines: {node: '>=12'}
drizzle-kit@0.31.4:
resolution: {integrity: sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA==}
hasBin: true
drizzle-orm@0.44.5:
resolution: {integrity: sha512-jBe37K7d8ZSKptdKfakQFdeljtu3P2Cbo7tJoJSVZADzIKOBo9IAJPOmMsH2bZl90bZgh8FQlD8BjxXA/zuBkQ==}
peerDependencies:
'@aws-sdk/client-rds-data': '>=3'
'@cloudflare/workers-types': '>=4'
'@electric-sql/pglite': '>=0.2.0'
'@libsql/client': '>=0.10.0'
'@libsql/client-wasm': '>=0.10.0'
'@neondatabase/serverless': '>=0.10.0'
'@op-engineering/op-sqlite': '>=2'
'@opentelemetry/api': ^1.4.1
'@planetscale/database': '>=1.13'
'@prisma/client': '*'
'@tidbcloud/serverless': '*'
'@types/better-sqlite3': '*'
'@types/pg': '*'
'@types/sql.js': '*'
'@upstash/redis': '>=1.34.7'
'@vercel/postgres': '>=0.8.0'
'@xata.io/client': '*'
better-sqlite3: '>=7'
bun-types: '*'
expo-sqlite: '>=14.0.0'
gel: '>=2'
knex: '*'
kysely: '*'
mysql2: '>=2'
pg: '>=8'
postgres: '>=3'
prisma: '*'
sql.js: '>=1'
sqlite3: '>=5'
peerDependenciesMeta:
'@aws-sdk/client-rds-data':
optional: true
'@cloudflare/workers-types':
optional: true
'@electric-sql/pglite':
optional: true
'@libsql/client':
optional: true
'@libsql/client-wasm':
optional: true
'@neondatabase/serverless':
optional: true
'@op-engineering/op-sqlite':
optional: true
'@opentelemetry/api':
optional: true
'@planetscale/database':
optional: true
'@prisma/client':
optional: true
'@tidbcloud/serverless':
optional: true
'@types/better-sqlite3':
optional: true
'@types/pg':
optional: true
'@types/sql.js':
optional: true
'@upstash/redis':
optional: true
'@vercel/postgres':
optional: true
'@xata.io/client':
optional: true
better-sqlite3:
optional: true
bun-types:
optional: true
expo-sqlite:
optional: true
gel:
optional: true
knex:
optional: true
kysely:
optional: true
mysql2:
optional: true
pg:
optional: true
postgres:
optional: true
prisma:
optional: true
sql.js:
optional: true
sqlite3:
optional: true
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@@ -2722,6 +2945,16 @@ packages:
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
engines: {node: '>= 0.4'}
esbuild-register@3.6.0:
resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==}
peerDependencies:
esbuild: '>=0.12 <1'
esbuild@0.18.20:
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
engines: {node: '>=12'}
hasBin: true
esbuild@0.25.5:
resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==}
engines: {node: '>=18'}
@@ -4429,6 +4662,13 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
source-map-support@0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
source-map@0.8.0-beta.0:
resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
engines: {node: '>= 8'}
@@ -5687,30 +5927,7 @@ snapshots:
'@csstools/css-tokenizer@3.0.4': {}
'@dnd-kit/accessibility@3.1.1(react@19.1.1)':
dependencies:
react: 19.1.1
tslib: 2.8.1
'@dnd-kit/core@6.3.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
dependencies:
'@dnd-kit/accessibility': 3.1.1(react@19.1.1)
'@dnd-kit/utilities': 3.2.2(react@19.1.1)
react: 19.1.1
react-dom: 19.1.1(react@19.1.1)
tslib: 2.8.1
'@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)':
dependencies:
'@dnd-kit/core': 6.3.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@dnd-kit/utilities': 3.2.2(react@19.1.1)
react: 19.1.1
tslib: 2.8.1
'@dnd-kit/utilities@3.2.2(react@19.1.1)':
dependencies:
react: 19.1.1
tslib: 2.8.1
'@drizzle-team/brocli@0.10.2': {}
'@emnapi/runtime@1.4.5':
dependencies:
@@ -5719,102 +5936,160 @@ snapshots:
'@epic-web/invariant@1.0.0': {}
'@esbuild-kit/core-utils@3.3.2':
dependencies:
esbuild: 0.18.20
source-map-support: 0.5.21
'@esbuild-kit/esm-loader@2.6.5':
dependencies:
'@esbuild-kit/core-utils': 3.3.2
get-tsconfig: 4.10.1
'@esbuild/aix-ppc64@0.25.5':
optional: true
'@esbuild/aix-ppc64@0.25.9':
optional: true
'@esbuild/android-arm64@0.18.20':
optional: true
'@esbuild/android-arm64@0.25.5':
optional: true
'@esbuild/android-arm64@0.25.9':
optional: true
'@esbuild/android-arm@0.18.20':
optional: true
'@esbuild/android-arm@0.25.5':
optional: true
'@esbuild/android-arm@0.25.9':
optional: true
'@esbuild/android-x64@0.18.20':
optional: true
'@esbuild/android-x64@0.25.5':
optional: true
'@esbuild/android-x64@0.25.9':
optional: true
'@esbuild/darwin-arm64@0.18.20':
optional: true
'@esbuild/darwin-arm64@0.25.5':
optional: true
'@esbuild/darwin-arm64@0.25.9':
optional: true
'@esbuild/darwin-x64@0.18.20':
optional: true
'@esbuild/darwin-x64@0.25.5':
optional: true
'@esbuild/darwin-x64@0.25.9':
optional: true
'@esbuild/freebsd-arm64@0.18.20':
optional: true
'@esbuild/freebsd-arm64@0.25.5':
optional: true
'@esbuild/freebsd-arm64@0.25.9':
optional: true
'@esbuild/freebsd-x64@0.18.20':
optional: true
'@esbuild/freebsd-x64@0.25.5':
optional: true
'@esbuild/freebsd-x64@0.25.9':
optional: true
'@esbuild/linux-arm64@0.18.20':
optional: true
'@esbuild/linux-arm64@0.25.5':
optional: true
'@esbuild/linux-arm64@0.25.9':
optional: true
'@esbuild/linux-arm@0.18.20':
optional: true
'@esbuild/linux-arm@0.25.5':
optional: true
'@esbuild/linux-arm@0.25.9':
optional: true
'@esbuild/linux-ia32@0.18.20':
optional: true
'@esbuild/linux-ia32@0.25.5':
optional: true
'@esbuild/linux-ia32@0.25.9':
optional: true
'@esbuild/linux-loong64@0.18.20':
optional: true
'@esbuild/linux-loong64@0.25.5':
optional: true
'@esbuild/linux-loong64@0.25.9':
optional: true
'@esbuild/linux-mips64el@0.18.20':
optional: true
'@esbuild/linux-mips64el@0.25.5':
optional: true
'@esbuild/linux-mips64el@0.25.9':
optional: true
'@esbuild/linux-ppc64@0.18.20':
optional: true
'@esbuild/linux-ppc64@0.25.5':
optional: true
'@esbuild/linux-ppc64@0.25.9':
optional: true
'@esbuild/linux-riscv64@0.18.20':
optional: true
'@esbuild/linux-riscv64@0.25.5':
optional: true
'@esbuild/linux-riscv64@0.25.9':
optional: true
'@esbuild/linux-s390x@0.18.20':
optional: true
'@esbuild/linux-s390x@0.25.5':
optional: true
'@esbuild/linux-s390x@0.25.9':
optional: true
'@esbuild/linux-x64@0.18.20':
optional: true
'@esbuild/linux-x64@0.25.5':
optional: true
@@ -5827,6 +6102,9 @@ snapshots:
'@esbuild/netbsd-arm64@0.25.9':
optional: true
'@esbuild/netbsd-x64@0.18.20':
optional: true
'@esbuild/netbsd-x64@0.25.5':
optional: true
@@ -5839,6 +6117,9 @@ snapshots:
'@esbuild/openbsd-arm64@0.25.9':
optional: true
'@esbuild/openbsd-x64@0.18.20':
optional: true
'@esbuild/openbsd-x64@0.25.5':
optional: true
@@ -5848,24 +6129,36 @@ snapshots:
'@esbuild/openharmony-arm64@0.25.9':
optional: true
'@esbuild/sunos-x64@0.18.20':
optional: true
'@esbuild/sunos-x64@0.25.5':
optional: true
'@esbuild/sunos-x64@0.25.9':
optional: true
'@esbuild/win32-arm64@0.18.20':
optional: true
'@esbuild/win32-arm64@0.25.5':
optional: true
'@esbuild/win32-arm64@0.25.9':
optional: true
'@esbuild/win32-ia32@0.18.20':
optional: true
'@esbuild/win32-ia32@0.25.5':
optional: true
'@esbuild/win32-ia32@0.25.9':
optional: true
'@esbuild/win32-x64@0.18.20':
optional: true
'@esbuild/win32-x64@0.25.5':
optional: true
@@ -7138,6 +7431,12 @@ snapshots:
'@types/normalize-package-data@2.4.4': {}
'@types/pg@8.15.5':
dependencies:
'@types/node': 24.3.0
pg-protocol: 1.10.3
pg-types: 2.2.0
'@types/qrcode@1.5.5':
dependencies:
'@types/node': 24.3.0
@@ -7773,6 +8072,22 @@ snapshots:
dotenv@17.2.2: {}
drizzle-kit@0.31.4:
dependencies:
'@drizzle-team/brocli': 0.10.2
'@esbuild-kit/esm-loader': 2.6.5
esbuild: 0.25.9
esbuild-register: 3.6.0(esbuild@0.25.9)
transitivePeerDependencies:
- supports-color
drizzle-orm@0.44.5(@prisma/client@6.13.0(prisma@6.13.0(typescript@5.9.2))(typescript@5.9.2))(@types/pg@8.15.5)(pg@8.16.3)(prisma@6.13.0(typescript@5.9.2)):
optionalDependencies:
'@prisma/client': 6.13.0(prisma@6.13.0(typescript@5.9.2))(typescript@5.9.2)
'@types/pg': 8.15.5
pg: 8.16.3
prisma: 6.13.0(typescript@5.9.2)
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -7897,6 +8212,38 @@ snapshots:
is-date-object: 1.1.0
is-symbol: 1.1.1
esbuild-register@3.6.0(esbuild@0.25.9):
dependencies:
debug: 4.4.1
esbuild: 0.25.9
transitivePeerDependencies:
- supports-color
esbuild@0.18.20:
optionalDependencies:
'@esbuild/android-arm': 0.18.20
'@esbuild/android-arm64': 0.18.20
'@esbuild/android-x64': 0.18.20
'@esbuild/darwin-arm64': 0.18.20
'@esbuild/darwin-x64': 0.18.20
'@esbuild/freebsd-arm64': 0.18.20
'@esbuild/freebsd-x64': 0.18.20
'@esbuild/linux-arm': 0.18.20
'@esbuild/linux-arm64': 0.18.20
'@esbuild/linux-ia32': 0.18.20
'@esbuild/linux-loong64': 0.18.20
'@esbuild/linux-mips64el': 0.18.20
'@esbuild/linux-ppc64': 0.18.20
'@esbuild/linux-riscv64': 0.18.20
'@esbuild/linux-s390x': 0.18.20
'@esbuild/linux-x64': 0.18.20
'@esbuild/netbsd-x64': 0.18.20
'@esbuild/openbsd-x64': 0.18.20
'@esbuild/sunos-x64': 0.18.20
'@esbuild/win32-arm64': 0.18.20
'@esbuild/win32-ia32': 0.18.20
'@esbuild/win32-x64': 0.18.20
esbuild@0.25.5:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.5
@@ -10047,6 +10394,13 @@ snapshots:
source-map-js@1.2.1: {}
source-map-support@0.5.21:
dependencies:
buffer-from: 1.1.2
source-map: 0.6.1
source-map@0.6.1: {}
source-map@0.8.0-beta.0:
dependencies:
whatwg-url: 7.1.0

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "public"."Zipline" ADD COLUMN "coreTrustProxy" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -20,7 +20,6 @@ model Zipline {
coreReturnHttpsUrls Boolean @default(false)
coreDefaultDomain String?
coreTempDirectory String // default join(tmpdir(), 'zipline')
coreTrustProxy Boolean @default(false)
chunksEnabled Boolean @default(true)
chunksMax String @default("95mb")

View File

@@ -1,99 +0,0 @@
import { FieldSettings, useFileTableSettingsStore } from '@/lib/store/fileTableSettings';
import {
closestCenter,
DndContext,
DragEndEvent,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Button, Checkbox, Group, Modal, Paper, Text } from '@mantine/core';
import { IconGripVertical } from '@tabler/icons-react';
import { useShallow } from 'zustand/shallow';
export const NAMES = {
name: 'Name',
originalName: 'Original Name',
tags: 'Tags',
type: 'Type',
size: 'Size',
createdAt: 'Created At',
favorite: 'Favorite',
views: 'Views',
};
function SortableTableField({ item }: { item: FieldSettings }) {
const setVisible = useFileTableSettingsStore((state) => state.setVisible);
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: item.field,
});
const style: React.CSSProperties = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
cursor: 'grab',
width: '100%',
};
return (
<Paper withBorder p='xs' ref={setNodeRef} style={style} {...attributes} {...listeners}>
<Group gap='xs'>
<IconGripVertical size='1rem' />
<Checkbox checked={item.visible} onChange={() => setVisible(item.field, !item.visible)} />
<Text>{NAMES[item.field]}</Text>
</Group>
</Paper>
);
}
export default function TableEditModal({ opened, onCLose }: { opened: boolean; onCLose: () => void }) {
const [fields, setIndex, reset] = useFileTableSettingsStore(
useShallow((state) => [state.fields, state.setIndex, state.reset]),
);
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
useSensor(KeyboardSensor),
);
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
const newIndex = fields.findIndex((item) => item.field === over?.id);
setIndex(active.id as FieldSettings['field'], newIndex);
}
};
return (
<Modal opened={opened} onClose={onCLose} title='Table Options' centered>
<Text mb='md' size='sm' c='dimmed'>
Select and drag fields below to make them appear/disappear/reorder in the file table view.
</Text>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={fields.map((item) => item.field)} strategy={verticalListSortingStrategy}>
{fields.map((item, index) => (
<div
key={index}
style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}
>
<SortableTableField item={item} />
</div>
))}
</SortableContext>
</DndContext>
<Button fullWidth color='red' onClick={() => reset()} variant='light' mt='md'>
Reset to Default
</Button>
</Modal>
);
}

View File

@@ -5,8 +5,6 @@ import { bytes } from '@/lib/bytes';
import { type File } from '@/lib/db/models/file';
import { Folder } from '@/lib/db/models/folder';
import { Tag } from '@/lib/db/models/tag';
import { useQueryState } from '@/lib/hooks/useQueryState';
import { useFileTableSettingsStore } from '@/lib/store/fileTableSettings';
import { useSettingsStore } from '@/lib/store/settings';
import {
ActionIcon,
@@ -36,17 +34,16 @@ import {
IconFile,
IconGridPatternFilled,
IconStar,
IconTableOptions,
IconTrashFilled,
} from '@tabler/icons-react';
import { DataTable } from 'mantine-datatable';
import { lazy, useEffect, useReducer, useState } from 'react';
import { Link } from 'react-router-dom';
import useSWR from 'swr';
import TableEditModal, { NAMES } from '../TableEditModal';
import { bulkDelete, bulkFavorite } from '../bulk';
import TagPill from '../tags/TagPill';
import { useApiPagination } from '../useApiPagination';
import { useQueryState } from '@/lib/hooks/useQueryState';
const FileModal = lazy(() => import('@/components/file/DashboardFile/FileModal'));
@@ -57,6 +54,13 @@ type ReducerQuery = {
const PER_PAGE_OPTIONS = [10, 20, 50];
const NAMES = {
name: 'Name',
originalName: 'Original name',
type: 'Type',
id: 'ID',
};
function SearchFilter({
setSearchField,
searchQuery,
@@ -84,8 +88,8 @@ function SearchFilter({
return (
<TextInput
label={NAMES[field as keyof typeof NAMES]}
placeholder={`Search by ${NAMES[field as keyof typeof NAMES].toLowerCase()}`}
label={NAMES[field]}
placeholder={`Search by ${NAMES[field].toLowerCase()}`}
value={searchQuery[field]}
onChange={onChange}
size='sm'
@@ -179,10 +183,6 @@ export default function FileTable({ id }: { id?: string }) {
const clipboard = useClipboard();
const warnDeletion = useSettingsStore((state) => state.settings.warnDeletion);
const [tableEditOpen, setTableEditOpen] = useState(false);
const fields = useFileTableSettingsStore((state) => state.fields);
const { data: folders } = useSWR<Extract<Response['/api/user/folders'], Folder[]>>(
'/api/user/folders?noincl=true',
);
@@ -264,100 +264,6 @@ export default function FileTable({ id }: { id?: string }) {
}),
});
const FIELDS = [
{
accessor: 'name',
sortable: true,
filter: (
<SearchFilter
setSearchField={setSearchField}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
field='name'
/>
),
filtering: searchField === 'name' && searchQuery.name.trim() !== '',
},
{
accessor: 'originalName',
sortable: true,
filter: (
<SearchFilter
setSearchField={setSearchField}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
field='originalName'
/>
),
filtering: searchField === 'originalName' && searchQuery.originalName.trim() !== '',
},
{
accessor: 'tags',
sortable: false,
width: 200,
render: (file: File) => (
<ScrollArea w={180} onClick={(e) => e.stopPropagation()}>
<Flex gap='sm'>
{file.tags!.map((tag) => (
<TagPill tag={tag} key={tag.id} />
))}
</Flex>
</ScrollArea>
),
filter: (
<TagsFilter
setSearchField={setSearchField}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
/>
),
filtering: searchField === 'tags' && searchQuery.tags.trim() !== '',
},
{
accessor: 'type',
sortable: true,
filter: (
<SearchFilter
setSearchField={setSearchField}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
field='type'
/>
),
filtering: searchField === 'type' && searchQuery.type.trim() !== '',
},
{ accessor: 'size', sortable: true, render: (file: File) => bytes(file.size) },
{
accessor: 'createdAt',
sortable: true,
render: (file: File) => <RelativeDate date={file.createdAt} />,
},
{
accessor: 'favorite',
sortable: true,
render: (file: File) => (file.favorite ? <Text c='yellow'>Yes</Text> : 'No'),
},
{
accessor: 'views',
sortable: true,
render: (file: File) => file.views,
},
{
accessor: 'id',
hidden: searchField !== 'id' || searchQuery.id.trim() === '',
filtering: searchField === 'id' && searchQuery.id.trim() !== '',
},
];
const visibleFields = fields.filter((f) => f.visible).map((f) => f.field);
const columns = FIELDS.filter((f) => visibleFields.includes(f.accessor as any));
columns.sort((a, b) => {
const aIndex = fields.findIndex((f) => f.field === a.accessor);
const bIndex = fields.findIndex((f) => f.field === b.accessor);
return aIndex - bIndex;
});
useEffect(() => {
if (data && selectedFile) {
const file = data.page.find((x) => x.id === selectedFile.id);
@@ -389,32 +295,19 @@ export default function FileTable({ id }: { id?: string }) {
file={selectedFile}
/>
<TableEditModal opened={tableEditOpen} onCLose={() => setTableEditOpen(false)} />
<Box>
<Group>
<Tooltip label='Table Options'>
<ActionIcon
variant='outline'
onClick={() => setTableEditOpen((open) => !open)}
style={{ position: 'relative', top: '-36.4px', left: '221px', margin: 0 }}
>
<IconTableOptions size='1rem' />
</ActionIcon>
</Tooltip>
<Tooltip label='Search by ID'>
<ActionIcon
variant='outline'
onClick={() => {
setIdSearchOpen((open) => !open);
}}
// lol if it works it works :shrug:
style={{ position: 'relative', top: '-36.4px', left: '221px', margin: 0 }}
>
<IconGridPatternFilled size='1rem' />
</ActionIcon>
</Tooltip>
</Group>
<Tooltip label='Search by ID'>
<ActionIcon
variant='outline'
onClick={() => {
setIdSearchOpen((open) => !open);
}}
// lol if it works it works :shrug:
style={{ position: 'relative', top: '-36.4px', left: '221px', margin: 0 }}
>
<IconGridPatternFilled size='1rem' />
</ActionIcon>
</Tooltip>
<Collapse in={selectedFiles.length > 0}>
<Paper withBorder p='sm' my='sm'>
@@ -524,7 +417,75 @@ export default function FileTable({ id }: { id?: string }) {
minHeight={200}
records={data?.page ?? []}
columns={[
...columns,
{
accessor: 'name',
sortable: true,
filter: (
<SearchFilter
setSearchField={setSearchField}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
field='name'
/>
),
filtering: searchField === 'name' && searchQuery.name.trim() !== '',
},
{
accessor: 'tags',
sortable: false,
width: 200,
render: (file) => (
<ScrollArea w={180} onClick={(e) => e.stopPropagation()}>
<Flex gap='sm'>
{file.tags!.map((tag) => (
<TagPill tag={tag} key={tag.id} />
))}
</Flex>
</ScrollArea>
),
filter: (
<TagsFilter
setSearchField={setSearchField}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
/>
),
filtering: searchField === 'tags' && searchQuery.tags.trim() !== '',
},
{
accessor: 'type',
sortable: true,
filter: (
<SearchFilter
setSearchField={setSearchField}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
field='type'
/>
),
filtering: searchField === 'type' && searchQuery.type.trim() !== '',
},
{ accessor: 'size', sortable: true, render: (file) => bytes(file.size) },
{
accessor: 'createdAt',
sortable: true,
render: (file) => <RelativeDate date={file.createdAt} />,
},
{
accessor: 'favorite',
sortable: true,
render: (file) => (file.favorite ? <Text c='yellow'>Yes</Text> : 'No'),
},
{
accessor: 'views',
sortable: true,
render: (file) => file.views,
},
{
accessor: 'id',
hidden: searchField !== 'id' || searchQuery.id.trim() === '',
filtering: searchField === 'id' && searchQuery.id.trim() !== '',
},
{
accessor: 'actions',
textAlign: 'right',

View File

@@ -17,13 +17,11 @@ export default function Core({
coreReturnHttpsUrls: boolean;
coreDefaultDomain: string | null | undefined;
coreTempDirectory: string;
coreTrustProxy: boolean;
}>({
initialValues: {
coreReturnHttpsUrls: false,
coreDefaultDomain: '',
coreTempDirectory: '/tmp/zipline',
coreTrustProxy: false,
},
enhanceGetInputProps: (payload) => ({
disabled: data?.tampered?.includes(payload.field) || false,
@@ -47,7 +45,6 @@ export default function Core({
coreReturnHttpsUrls: data.settings.coreReturnHttpsUrls ?? false,
coreDefaultDomain: data.settings.coreDefaultDomain ?? '',
coreTempDirectory: data.settings.coreTempDirectory ?? '/tmp/zipline',
coreTrustProxy: data.settings.coreTrustProxy ?? false,
});
}, [data]);
@@ -58,20 +55,14 @@ export default function Core({
<Title order={2}>Core</Title>
<form onSubmit={form.onSubmit(onSubmit)}>
<Switch
mt='md'
label='Return HTTPS URLs'
description='Return URLs with HTTPS protocol.'
{...form.getInputProps('coreReturnHttpsUrls', { type: 'checkbox' })}
/>
<SimpleGrid mt='md' cols={{ base: 1, md: 2 }} spacing='lg'>
<Switch
mt='md'
label='Return HTTPS URLs'
description='Return URLs with HTTPS protocol.'
{...form.getInputProps('coreReturnHttpsUrls', { type: 'checkbox' })}
/>
<Switch
label='Trust Proxies'
description='Trust the X-Forwarded-* headers set by proxies. Only enable this if you are behind a trusted proxy (nginx, caddy, etc.). Requires a server restart.'
{...form.getInputProps('coreTrustProxy', { type: 'checkbox' })}
/>
<TextInput
label='Default Domain'
description='The domain to use when generating URLs. This value should not include the protocol.'

View File

@@ -0,0 +1,287 @@
CREATE TYPE "public"."IncompleteFileStatus" AS ENUM('PENDING', 'PROCESSING', 'COMPLETE', 'FAILED');--> statement-breakpoint
CREATE TYPE "public"."OAuthProviderType" AS ENUM('DISCORD', 'GOOGLE', 'GITHUB', 'OIDC');--> statement-breakpoint
CREATE TYPE "public"."Role" AS ENUM('USER', 'ADMIN', 'SUPERADMIN');--> statement-breakpoint
CREATE TYPE "public"."UserFilesQuota" AS ENUM('BY_BYTES', 'BY_FILES');--> statement-breakpoint
--> statement-breakpoint
CREATE TABLE "Zipline" (
"id" text PRIMARY KEY NOT NULL,
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
"updatedAt" timestamp(3) NOT NULL,
"firstSetup" boolean DEFAULT true NOT NULL,
"coreReturnHttpsUrls" boolean DEFAULT false NOT NULL,
"coreDefaultDomain" text,
"coreTempDirectory" text NOT NULL,
"chunksEnabled" boolean DEFAULT true NOT NULL,
"chunksMax" text DEFAULT '95mb' NOT NULL,
"chunksSize" text DEFAULT '25mb' NOT NULL,
"tasksDeleteInterval" text DEFAULT '30m' NOT NULL,
"tasksClearInvitesInterval" text DEFAULT '30m' NOT NULL,
"tasksMaxViewsInterval" text DEFAULT '30m' NOT NULL,
"tasksThumbnailsInterval" text DEFAULT '30m' NOT NULL,
"tasksMetricsInterval" text DEFAULT '30m' NOT NULL,
"filesRoute" text DEFAULT '/u' NOT NULL,
"filesLength" integer DEFAULT 6 NOT NULL,
"filesDefaultFormat" text DEFAULT 'random' NOT NULL,
"filesDisabledExtensions" text[],
"filesMaxFileSize" text DEFAULT '100mb' NOT NULL,
"filesDefaultExpiration" text,
"filesAssumeMimetypes" boolean DEFAULT false NOT NULL,
"filesDefaultDateFormat" text DEFAULT 'YYYY-MM-DD_HH:mm:ss' NOT NULL,
"filesRemoveGpsMetadata" boolean DEFAULT false NOT NULL,
"urlsRoute" text DEFAULT '/go' NOT NULL,
"urlsLength" integer DEFAULT 6 NOT NULL,
"featuresImageCompression" boolean DEFAULT true NOT NULL,
"featuresRobotsTxt" boolean DEFAULT true NOT NULL,
"featuresHealthcheck" boolean DEFAULT true NOT NULL,
"featuresUserRegistration" boolean DEFAULT false NOT NULL,
"featuresOauthRegistration" boolean DEFAULT false NOT NULL,
"featuresDeleteOnMaxViews" boolean DEFAULT true NOT NULL,
"featuresThumbnailsEnabled" boolean DEFAULT true NOT NULL,
"featuresThumbnailsNumberThreads" integer DEFAULT 4 NOT NULL,
"featuresMetricsEnabled" boolean DEFAULT true NOT NULL,
"featuresMetricsAdminOnly" boolean DEFAULT false NOT NULL,
"featuresMetricsShowUserSpecific" boolean DEFAULT true NOT NULL,
"invitesEnabled" boolean DEFAULT true NOT NULL,
"invitesLength" integer DEFAULT 6 NOT NULL,
"websiteTitle" text DEFAULT 'Zipline' NOT NULL,
"websiteTitleLogo" text,
"websiteExternalLinks" jsonb DEFAULT '[{"url":"https://github.com/diced/zipline","name":"GitHub"},{"url":"https://zipline.diced.sh/","name":"Documentation"}]'::jsonb NOT NULL,
"websiteLoginBackground" text,
"websiteDefaultAvatar" text,
"websiteTos" text,
"websiteThemeDefault" text DEFAULT 'system' NOT NULL,
"websiteThemeDark" text DEFAULT 'builtin:dark_gray' NOT NULL,
"websiteThemeLight" text DEFAULT 'builtin:light_gray' NOT NULL,
"oauthBypassLocalLogin" boolean DEFAULT false NOT NULL,
"oauthLoginOnly" boolean DEFAULT false NOT NULL,
"oauthDiscordClientId" text,
"oauthDiscordClientSecret" text,
"oauthDiscordRedirectUri" text,
"oauthGoogleClientId" text,
"oauthGoogleClientSecret" text,
"oauthGoogleRedirectUri" text,
"oauthGithubClientId" text,
"oauthGithubClientSecret" text,
"oauthGithubRedirectUri" text,
"oauthOidcClientId" text,
"oauthOidcClientSecret" text,
"oauthOidcAuthorizeUrl" text,
"oauthOidcTokenUrl" text,
"oauthOidcUserinfoUrl" text,
"oauthOidcRedirectUri" text,
"mfaTotpEnabled" boolean DEFAULT false NOT NULL,
"mfaTotpIssuer" text DEFAULT 'Zipline' NOT NULL,
"mfaPasskeys" boolean DEFAULT false NOT NULL,
"ratelimitEnabled" boolean DEFAULT true NOT NULL,
"ratelimitMax" integer DEFAULT 10 NOT NULL,
"ratelimitWindow" integer,
"ratelimitAdminBypass" boolean DEFAULT true NOT NULL,
"ratelimitAllowList" text[],
"httpWebhookOnUpload" text,
"httpWebhookOnShorten" text,
"discordWebhookUrl" text,
"discordUsername" text,
"discordAvatarUrl" text,
"discordOnUploadWebhookUrl" text,
"discordOnUploadUsername" text,
"discordOnUploadAvatarUrl" text,
"discordOnUploadContent" text,
"discordOnUploadEmbed" jsonb,
"discordOnShortenWebhookUrl" text,
"discordOnShortenUsername" text,
"discordOnShortenAvatarUrl" text,
"discordOnShortenContent" text,
"discordOnShortenEmbed" jsonb,
"pwaEnabled" boolean DEFAULT false NOT NULL,
"pwaTitle" text DEFAULT 'Zipline' NOT NULL,
"pwaShortName" text DEFAULT 'Zipline' NOT NULL,
"pwaDescription" text DEFAULT 'Zipline' NOT NULL,
"pwaThemeColor" text DEFAULT '#000000' NOT NULL,
"pwaBackgroundColor" text DEFAULT '#000000' NOT NULL,
"websiteLoginBackgroundBlur" boolean DEFAULT true NOT NULL,
"filesRandomWordsNumAdjectives" integer DEFAULT 2 NOT NULL,
"filesRandomWordsSeparator" text DEFAULT '-' NOT NULL,
"featuresVersionAPI" text DEFAULT 'https://zipline-version.diced.sh' NOT NULL,
"featuresVersionChecking" boolean DEFAULT true NOT NULL,
"oauthDiscordAllowedIds" text[] DEFAULT '{"RAY"}',
"oauthDiscordDeniedIds" text[] DEFAULT '{"RAY"}',
"domains" text[] DEFAULT '{"RAY"}',
"filesDefaultCompressionFormat" text DEFAULT 'jpg',
"featuresThumbnailsFormat" text DEFAULT 'jpg' NOT NULL
);
--> statement-breakpoint
CREATE TABLE "Metric" (
"id" text PRIMARY KEY NOT NULL,
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
"updatedAt" timestamp(3) NOT NULL,
"data" jsonb NOT NULL
);
--> statement-breakpoint
CREATE TABLE "Url" (
"id" text PRIMARY KEY NOT NULL,
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
"updatedAt" timestamp(3) NOT NULL,
"code" text NOT NULL,
"vanity" text,
"destination" text NOT NULL,
"views" integer DEFAULT 0 NOT NULL,
"maxViews" integer,
"password" text,
"userId" text,
"enabled" boolean DEFAULT true NOT NULL
);
--> statement-breakpoint
CREATE TABLE "Folder" (
"id" text PRIMARY KEY NOT NULL,
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
"updatedAt" timestamp(3) NOT NULL,
"name" text NOT NULL,
"public" boolean DEFAULT false NOT NULL,
"userId" text NOT NULL,
"allowUploads" boolean DEFAULT false NOT NULL
);
--> statement-breakpoint
CREATE TABLE "User" (
"id" text PRIMARY KEY NOT NULL,
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
"updatedAt" timestamp(3) NOT NULL,
"username" text NOT NULL,
"password" text,
"avatar" text,
"token" text NOT NULL,
"role" "Role" DEFAULT 'USER' NOT NULL,
"view" jsonb DEFAULT '{}'::jsonb NOT NULL,
"totpSecret" text,
"sessions" text[]
);
--> statement-breakpoint
CREATE TABLE "Export" (
"id" text PRIMARY KEY NOT NULL,
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
"updatedAt" timestamp(3) NOT NULL,
"completed" boolean DEFAULT false NOT NULL,
"path" text NOT NULL,
"files" integer NOT NULL,
"size" text NOT NULL,
"userId" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "UserQuota" (
"id" text PRIMARY KEY NOT NULL,
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
"updatedAt" timestamp(3) NOT NULL,
"filesQuota" "UserFilesQuota" NOT NULL,
"maxBytes" text,
"maxFiles" integer,
"maxUrls" integer,
"userId" text
);
--> statement-breakpoint
CREATE TABLE "UserPasskey" (
"id" text PRIMARY KEY NOT NULL,
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
"updatedAt" timestamp(3) NOT NULL,
"lastUsed" timestamp(3),
"name" text NOT NULL,
"reg" jsonb NOT NULL,
"userId" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "OAuthProvider" (
"id" text PRIMARY KEY NOT NULL,
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
"updatedAt" timestamp(3) NOT NULL,
"userId" text NOT NULL,
"provider" "OAuthProviderType" NOT NULL,
"username" text NOT NULL,
"accessToken" text NOT NULL,
"refreshToken" text,
"oauthId" text
);
--> statement-breakpoint
CREATE TABLE "File" (
"id" text PRIMARY KEY NOT NULL,
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
"updatedAt" timestamp(3) NOT NULL,
"deletesAt" timestamp(3),
"name" text NOT NULL,
"originalName" text,
"size" bigint NOT NULL,
"type" text NOT NULL,
"views" integer DEFAULT 0 NOT NULL,
"maxViews" integer,
"favorite" boolean DEFAULT false NOT NULL,
"password" text,
"userId" text,
"folderId" text
);
--> statement-breakpoint
CREATE TABLE "Thumbnail" (
"id" text PRIMARY KEY NOT NULL,
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
"updatedAt" timestamp(3) NOT NULL,
"path" text NOT NULL,
"fileId" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "IncompleteFile" (
"id" text PRIMARY KEY NOT NULL,
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
"updatedAt" timestamp(3) NOT NULL,
"status" "IncompleteFileStatus" NOT NULL,
"chunksTotal" integer NOT NULL,
"chunksComplete" integer NOT NULL,
"metadata" jsonb NOT NULL,
"userId" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "Tag" (
"id" text PRIMARY KEY NOT NULL,
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
"updatedAt" timestamp(3) NOT NULL,
"name" text NOT NULL,
"color" text NOT NULL,
"userId" text
);
--> statement-breakpoint
CREATE TABLE "Invite" (
"id" text PRIMARY KEY NOT NULL,
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
"updatedAt" timestamp(3) NOT NULL,
"expiresAt" timestamp(3),
"code" text NOT NULL,
"uses" integer DEFAULT 0 NOT NULL,
"maxUses" integer,
"inviterId" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "_FileToTag" (
"A" text NOT NULL,
"B" text NOT NULL,
CONSTRAINT "_FileToTag_AB_pkey" PRIMARY KEY("A","B")
);
--> statement-breakpoint
ALTER TABLE "Url" ADD CONSTRAINT "Url_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "Folder" ADD CONSTRAINT "Folder_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "Export" ADD CONSTRAINT "Export_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "UserQuota" ADD CONSTRAINT "UserQuota_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "UserPasskey" ADD CONSTRAINT "UserPasskey_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "OAuthProvider" ADD CONSTRAINT "OAuthProvider_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "File" ADD CONSTRAINT "File_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "File" ADD CONSTRAINT "File_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "public"."Folder"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "Thumbnail" ADD CONSTRAINT "Thumbnail_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "public"."File"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "IncompleteFile" ADD CONSTRAINT "IncompleteFile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "Tag" ADD CONSTRAINT "Tag_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "Invite" ADD CONSTRAINT "Invite_inviterId_fkey" FOREIGN KEY ("inviterId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "_FileToTag" ADD CONSTRAINT "_FileToTag_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."File"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "_FileToTag" ADD CONSTRAINT "_FileToTag_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."Tag"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
CREATE UNIQUE INDEX "Url_code_vanity_key" ON "Url" USING btree ("code" text_ops,"vanity" text_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "User_token_key" ON "User" USING btree ("token" text_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "User_username_key" ON "User" USING btree ("username" text_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "UserQuota_userId_key" ON "UserQuota" USING btree ("userId" text_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "OAuthProvider_provider_oauthId_key" ON "OAuthProvider" USING btree ("provider" text_ops,"oauthId" text_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "Thumbnail_fileId_key" ON "Thumbnail" USING btree ("fileId" text_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "Tag_name_key" ON "Tag" USING btree ("name" text_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "Invite_code_key" ON "Invite" USING btree ("code" text_ops);--> statement-breakpoint
CREATE INDEX "_FileToTag_B_index" ON "_FileToTag" USING btree ("B" text_ops);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1756926875085,
"tag": "0000_bouncy_mantis",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1756931651183,
"tag": "0001_next_red_ghost",
"breakpoints": true
}
]
}

125
src/drizzle/relations.ts Normal file
View File

@@ -0,0 +1,125 @@
import { relations } from 'drizzle-orm/relations';
import {
user,
url,
folder,
exportTable,
userQuota,
userPasskey,
oauthProvider,
file,
thumbnail,
incompleteFile,
tag,
invite,
fileToTag,
} from './schema';
export const urlRelations = relations(url, ({ one }) => ({
user: one(user, {
fields: [url.userId],
references: [user.id],
}),
}));
export const userRelations = relations(user, ({ many }) => ({
urls: many(url),
folders: many(folder),
exports: many(exportTable),
userQuotas: many(userQuota),
userPasskeys: many(userPasskey),
oauthProviders: many(oauthProvider),
files: many(file),
incompleteFiles: many(incompleteFile),
tags: many(tag),
invites: many(invite),
}));
export const folderRelations = relations(folder, ({ one, many }) => ({
user: one(user, {
fields: [folder.userId],
references: [user.id],
}),
files: many(file),
}));
export const exportRelations = relations(exportTable, ({ one }) => ({
user: one(user, {
fields: [exportTable.userId],
references: [user.id],
}),
}));
export const userQuotaRelations = relations(userQuota, ({ one }) => ({
user: one(user, {
fields: [userQuota.userId],
references: [user.id],
}),
}));
export const userPasskeyRelations = relations(userPasskey, ({ one }) => ({
user: one(user, {
fields: [userPasskey.userId],
references: [user.id],
}),
}));
export const oauthProviderRelations = relations(oauthProvider, ({ one }) => ({
user: one(user, {
fields: [oauthProvider.userId],
references: [user.id],
}),
}));
export const fileRelations = relations(file, ({ one, many }) => ({
user: one(user, {
fields: [file.userId],
references: [user.id],
}),
folder: one(folder, {
fields: [file.folderId],
references: [folder.id],
}),
thumbnails: many(thumbnail),
fileToTags: many(fileToTag),
}));
export const thumbnailRelations = relations(thumbnail, ({ one }) => ({
file: one(file, {
fields: [thumbnail.fileId],
references: [file.id],
}),
}));
export const incompleteFileRelations = relations(incompleteFile, ({ one }) => ({
user: one(user, {
fields: [incompleteFile.userId],
references: [user.id],
}),
}));
export const tagRelations = relations(tag, ({ one, many }) => ({
user: one(user, {
fields: [tag.userId],
references: [user.id],
}),
fileToTags: many(fileToTag),
}));
export const inviteRelations = relations(invite, ({ one }) => ({
user: one(user, {
fields: [invite.inviterId],
references: [user.id],
}),
}));
export const fileToTagRelations = relations(fileToTag, ({ one }) => ({
file: one(file, {
fields: [fileToTag.a],
references: [file.id],
}),
tag: one(tag, {
fields: [fileToTag.b],
references: [tag.id],
}),
}));

497
src/drizzle/schema.ts Normal file
View File

@@ -0,0 +1,497 @@
import {
pgTable,
timestamp,
text,
integer,
boolean,
jsonb,
uniqueIndex,
foreignKey,
bigint,
index,
primaryKey,
pgEnum,
} from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
export const incompleteFileStatus = pgEnum('IncompleteFileStatus', [
'PENDING',
'PROCESSING',
'COMPLETE',
'FAILED',
]);
export const oauthProviderType = pgEnum('OAuthProviderType', ['DISCORD', 'GOOGLE', 'GITHUB', 'OIDC']);
export const role = pgEnum('Role', ['USER', 'ADMIN', 'SUPERADMIN']);
export const userFilesQuota = pgEnum('UserFilesQuota', ['BY_BYTES', 'BY_FILES']);
export const zipline = pgTable('Zipline', {
id: text().primaryKey().notNull(),
createdAt: timestamp({ precision: 3, mode: 'string' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
firstSetup: boolean().default(true).notNull(),
coreReturnHttpsUrls: boolean().default(false).notNull(),
coreDefaultDomain: text(),
coreTempDirectory: text().notNull(),
chunksEnabled: boolean().default(true).notNull(),
chunksMax: text().default('95mb').notNull(),
chunksSize: text().default('25mb').notNull(),
tasksDeleteInterval: text().default('30m').notNull(),
tasksClearInvitesInterval: text().default('30m').notNull(),
tasksMaxViewsInterval: text().default('30m').notNull(),
tasksThumbnailsInterval: text().default('30m').notNull(),
tasksMetricsInterval: text().default('30m').notNull(),
filesRoute: text().default('/u').notNull(),
filesLength: integer().default(6).notNull(),
filesDefaultFormat: text().default('random').notNull(),
filesDisabledExtensions: text().array(),
filesMaxFileSize: text().default('100mb').notNull(),
filesDefaultExpiration: text(),
filesAssumeMimetypes: boolean().default(false).notNull(),
filesDefaultDateFormat: text().default('YYYY-MM-DD_HH:mm:ss').notNull(),
filesRemoveGpsMetadata: boolean().default(false).notNull(),
urlsRoute: text().default('/go').notNull(),
urlsLength: integer().default(6).notNull(),
featuresImageCompression: boolean().default(true).notNull(),
featuresRobotsTxt: boolean().default(true).notNull(),
featuresHealthcheck: boolean().default(true).notNull(),
featuresUserRegistration: boolean().default(false).notNull(),
featuresOauthRegistration: boolean().default(false).notNull(),
featuresDeleteOnMaxViews: boolean().default(true).notNull(),
featuresThumbnailsEnabled: boolean().default(true).notNull(),
featuresThumbnailsNumberThreads: integer().default(4).notNull(),
featuresMetricsEnabled: boolean().default(true).notNull(),
featuresMetricsAdminOnly: boolean().default(false).notNull(),
featuresMetricsShowUserSpecific: boolean().default(true).notNull(),
invitesEnabled: boolean().default(true).notNull(),
invitesLength: integer().default(6).notNull(),
websiteTitle: text().default('Zipline').notNull(),
websiteTitleLogo: text(),
websiteExternalLinks: jsonb()
.default([
{ url: 'https://github.com/diced/zipline', name: 'GitHub' },
{ url: 'https://zipline.diced.sh/', name: 'Documentation' },
])
.notNull(),
websiteLoginBackground: text(),
websiteDefaultAvatar: text(),
websiteTos: text(),
websiteThemeDefault: text().default('system').notNull(),
websiteThemeDark: text().default('builtin:dark_gray').notNull(),
websiteThemeLight: text().default('builtin:light_gray').notNull(),
oauthBypassLocalLogin: boolean().default(false).notNull(),
oauthLoginOnly: boolean().default(false).notNull(),
oauthDiscordClientId: text(),
oauthDiscordClientSecret: text(),
oauthDiscordRedirectUri: text(),
oauthGoogleClientId: text(),
oauthGoogleClientSecret: text(),
oauthGoogleRedirectUri: text(),
oauthGithubClientId: text(),
oauthGithubClientSecret: text(),
oauthGithubRedirectUri: text(),
oauthOidcClientId: text(),
oauthOidcClientSecret: text(),
oauthOidcAuthorizeUrl: text(),
oauthOidcTokenUrl: text(),
oauthOidcUserinfoUrl: text(),
oauthOidcRedirectUri: text(),
mfaTotpEnabled: boolean().default(false).notNull(),
mfaTotpIssuer: text().default('Zipline').notNull(),
mfaPasskeys: boolean().default(false).notNull(),
ratelimitEnabled: boolean().default(true).notNull(),
ratelimitMax: integer().default(10).notNull(),
ratelimitWindow: integer(),
ratelimitAdminBypass: boolean().default(true).notNull(),
ratelimitAllowList: text().array(),
httpWebhookOnUpload: text(),
httpWebhookOnShorten: text(),
discordWebhookUrl: text(),
discordUsername: text(),
discordAvatarUrl: text(),
discordOnUploadWebhookUrl: text(),
discordOnUploadUsername: text(),
discordOnUploadAvatarUrl: text(),
discordOnUploadContent: text(),
discordOnUploadEmbed: jsonb(),
discordOnShortenWebhookUrl: text(),
discordOnShortenUsername: text(),
discordOnShortenAvatarUrl: text(),
discordOnShortenContent: text(),
discordOnShortenEmbed: jsonb(),
pwaEnabled: boolean().default(false).notNull(),
pwaTitle: text().default('Zipline').notNull(),
pwaShortName: text().default('Zipline').notNull(),
pwaDescription: text().default('Zipline').notNull(),
pwaThemeColor: text().default('#000000').notNull(),
pwaBackgroundColor: text().default('#000000').notNull(),
websiteLoginBackgroundBlur: boolean().default(true).notNull(),
filesRandomWordsNumAdjectives: integer().default(2).notNull(),
filesRandomWordsSeparator: text().default('-').notNull(),
featuresVersionAPI: text().default('https://zipline-version.diced.sh').notNull(),
featuresVersionChecking: boolean().default(true).notNull(),
oauthDiscordAllowedIds: text().array().default(['RAY']),
oauthDiscordDeniedIds: text().array().default(['RAY']),
domains: text().array().default(['RAY']),
filesDefaultCompressionFormat: text().default('jpg'),
featuresThumbnailsFormat: text().default('jpg').notNull(),
});
export const metric = pgTable('Metric', {
id: text().primaryKey().notNull(),
createdAt: timestamp({ precision: 3, mode: 'string' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
data: jsonb().notNull(),
});
export const url = pgTable(
'Url',
{
id: text().primaryKey().notNull(),
createdAt: timestamp({ precision: 3, mode: 'string' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
code: text().notNull(),
vanity: text(),
destination: text().notNull(),
views: integer().default(0).notNull(),
maxViews: integer(),
password: text(),
userId: text(),
enabled: boolean().default(true).notNull(),
},
(table) => [
uniqueIndex('Url_code_vanity_key').using(
'btree',
table.code.asc().nullsLast().op('text_ops'),
table.vanity.asc().nullsLast().op('text_ops'),
),
foreignKey({
columns: [table.userId],
foreignColumns: [user.id],
name: 'Url_userId_fkey',
})
.onUpdate('cascade')
.onDelete('set null'),
],
);
export const folder = pgTable(
'Folder',
{
id: text().primaryKey().notNull(),
createdAt: timestamp({ precision: 3, mode: 'string' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
name: text().notNull(),
public: boolean().default(false).notNull(),
userId: text().notNull(),
allowUploads: boolean().default(false).notNull(),
},
(table) => [
foreignKey({
columns: [table.userId],
foreignColumns: [user.id],
name: 'Folder_userId_fkey',
})
.onUpdate('cascade')
.onDelete('cascade'),
],
);
export const user = pgTable(
'User',
{
id: text().primaryKey().notNull(),
createdAt: timestamp({ precision: 3, mode: 'string' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
username: text().notNull(),
password: text(),
avatar: text(),
token: text().notNull(),
role: role().default('USER').notNull(),
view: jsonb().default({}).notNull(),
totpSecret: text(),
sessions: text().array(),
},
(table) => [
uniqueIndex('User_token_key').using('btree', table.token.asc().nullsLast().op('text_ops')),
uniqueIndex('User_username_key').using('btree', table.username.asc().nullsLast().op('text_ops')),
],
);
export const exportTable = pgTable(
'Export',
{
id: text().primaryKey().notNull(),
createdAt: timestamp({ precision: 3, mode: 'string' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
completed: boolean().default(false).notNull(),
path: text().notNull(),
files: integer().notNull(),
size: text().notNull(),
userId: text().notNull(),
},
(table) => [
foreignKey({
columns: [table.userId],
foreignColumns: [user.id],
name: 'Export_userId_fkey',
})
.onUpdate('cascade')
.onDelete('cascade'),
],
);
export const userQuota = pgTable(
'UserQuota',
{
id: text().primaryKey().notNull(),
createdAt: timestamp({ precision: 3, mode: 'string' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
filesQuota: userFilesQuota().notNull(),
maxBytes: text(),
maxFiles: integer(),
maxUrls: integer(),
userId: text(),
},
(table) => [
uniqueIndex('UserQuota_userId_key').using('btree', table.userId.asc().nullsLast().op('text_ops')),
foreignKey({
columns: [table.userId],
foreignColumns: [user.id],
name: 'UserQuota_userId_fkey',
})
.onUpdate('cascade')
.onDelete('cascade'),
],
);
export const userPasskey = pgTable(
'UserPasskey',
{
id: text().primaryKey().notNull(),
createdAt: timestamp({ precision: 3, mode: 'string' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
lastUsed: timestamp({ precision: 3, mode: 'string' }),
name: text().notNull(),
reg: jsonb().notNull(),
userId: text().notNull(),
},
(table) => [
foreignKey({
columns: [table.userId],
foreignColumns: [user.id],
name: 'UserPasskey_userId_fkey',
})
.onUpdate('cascade')
.onDelete('cascade'),
],
);
export const oauthProvider = pgTable(
'OAuthProvider',
{
id: text().primaryKey().notNull(),
createdAt: timestamp({ precision: 3, mode: 'string' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
userId: text().notNull(),
provider: oauthProviderType().notNull(),
username: text().notNull(),
accessToken: text().notNull(),
refreshToken: text(),
oauthId: text(),
},
(table) => [
uniqueIndex('OAuthProvider_provider_oauthId_key').using(
'btree',
table.provider.asc().nullsLast().op('text_ops'),
table.oauthId.asc().nullsLast().op('text_ops'),
),
foreignKey({
columns: [table.userId],
foreignColumns: [user.id],
name: 'OAuthProvider_userId_fkey',
})
.onUpdate('cascade')
.onDelete('restrict'),
],
);
export const file = pgTable(
'File',
{
id: text().primaryKey().notNull(),
createdAt: timestamp({ precision: 3, mode: 'string' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
deletesAt: timestamp({ precision: 3, mode: 'string' }),
name: text().notNull(),
originalName: text(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
size: bigint({ mode: 'number' }).notNull(),
type: text().notNull(),
views: integer().default(0).notNull(),
maxViews: integer(),
favorite: boolean().default(false).notNull(),
password: text(),
userId: text(),
folderId: text(),
},
(table) => [
foreignKey({
columns: [table.userId],
foreignColumns: [user.id],
name: 'File_userId_fkey',
})
.onUpdate('cascade')
.onDelete('set null'),
foreignKey({
columns: [table.folderId],
foreignColumns: [folder.id],
name: 'File_folderId_fkey',
})
.onUpdate('cascade')
.onDelete('set null'),
],
);
export const thumbnail = pgTable(
'Thumbnail',
{
id: text().primaryKey().notNull(),
createdAt: timestamp({ precision: 3, mode: 'string' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
path: text().notNull(),
fileId: text().notNull(),
},
(table) => [
uniqueIndex('Thumbnail_fileId_key').using('btree', table.fileId.asc().nullsLast().op('text_ops')),
foreignKey({
columns: [table.fileId],
foreignColumns: [file.id],
name: 'Thumbnail_fileId_fkey',
})
.onUpdate('cascade')
.onDelete('cascade'),
],
);
export const incompleteFile = pgTable(
'IncompleteFile',
{
id: text().primaryKey().notNull(),
createdAt: timestamp({ precision: 3, mode: 'string' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
status: incompleteFileStatus().notNull(),
chunksTotal: integer().notNull(),
chunksComplete: integer().notNull(),
metadata: jsonb().notNull(),
userId: text().notNull(),
},
(table) => [
foreignKey({
columns: [table.userId],
foreignColumns: [user.id],
name: 'IncompleteFile_userId_fkey',
})
.onUpdate('cascade')
.onDelete('cascade'),
],
);
export const tag = pgTable(
'Tag',
{
id: text().primaryKey().notNull(),
createdAt: timestamp({ precision: 3, mode: 'string' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
name: text().notNull(),
color: text().notNull(),
userId: text(),
},
(table) => [
uniqueIndex('Tag_name_key').using('btree', table.name.asc().nullsLast().op('text_ops')),
foreignKey({
columns: [table.userId],
foreignColumns: [user.id],
name: 'Tag_userId_fkey',
})
.onUpdate('cascade')
.onDelete('set null'),
],
);
export const invite = pgTable(
'Invite',
{
id: text().primaryKey().notNull(),
createdAt: timestamp({ precision: 3, mode: 'string' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
expiresAt: timestamp({ precision: 3, mode: 'string' }),
code: text().notNull(),
uses: integer().default(0).notNull(),
maxUses: integer(),
inviterId: text().notNull(),
},
(table) => [
uniqueIndex('Invite_code_key').using('btree', table.code.asc().nullsLast().op('text_ops')),
foreignKey({
columns: [table.inviterId],
foreignColumns: [user.id],
name: 'Invite_inviterId_fkey',
})
.onUpdate('cascade')
.onDelete('cascade'),
],
);
export const fileToTag = pgTable(
'_FileToTag',
{
a: text('A').notNull(),
b: text('B').notNull(),
},
(table) => [
index().using('btree', table.b.asc().nullsLast().op('text_ops')),
foreignKey({
columns: [table.a],
foreignColumns: [file.id],
name: '_FileToTag_A_fkey',
})
.onUpdate('cascade')
.onDelete('cascade'),
foreignKey({
columns: [table.b],
foreignColumns: [tag.id],
name: '_FileToTag_B_fkey',
})
.onUpdate('cascade')
.onDelete('cascade'),
primaryKey({ columns: [table.a, table.b], name: '_FileToTag_AB_pkey' }),
],
);

View File

@@ -6,7 +6,6 @@ export const DATABASE_TO_PROP = {
coreReturnHttpsUrls: 'core.returnHttpsUrls',
coreDefaultDomain: 'core.defaultDomain',
coreTempDirectory: 'core.tempDirectory',
coreTrustProxy: 'core.trustProxy',
chunksMax: 'chunks.max',
chunksSize: 'chunks.size',

View File

@@ -1,9 +1,8 @@
import { log } from '@/lib/logger';
import { readFileSync } from 'node:fs';
import { parse } from './transform';
export type EnvType = 'string' | 'string[]' | 'number' | 'boolean' | 'byte' | 'ms' | 'json';
export function env(property: string, env: string, type: EnvType, isDb: boolean = false) {
export function env(property: string, env: string | string[], type: EnvType, isDb: boolean = false) {
return {
variable: env,
property,
@@ -16,14 +15,7 @@ export const ENVS = [
env('core.port', 'CORE_PORT', 'number'),
env('core.hostname', 'CORE_HOSTNAME', 'string'),
env('core.secret', 'CORE_SECRET', 'string'),
env('core.databaseUrl', 'DATABASE_URL', 'string'),
// or
env('core.database.username', 'DATABASE_USERNAME', 'string', true),
env('core.database.password', 'DATABASE_PASSWORD', 'string', true),
env('core.database.host', 'DATABASE_HOST', 'string', true),
env('core.database.port', 'DATABASE_PORT', 'number', true),
env('core.database.name', 'DATABASE_NAME', 'string', true),
env('core.databaseUrl', ['DATABASE_URL', 'CORE_DATABASE_URL'], 'string'),
env('datasource.type', 'DATASOURCE_TYPE', 'string'),
env('datasource.s3.accessKeyId', 'DATASOURCE_S3_ACCESS_KEY_ID', 'string'),
@@ -40,7 +32,6 @@ export const ENVS = [
env('ssl.cert', 'SSL_CERT', 'string'),
// database stuff
env('core.trustProxy', 'CORE_TRUST_PROXY', 'boolean', true),
env('core.returnHttpsUrls', 'CORE_RETURN_HTTPS_URLS', 'boolean', true),
env('core.defaultDomain', 'CORE_DEFAULT_DOMAIN', 'string', true),
env('core.tempDirectory', 'CORE_TEMP_DIRECTORY', 'string', true),
@@ -168,62 +159,11 @@ export const PROP_TO_ENV: Record<string, string | string[]> = Object.fromEntries
ENVS.map((env) => [env.property, env.variable]),
);
export const REQUIRED_DB_VARS = [
'DATABASE_USERNAME',
'DATABASE_PASSWORD',
'DATABASE_HOST',
'DATABASE_PORT',
'DATABASE_NAME',
];
type EnvResult = {
env: Record<string, any>;
dbEnv: Record<string, any>;
};
export function checkDbVars(): boolean {
if (process.env.DATABASE_URL) return true;
for (let i = 0; i !== REQUIRED_DB_VARS.length; ++i) {
if (process.env[REQUIRED_DB_VARS[i]] === undefined) {
return false;
}
}
return true;
}
export function readDbVars(): Record<string, string> {
const logger = log('config').c('readDbVars');
if (process.env.DATABASE_URL) return { DATABASE_URL: process.env.DATABASE_URL };
const dbVars: Record<string, string> = {};
for (let i = 0; i !== REQUIRED_DB_VARS.length; ++i) {
const value = process.env[REQUIRED_DB_VARS[i]];
const valueFileName = process.env[`${REQUIRED_DB_VARS[i]}_FILE`];
if (valueFileName) {
try {
dbVars[REQUIRED_DB_VARS[i]] = readFileSync(valueFileName, 'utf-8').trim();
} catch {
logger.error(`Failed to read database env value from file for ${REQUIRED_DB_VARS[i]}. Exiting...`);
process.exit(1);
}
} else if (value) {
dbVars[REQUIRED_DB_VARS[i]] = value;
}
}
if (!Object.keys(dbVars).length || Object.keys(dbVars).length !== REQUIRED_DB_VARS.length) {
logger.error(
`No database environment variables found (DATABASE_URL or all of [${REQUIRED_DB_VARS.join(', ')}]), exiting...`,
);
process.exit(1);
}
return dbVars;
}
export function readEnv(): EnvResult {
const logger = log('config').c('readEnv');
const envResult: EnvResult = {
@@ -233,19 +173,12 @@ export function readEnv(): EnvResult {
for (let i = 0; i !== ENVS.length; ++i) {
const env = ENVS[i];
let value = process.env[env.variable];
const valueFileName = process.env[`${env.variable}_FILE`];
if (valueFileName) {
try {
value = readFileSync(valueFileName, 'utf-8').trim();
logger.debug('Using env value from file', { variable: env.variable, file: valueFileName });
} catch (e) {
logger.error(`Failed to read env value from file for ${env.variable}. Skipping...`).error(e as Error);
continue;
}
if (Array.isArray(env.variable)) {
env.variable = env.variable.find((v) => process.env[v] !== undefined) || 'DATABASE_URL';
}
const value = process.env[env.variable];
if (value === undefined) continue;
if (env.variable === 'DATASOURCE_TYPE') {

View File

@@ -13,14 +13,6 @@ export const rawConfig: any = {
databaseUrl: undefined,
returnHttpsUrls: undefined,
tempDirectory: undefined,
trustProxy: undefined,
database: {
username: undefined,
password: undefined,
host: undefined,
port: undefined,
name: undefined,
},
},
chunks: {
max: undefined,

View File

@@ -67,36 +67,13 @@ export const schema = z.object({
});
}
}),
databaseUrl: z.url(),
returnHttpsUrls: z.boolean().default(false),
defaultDomain: z.string().nullable().default(null),
tempDirectory: z
.string()
.transform((s) => resolve(s))
.default(join(tmpdir(), 'zipline')),
trustProxy: z.boolean().default(false),
databaseUrl: z.url(),
database: z
.object({
username: z.string().nullable().default(null),
password: z.string().nullable().default(null),
host: z.string().nullable().default(null),
port: z.number().nullable().default(null),
name: z.string().nullable().default(null),
})
.superRefine((val, c) => {
const values = Object.values(val);
const someSet = values.some((v) => v !== null);
const allSet = values.every((v) => v !== null);
if (someSet && !allSet) {
c.addIssue({
code: 'custom',
message: 'If one database field is set, all fields must be set',
});
}
}),
}),
chunks: z.object({
max: z.string().default('95mb'),

View File

@@ -4,7 +4,6 @@ import { type Prisma, PrismaClient } from '@/prisma/client';
import { metadataSchema } from './models/incompleteFile';
import { metricDataSchema } from './models/metric';
import { userViewSchema } from './models/user';
import { readDbVars, REQUIRED_DB_VARS } from '../config/read/env';
const building = !!process.env.ZIPLINE_BUILD;
@@ -32,27 +31,12 @@ function parseDbLog(env: string): Prisma.LogLevel[] {
.filter((v) => v) as unknown as Prisma.LogLevel[];
}
function pgConnectionString() {
const vars = readDbVars();
if (vars.DATABASE_URL) return vars.DATABASE_URL;
return `postgresql://${vars.DATABASE_USERNAME}:${vars.DATABASE_PASSWORD}@${vars.DATABASE_HOST}:${vars.DATABASE_PORT}/${vars.DATABASE_NAME}`;
}
function getClient() {
const logger = log('db');
const connectionString = pgConnectionString();
if (!connectionString) {
logger.error(`either DATABASE_URL or all of [${REQUIRED_DB_VARS.join(', ')}] not set, exiting...`);
process.exit(1);
}
logger.info('connecting to database ' + process.env.DATABASE_URL);
process.env.DATABASE_URL = connectionString;
logger.info('connecting to database', { url: connectionString });
const adapter = new PrismaPg({ connectionString });
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
const client = new PrismaClient({
adapter,
log: process.env.ZIPLINE_DB_LOG ? parseDbLog(process.env.ZIPLINE_DB_LOG) : undefined,

View File

@@ -0,0 +1,87 @@
import { migrate } from 'drizzle-orm/node-postgres/migrator';
import { drizzle } from 'drizzle-orm/node-postgres';
import pg from 'pg';
import { join } from 'path';
import { log } from '@/lib/logger';
const logger = log('db').c('drizzle');
async function drizzleBootstrap(client: pg.Client) {
await client.query('CREATE SCHEMA IF NOT EXISTS "drizzle"');
await client.query(`
CREATE TABLE IF NOT EXISTS "drizzle"."__drizzle_migrations" (
id SERIAL PRIMARY KEY,
hash text NOT NULL,
created_at numeric
)
`);
}
async function migrateExistingPrisma(client: pg.Client) {
// check if there is a _prisma_migrations table
// if there is then we continue with prisma -> drizzle.
const resPrisma = await client.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = '_prisma_migrations'
)
`);
const existsPrisma = resPrisma.rows[0]?.exists;
if (!existsPrisma) {
logger.debug('no existing prisma migrations found, skipping prisma -> drizzle migration step');
return;
}
logger.debug('existing prisma migrations found, migrating to drizzle');
// at this point, there should already be a __drizzle_migrations table
// now looking for the first migration so we can manually insert it if needed.
const firstMigration = 1756926875085;
const res = await client.query(
`
SELECT COUNT(*) FROM drizzle.__drizzle_migrations WHERE created_at = $1
`,
[firstMigration],
);
const count = parseInt(res.rows[0]?.count || '0', 10);
logger.debug('finding existing first migrations', { count });
if (count === 0) {
logger.debug('inserting first migration manually');
await client.query(
`
INSERT INTO drizzle.__drizzle_migrations (created_at, hash)
VALUES ($1, $2)
`,
[firstMigration, 'manual'],
);
}
}
export async function runDrizzleMigrations() {
const client = new pg.Client({ connectionString: process.env.DATABASE_URL });
await client.connect();
const db = drizzle(client);
// ensure drizzle migrations table exists
await drizzleBootstrap(client);
// migrate from prisma to drizzle
await migrateExistingPrisma(client);
// now we can run migrations with drizzle
await migrate(db, {
migrationsFolder: join(process.cwd(), 'src', 'drizzle'),
});
logger.info('migrations complete');
}

View File

@@ -1,58 +0,0 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
const FIELDS = ['name', 'originalName', 'tags', 'type', 'size', 'createdAt', 'favorite', 'views'] as const;
export const defaultFields: FieldSettings[] = [
{ field: 'name', visible: true },
{ field: 'originalName', visible: false },
{ field: 'tags', visible: true },
{ field: 'type', visible: true },
{ field: 'size', visible: true },
{ field: 'createdAt', visible: true },
{ field: 'favorite', visible: true },
{ field: 'views', visible: true },
];
export type FieldSettings = {
field: (typeof FIELDS)[number];
visible: boolean;
};
export type FileTableSettings = {
fields: FieldSettings[];
setVisible: (field: FieldSettings['field'], visible: boolean) => void;
setIndex: (field: FieldSettings['field'], index: number) => void;
reset: () => void;
};
export const useFileTableSettingsStore = create<FileTableSettings>()(
persist(
(set) => ({
fields: defaultFields,
setVisible: (field, visible) =>
set((state) => ({
fields: state.fields.map((f) => (f.field === field ? { ...f, visible } : f)),
})),
setIndex: (field, index) =>
set((state) => {
const currentIndex = state.fields.findIndex((f) => f.field === field);
if (currentIndex === -1 || index < 0 || index >= state.fields.length) return state;
const newFields = [...state.fields];
const [movedField] = newFields.splice(currentIndex, 1);
newFields.splice(index, 0, movedField);
return { fields: newFields };
}),
reset: () => set({ fields: defaultFields }),
}),
{
name: 'zipline-file-table-settings',
},
),
);

View File

@@ -1,6 +1,5 @@
import { bytes } from '@/lib/bytes';
import { reloadSettings } from '@/lib/config';
import { checkDbVars, REQUIRED_DB_VARS } from '@/lib/config/read/env';
import { getDatasource } from '@/lib/datasource';
import { prisma } from '@/lib/db';
import { runMigrations } from '@/lib/db/migration';
@@ -47,8 +46,8 @@ async function main() {
const argv = process.argv.slice(2);
logger.info('starting zipline', { mode: MODE, version: version, argv });
if (!checkDbVars()) {
logger.error(`either DATABASE_URL or all of [${REQUIRED_DB_VARS.join(', ')}] not set, exiting...`);
if (!process.env.DATABASE_URL) {
logger.error('DATABASE_URL not set, exiting...');
process.exit(1);
}
@@ -66,13 +65,6 @@ async function main() {
await mkdir(config.core.tempDirectory, { recursive: true });
logger.debug('creating server', {
port: config.core.port,
hostname: config.core.hostname,
ssl: notNull(config.ssl.key, config.ssl.cert),
trustProxy: config.core.trustProxy,
});
const server = fastify({
https: notNull(config.ssl.key, config.ssl.cert)
? {
@@ -80,7 +72,6 @@ async function main() {
cert: await readFile(config.ssl.cert!, 'utf8'),
}
: null,
trustProxy: config.core.trustProxy,
});
await server.register(fastifyCookie, {

View File

@@ -118,7 +118,6 @@ export default fastifyPlugin(
.nullable()
.refine((value) => !value || /^[a-z0-9-.]+$/.test(value), 'Invalid domain format'),
coreReturnHttpsUrls: z.boolean(),
coreTrustProxy: z.boolean(),
chunksEnabled: z.boolean(),
chunksMax: zBytes,

View File

@@ -41,7 +41,6 @@ export const getExtension = (filename: string, override?: string): string => {
export type ApiUploadResponse = {
files: {
id: string;
name: string;
type: string;
url: string;
pending?: boolean;
@@ -213,7 +212,6 @@ export default fastifyPlugin(
response.files.push({
id: fileUpload.id,
name: fileUpload.name,
type: fileUpload.type,
url: encodeURI(responseUrl),
removedGps: removedGps || undefined,

View File

@@ -7,13 +7,13 @@ import { guess } from '@/lib/mimes';
import { randomCharacters } from '@/lib/random';
import { formatFileName } from '@/lib/uploader/formatFileName';
import { UploadHeaders, UploadOptions, parseHeaders } from '@/lib/uploader/parseHeaders';
import { Prisma } from '@/prisma/client';
import { userMiddleware } from '@/server/middleware/user';
import fastifyPlugin from 'fastify-plugin';
import { readdir, rename, rm } from 'fs/promises';
import { join } from 'path';
import { Worker } from 'worker_threads';
import { ApiUploadResponse, getExtension } from '.';
import { Prisma } from '@/prisma/client';
const logger = log('api').c('upload').c('partial');
@@ -256,7 +256,6 @@ export default fastifyPlugin(
response.files.push({
id: fileUpload.id,
name: fileUpload.name,
type: fileUpload.type,
url: responseUrl,
pending: true,