fix: refactoring, adding express api.

This commit is contained in:
charlesgauthereau
2026-02-01 15:53:50 +01:00
parent cc87cf68b5
commit 9c4d862297
17 changed files with 149 additions and 323 deletions
+2 -16
View File
@@ -26,20 +26,6 @@ export async function GET(
return NextResponse.json({error: "Missing storageId in search params"}, {status: 404})
}
// const settings = await db.query.setting.findFirst({
// where: eq(drizzleDb.schemas.setting.name, "system"),
// with: {
// storageChannel: true
// }
// });
//
// if (!settings || !settings.storageChannel) {
// return NextResponse.json({error: "Unable to get settings or no default storage channel"});
// }
const ext = fileName.split(".").pop()?.toLowerCase();
const contentType =
ext === "png"
@@ -69,7 +55,7 @@ export async function GET(
const result = await dispatchStorage(input, undefined, storageId);
if (!result.file || !Buffer.isBuffer(result.file)) {
if (!result.file || !(result.file instanceof Readable)) {
console.error(`An error occurred while getting file :`, result);
return NextResponse.json(
{error: "Invalid file payload"},
@@ -77,7 +63,7 @@ export async function GET(
);
}
const fileStream = Readable.from(result.file);
const fileStream = Readable.from(result.file as Readable);
const stream = new ReadableStream({
start(controller) {
+5 -5
View File
@@ -3,11 +3,11 @@ name: portabase-prod
services:
app:
# build:
# context: .
# dockerfile: docker/dockerfile/Dockerfile
# target: prod
image: solucetechnologies/portabase:latest
build:
context: .
dockerfile: docker/dockerfile/Dockerfile
target: prod
# image: solucetechnologies/portabase:latest
ports:
- '8887:80'
environment:
+8 -2
View File
@@ -62,7 +62,6 @@ RUN pnpm run build
FROM base AS prod
WORKDIR /app
@@ -70,6 +69,9 @@ WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN corepack enable && corepack prepare pnpm@latest --activate
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
@@ -81,14 +83,18 @@ RUN mkdir -p /app/private/uploads
RUN chown -R nextjs:nodejs /app/private
RUN chown -R nextjs:nodejs /app/public
COPY --from=builder /app/next.config.ts ./
COPY --from=builder /app/server ./server
COPY --from=builder /app/src ./src
COPY --from=builder /app/portabase.config.ts ./
COPY --from=builder /app/drizzle.config.ts ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --chown=nextjs:nodejs src/db ./src/db
COPY --from=deps /app/node_modules ./node_modules
USER root
COPY ./docker/entrypoints/app-prod-entrypoint.sh /app/app-prod-entrypoint.sh
+1 -1
View File
@@ -7,7 +7,7 @@ else
echo "[WARN] No TZ provided, using default container timezone"
fi
node server/server.js
pnpm run start
exec "$@"
+5 -6
View File
@@ -4,10 +4,8 @@
"private": true,
"scripts": {
"dev": "NODE_ENV=development tsx watch server/server.ts",
"build": "next build",
"start": "NODE_ENV=production node server/server.js",
"build": "next build --experimental-build-mode compile",
"start": "NODE_ENV=production tsx server/server.ts",
"lint": "next lint",
"email": "email dev --dir ./src/components/emails",
"db:generate": "drizzle-kit generate",
@@ -71,7 +69,7 @@
"lucide-react": "^0.553.0",
"minio": "^8.0.5",
"motion": "^12.23.24",
"next": "16.0.10",
"next": "16.1.5",
"next-safe-action": "^7.10.8",
"next-themes": "^0.4.6",
"node-cron": "^4.2.1",
@@ -115,8 +113,9 @@
"@types/react-dom": "^19.1.5",
"@zenstackhq/openapi": "^2.14.2",
"@zenstackhq/tanstack-query": "^2.14.2",
"baseline-browser-mapping": "^2.8.32",
"baseline-browser-mapping": "^2.9.19",
"drizzle-kit": "^0.31.1",
"esbuild": "^0.27.2",
"eslint": "^9.39.0",
"eslint-config-next": "^16.0.1",
"eslint-plugin-tailwindcss": "^3.18.0",
+62 -58
View File
@@ -127,7 +127,7 @@ importers:
version: 6.0.0
better-auth:
specifier: 1.4.5
version: 1.4.5(next@16.0.10(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
version: 1.4.5(next@16.1.5(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -174,11 +174,11 @@ importers:
specifier: ^12.23.24
version: 12.24.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next:
specifier: 16.0.10
version: 16.0.10(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
specifier: 16.1.5
version: 16.1.5(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next-safe-action:
specifier: ^7.10.8
version: 7.10.8(next@16.0.10(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76)
version: 7.10.8(next@16.1.5(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76)
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -301,11 +301,14 @@ importers:
specifier: ^2.14.2
version: 2.22.2(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(typescript@5.9.3)(zod@3.25.76)
baseline-browser-mapping:
specifier: ^2.8.32
version: 2.9.11
specifier: ^2.9.19
version: 2.9.19
drizzle-kit:
specifier: ^0.31.1
version: 0.31.8
esbuild:
specifier: ^0.27.2
version: 0.27.2
eslint:
specifier: ^9.39.0
version: 9.39.2(jiti@2.6.1)
@@ -1610,8 +1613,8 @@ packages:
'@next/env@15.5.2':
resolution: {integrity: sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg==}
'@next/env@16.0.10':
resolution: {integrity: sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==}
'@next/env@16.1.5':
resolution: {integrity: sha512-CRSCPJiSZoi4Pn69RYBDI9R7YK2g59vLexPQFXY0eyw+ILevIenCywzg+DqmlBik9zszEnw2HLFOUlLAcJbL7g==}
'@next/eslint-plugin-next@16.1.1':
resolution: {integrity: sha512-Ovb/6TuLKbE1UiPcg0p39Ke3puyTCIKN9hGbNItmpQsp+WX3qrjO3WaMVSi6JHr9X1NrmthqIguVHodMJbh/dw==}
@@ -1622,8 +1625,8 @@ packages:
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-arm64@16.0.10':
resolution: {integrity: sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg==}
'@next/swc-darwin-arm64@16.1.5':
resolution: {integrity: sha512-eK7Wdm3Hjy/SCL7TevlH0C9chrpeOYWx2iR7guJDaz4zEQKWcS1IMVfMb9UKBFMg1XgzcPTYPIp1Vcpukkjg6Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
@@ -1634,8 +1637,8 @@ packages:
cpu: [x64]
os: [darwin]
'@next/swc-darwin-x64@16.0.10':
resolution: {integrity: sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw==}
'@next/swc-darwin-x64@16.1.5':
resolution: {integrity: sha512-foQscSHD1dCuxBmGkbIr6ScAUF6pRoDZP6czajyvmXPAOFNnQUJu2Os1SGELODjKp/ULa4fulnBWoHV3XdPLfA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
@@ -1646,8 +1649,8 @@ packages:
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-gnu@16.0.10':
resolution: {integrity: sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw==}
'@next/swc-linux-arm64-gnu@16.1.5':
resolution: {integrity: sha512-qNIb42o3C02ccIeSeKjacF3HXotGsxh/FMk/rSRmCzOVMtoWH88odn2uZqF8RLsSUWHcAqTgYmPD3pZ03L9ZAA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -1658,8 +1661,8 @@ packages:
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-musl@16.0.10':
resolution: {integrity: sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==}
'@next/swc-linux-arm64-musl@16.1.5':
resolution: {integrity: sha512-U+kBxGUY1xMAzDTXmuVMfhaWUZQAwzRaHJ/I6ihtR5SbTVUEaDRiEU9YMjy1obBWpdOBuk1bcm+tsmifYSygfw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -1670,8 +1673,8 @@ packages:
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-gnu@16.0.10':
resolution: {integrity: sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==}
'@next/swc-linux-x64-gnu@16.1.5':
resolution: {integrity: sha512-gq2UtoCpN7Ke/7tKaU7i/1L7eFLfhMbXjNghSv0MVGF1dmuoaPeEVDvkDuO/9LVa44h5gqpWeJ4mRRznjDv7LA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -1682,8 +1685,8 @@ packages:
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-musl@16.0.10':
resolution: {integrity: sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==}
'@next/swc-linux-x64-musl@16.1.5':
resolution: {integrity: sha512-bQWSE729PbXT6mMklWLf8dotislPle2L70E9q6iwETYEOt092GDn0c+TTNj26AjmeceSsC4ndyGsK5nKqHYXjQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -1694,8 +1697,8 @@ packages:
cpu: [arm64]
os: [win32]
'@next/swc-win32-arm64-msvc@16.0.10':
resolution: {integrity: sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==}
'@next/swc-win32-arm64-msvc@16.1.5':
resolution: {integrity: sha512-LZli0anutkIllMtTAWZlDqdfvjWX/ch8AFK5WgkNTvaqwlouiD1oHM+WW8RXMiL0+vAkAJyAGEzPPjO+hnrSNQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
@@ -1706,8 +1709,8 @@ packages:
cpu: [x64]
os: [win32]
'@next/swc-win32-x64-msvc@16.0.10':
resolution: {integrity: sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q==}
'@next/swc-win32-x64-msvc@16.1.5':
resolution: {integrity: sha512-7is37HJTNQGhjPpQbkKjKEboHYQnCgpVt/4rBrrln0D9nderNxZ8ZWs8w1fAtzUx7wEyYjQ+/13myFgFj6K2Ng==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -3627,8 +3630,8 @@ packages:
resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==}
engines: {node: ^4.5.0 || >= 5.9}
baseline-browser-mapping@2.9.11:
resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==}
baseline-browser-mapping@2.9.19:
resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
hasBin: true
bcrypt-pbkdf@1.0.2:
@@ -5539,8 +5542,8 @@ packages:
sass:
optional: true
next@16.0.10:
resolution: {integrity: sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==}
next@16.1.5:
resolution: {integrity: sha512-f+wE+NSbiQgh3DSAlTaw2FwY5yGdVViAtp8TotNQj4kk4Q8Bh1sC/aL9aH+Rg1YAVn18OYXsRDT7U/079jgP7w==}
engines: {node: '>=20.9.0'}
hasBin: true
peerDependencies:
@@ -8240,7 +8243,7 @@ snapshots:
'@next/env@15.5.2': {}
'@next/env@16.0.10': {}
'@next/env@16.1.5': {}
'@next/eslint-plugin-next@16.1.1':
dependencies:
@@ -8249,49 +8252,49 @@ snapshots:
'@next/swc-darwin-arm64@15.5.2':
optional: true
'@next/swc-darwin-arm64@16.0.10':
'@next/swc-darwin-arm64@16.1.5':
optional: true
'@next/swc-darwin-x64@15.5.2':
optional: true
'@next/swc-darwin-x64@16.0.10':
'@next/swc-darwin-x64@16.1.5':
optional: true
'@next/swc-linux-arm64-gnu@15.5.2':
optional: true
'@next/swc-linux-arm64-gnu@16.0.10':
'@next/swc-linux-arm64-gnu@16.1.5':
optional: true
'@next/swc-linux-arm64-musl@15.5.2':
optional: true
'@next/swc-linux-arm64-musl@16.0.10':
'@next/swc-linux-arm64-musl@16.1.5':
optional: true
'@next/swc-linux-x64-gnu@15.5.2':
optional: true
'@next/swc-linux-x64-gnu@16.0.10':
'@next/swc-linux-x64-gnu@16.1.5':
optional: true
'@next/swc-linux-x64-musl@15.5.2':
optional: true
'@next/swc-linux-x64-musl@16.0.10':
'@next/swc-linux-x64-musl@16.1.5':
optional: true
'@next/swc-win32-arm64-msvc@15.5.2':
optional: true
'@next/swc-win32-arm64-msvc@16.0.10':
'@next/swc-win32-arm64-msvc@16.1.5':
optional: true
'@next/swc-win32-x64-msvc@15.5.2':
optional: true
'@next/swc-win32-x64-msvc@16.0.10':
'@next/swc-win32-x64-msvc@16.1.5':
optional: true
'@noble/ciphers@2.1.1': {}
@@ -10345,7 +10348,7 @@ snapshots:
dependencies:
'@types/node': 22.19.3
tapable: 2.3.0
webpack: 5.104.1(esbuild@0.27.2)
webpack: 5.104.1(esbuild@0.25.10)
transitivePeerDependencies:
- '@swc/core'
- esbuild
@@ -10934,7 +10937,7 @@ snapshots:
base64id@2.0.0: {}
baseline-browser-mapping@2.9.11: {}
baseline-browser-mapping@2.9.19: {}
bcrypt-pbkdf@1.0.2:
dependencies:
@@ -10947,7 +10950,7 @@ snapshots:
bcryptjs@2.4.3: {}
better-auth@1.4.5(next@16.0.10(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
better-auth@1.4.5(next@16.1.5(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
'@better-auth/core': 1.4.5(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.4(zod@3.25.76))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)
'@better-auth/telemetry': 1.4.5(@better-auth/core@1.4.5(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.4(zod@3.25.76))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0))
@@ -10963,7 +10966,7 @@ snapshots:
nanostores: 1.1.0
zod: 4.3.5
optionalDependencies:
next: 16.0.10(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next: 16.1.5(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -11025,7 +11028,7 @@ snapshots:
browserslist@4.28.1:
dependencies:
baseline-browser-mapping: 2.9.11
baseline-browser-mapping: 2.9.19
caniuse-lite: 1.0.30001762
electron-to-chromium: 1.5.267
node-releases: 2.0.27
@@ -12979,9 +12982,9 @@ snapshots:
neo-async@2.6.2: {}
next-safe-action@7.10.8(next@16.0.10(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76):
next-safe-action@7.10.8(next@16.1.5(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76):
dependencies:
next: 16.0.10(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next: 16.1.5(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
optionalDependencies:
@@ -13015,24 +13018,25 @@ snapshots:
- '@babel/core'
- babel-plugin-macros
next@16.0.10(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
next@16.1.5(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
'@next/env': 16.0.10
'@next/env': 16.1.5
'@swc/helpers': 0.5.15
baseline-browser-mapping: 2.9.19
caniuse-lite: 1.0.30001762
postcss: 8.4.31
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.3)
optionalDependencies:
'@next/swc-darwin-arm64': 16.0.10
'@next/swc-darwin-x64': 16.0.10
'@next/swc-linux-arm64-gnu': 16.0.10
'@next/swc-linux-arm64-musl': 16.0.10
'@next/swc-linux-x64-gnu': 16.0.10
'@next/swc-linux-x64-musl': 16.0.10
'@next/swc-win32-arm64-msvc': 16.0.10
'@next/swc-win32-x64-msvc': 16.0.10
'@next/swc-darwin-arm64': 16.1.5
'@next/swc-darwin-x64': 16.1.5
'@next/swc-linux-arm64-gnu': 16.1.5
'@next/swc-linux-arm64-musl': 16.1.5
'@next/swc-linux-x64-gnu': 16.1.5
'@next/swc-linux-x64-musl': 16.1.5
'@next/swc-win32-arm64-msvc': 16.1.5
'@next/swc-win32-x64-msvc': 16.1.5
sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
@@ -14257,14 +14261,14 @@ snapshots:
ansi-escapes: 4.3.2
supports-hyperlinks: 2.3.0
terser-webpack-plugin@5.3.16(esbuild@0.25.10)(webpack@5.104.1(esbuild@0.25.10)):
terser-webpack-plugin@5.3.16(esbuild@0.25.10)(webpack@5.104.1(esbuild@0.27.2)):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
jest-worker: 27.5.1
schema-utils: 4.3.3
serialize-javascript: 6.0.2
terser: 5.44.1
webpack: 5.104.1(esbuild@0.27.2)
webpack: 5.104.1(esbuild@0.25.10)
optionalDependencies:
esbuild: 0.25.10
@@ -14629,7 +14633,7 @@ snapshots:
webpack-sources@3.3.3: {}
webpack@5.104.1(esbuild@0.27.2):
webpack@5.104.1(esbuild@0.25.10):
dependencies:
'@types/eslint-scope': 3.7.7
'@types/estree': 1.0.8
@@ -14653,7 +14657,7 @@ snapshots:
neo-async: 2.6.2
schema-utils: 4.3.3
tapable: 2.3.0
terser-webpack-plugin: 5.3.16(esbuild@0.25.10)(webpack@5.104.1(esbuild@0.25.10))
terser-webpack-plugin: 5.3.16(esbuild@0.25.10)(webpack@5.104.1(esbuild@0.27.2))
watchpack: 2.5.0
webpack-sources: 3.3.3
transitivePeerDependencies:
-30
View File
@@ -1,30 +0,0 @@
import next from "next";
import express from "express";
import router from "./src/features/api/router";
const port = Number(process.env.PORT) || 80;
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev, port: port, turbopack: true });
const handle = app.getRequestHandler();
app.prepare().then(() => {
const server = express();
server.use("/services/v1", router);
server.use((req, res, next) => {
if (req.path.startsWith('/services/v1')) return next();
handle(req, res).catch((err) => {
console.error('Next.js App Router error:', err);
res.status(500).send('Internal Server Error');
});
});
server.listen(port, () => {
console.log(`Next.js App Router running at http://localhost:${port}`);
console.log(`Express API running at http://localhost:${port}/express`);
});
});
+4 -3
View File
@@ -8,8 +8,9 @@ const dev = process.env.NODE_ENV !== "production";
async function start() {
const nextApp = next({
dev,
port,
turbopack: true
hostname: "0.0.0.0",
turbopack: dev,
port
});
const handle = nextApp.getRequestHandler();
@@ -28,7 +29,7 @@ async function start() {
});
});
server.listen(port, () => {
server.listen(port, "0.0.0.0", () => {
console.log(
`NEXT APP → http://localhost:${port}`
);
-145
View File
@@ -1,145 +0,0 @@
import {Backup, DatabaseWith} from "@/db/schema/07_database";
import {dispatchStorage} from "@/features/storages/dispatch";
import type {StorageInput, StorageResult} from "@/features/storages/types";
import * as drizzleDb from "@/db";
import {withUpdatedAt} from "@/db/utils";
import {eq} from "drizzle-orm";
import {db} from "@/db";
import {PassThrough} from "stream";
import fs from "node:fs";
import forge from "node-forge";
import crypto from "node:crypto";
import {pipeline} from "node:stream";
import { promisify } from "util";
export async function storeBackupFilesStream(
backup: Backup,
database: DatabaseWith,
fileStream: NodeJS.ReadableStream,
fileName: string
): Promise<StorageResult[]> {
const settings = await db.query.setting.findFirst({
where: eq(drizzleDb.schemas.setting.name, "system"),
with: {storageChannel: true},
});
const defaultPolicy = settings?.storageChannel
? [{
id: null,
storageChannelId: settings.storageChannel.id,
enabled: settings.storageChannel.enabled,
}]
: [];
const enabledPolicies = database.storagePolicies?.filter(p => p.enabled) ?? [];
const policies = enabledPolicies.length ? enabledPolicies : defaultPolicy;
if (!policies.length) {
await db.update(drizzleDb.schemas.backup)
.set(withUpdatedAt({status: "failed"}))
.where(eq(drizzleDb.schemas.backup.id, backup.id));
return [];
}
const storagePath = `backups/${database.project?.slug}/${fileName}`;
// const teeStreams = teeStreamSafe(fileStream, policies.length);
const teeStreams = teeStream(fileStream, policies.length);
const results = await Promise.all(
policies.map(async (policy, index) => {
const pass = teeStreams[index];
console.log("policy", index, policy.id)
const [backupStorage] = await db
.insert(drizzleDb.schemas.backupStorage)
.values({
backupId: backup.id,
storageChannelId: policy.storageChannelId,
status: "pending",
path: storagePath,
})
.returning();
const input: StorageInput = {
action: "upload",
data: {
path: storagePath,
file: teeStreams[index],
},
};
let result: StorageResult;
try {
result = policy.id
? await dispatchStorage(input, policy.id)
: await dispatchStorage(input, undefined, policy.storageChannelId);
} catch (err: any) {
pass.destroy(err);
result = {success: false, provider: null, error: err.message};
}
await db.update(drizzleDb.schemas.backupStorage)
.set(withUpdatedAt({status: result.success ? "success" : "failed"}))
.where(eq(drizzleDb.schemas.backupStorage.id, backupStorage.id));
return result;
})
);
console.log("result upload", results);
const backupStatus = results.some(r => r.success) ? "success" : "failed";
await db.update(drizzleDb.schemas.backup)
.set(withUpdatedAt({status: backupStatus}))
.where(eq(drizzleDb.schemas.backup.id, backup.id));
return results;
}
export function createDecryptionStream(
encryptedAesKeyHex: string,
ivHex: string
) {
const privateKeyPem = fs.readFileSync(
"private/keys/server_private.pem",
"utf8"
);
const privateKey = forge.pki.privateKeyFromPem(privateKeyPem);
const encryptedBytes = forge.util.hexToBytes(encryptedAesKeyHex);
const aesKeyBytes = privateKey.decrypt(encryptedBytes, "RSA-OAEP", {
md: forge.md.sha256.create(),
mgf1: {md: forge.md.sha256.create()},
});
const aesKey = Buffer.from(aesKeyBytes, "binary");
const iv = Buffer.from(ivHex, "hex");
return crypto.createDecipheriv("aes-256-cbc", aesKey, iv);
}
export function getFileExtension(dbType: string) {
switch (dbType) {
case "postgresql":
return ".dump";
case "mysql":
return ".sql";
default:
return ".dump";
}
}
const pipelinePromise = promisify(pipeline);
function teeStream(stream: NodeJS.ReadableStream, n: number): PassThrough[] {
const taps = Array.from({ length: n }, () => new PassThrough());
taps.forEach(tap => pipelinePromise(stream, tap).catch(err => tap.destroy(err)));
return taps;
}
+36 -7
View File
@@ -6,8 +6,10 @@ import {db} from "@/db";
import * as drizzleDb from "@/db";
import {eq} from "drizzle-orm";
import {withUpdatedAt} from "@/db/utils";
import {saveStreamToTempFile} from "@/features/api/upload/helpers/file";
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
import {eventEmitter} from "@/features/shared/event";
import forge from "node-forge";
import crypto from "node:crypto";
/**
* Save stream to a temporary file and upload to all storage providers in parallel.
@@ -15,12 +17,11 @@ import {saveStreamToTempFile} from "@/features/api/upload/helpers/file";
export default async function uploadTempFileToProviders(
backup: Backup,
database: DatabaseWith,
inputStream: NodeJS.ReadableStream,
// inputStream: NodeJS.ReadableStream,
tmpPath: string,
fileName: string
): Promise<StorageResult[]> {
const tmpPath = await saveStreamToTempFile(inputStream, fileName);
const stats = fs.statSync(tmpPath);
const fileSize = stats.size;
console.log(tmpPath);
@@ -78,8 +79,6 @@ export default async function uploadTempFileToProviders(
} catch (err: any) {
result = {success: false, provider: null, error: err.message};
}
console.log(result);
await db.update(drizzleDb.schemas.backupStorage)
.set(withUpdatedAt({status: result.success ? "success" : "failed"}))
.where(eq(drizzleDb.schemas.backupStorage.id, backupStorage.id));
@@ -99,6 +98,36 @@ export default async function uploadTempFileToProviders(
fs.existsSync(tmpPath) && fs.unlinkSync(tmpPath);
console.log(results);
eventEmitter.emit('modification', {update: true});
if (!backupStatus) {
await sendNotificationsBackupRestore(database, "error_backup");
}
await sendNotificationsBackupRestore(database, "success_backup");
return results;
}
export function createDecryptionStream(
encryptedAesKeyHex: string,
ivHex: string
) {
const privateKeyPem = fs.readFileSync(
"private/keys/server_private.pem",
"utf8"
);
const privateKey = forge.pki.privateKeyFromPem(privateKeyPem);
const encryptedBytes = forge.util.hexToBytes(encryptedAesKeyHex);
const aesKeyBytes = privateKey.decrypt(encryptedBytes, "RSA-OAEP", {
md: forge.md.sha256.create(),
mgf1: {md: forge.md.sha256.create()},
});
const aesKey = Buffer.from(aesKeyBytes, "binary");
const iv = Buffer.from(ivHex, "hex");
return crypto.createDecipheriv("aes-256-cbc", aesKey, iv);
}
+12
View File
@@ -13,3 +13,15 @@ export async function saveStreamToTempFile(stream: NodeJS.ReadableStream, fileNa
await pipelineAsync(stream, writeStream);
return tmpPath;
}
export function getFileExtension(dbType: string) {
switch (dbType) {
case "postgresql":
return ".dump";
case "mysql":
return ".sql";
default:
return ".dump";
}
}
+11 -5
View File
@@ -3,13 +3,13 @@ import {v4 as uuidv4} from "uuid";
import {and, eq} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {db} from "@/db";
import {createDecryptionStream, getFileExtension} from "./helpers";
import {isUuidv4} from "@/utils/verify-uuid";
import uploadTempFileToProviders from "@/features/api/upload/helpers/common";
import uploadTempFileToProviders, {createDecryptionStream} from "@/features/api/upload/helpers/common";
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
import {Backup} from "@/db/schema/07_database";
import {withUpdatedAt} from "@/db/utils";
import {eventEmitter} from "@/features/shared/event";
import {getFileExtension, saveStreamToTempFile} from "@/features/api/upload/helpers/file";
const router: Router = express.Router();
@@ -86,9 +86,15 @@ router.post("/:agentId", async (req: Request, res: Response) => {
const decryptedStream = req.pipe(decipher);
await uploadTempFileToProviders(backup, database, decryptedStream, fileName);
await sendNotificationsBackupRestore(database, "success_backup");
eventEmitter.emit('modification', {update: true});
const tmpPath = await saveStreamToTempFile(decryptedStream, fileName);
if (!tmpPath) {
return res.status(500).json({
error: "Unable to save tmp backup file",
});
}
uploadTempFileToProviders(backup, database, tmpPath, fileName);
return res.json({success: true});
} else {
+1 -1
View File
@@ -11,7 +11,7 @@ import * as drizzleDb from "@/db";
import {withUpdatedAt} from "@/db/utils";
import {eq} from "drizzle-orm";
import {db} from "@/db";
import crypto, {createHash} from "crypto";
import {createHash} from "crypto";
import {getServerUrl} from "@/utils/get-server-url";
import path from "path";
@@ -1,36 +1,3 @@
// import {drive_v3, google} from "googleapis";
// import Drive = drive_v3.Drive;
// import {GoogleDriveConfig} from "@/features/storages/providers/google-drive/types";
//
//
// export async function getGoogleDriveClient(config: GoogleDriveConfig): Promise<Drive> {
// const auth = new google.auth.JWT({
// email: config.clientEmail,
// key: config.privateKey?.replace(/\\n/g, "\n"),
// scopes: ["https://www.googleapis.com/auth/drive"],
// });
//
// return google.drive({
// version: "v3",
// auth,
// });
// }
//
//
// export async function findFileByName(
// drive: any,
// name: string,
// folderId: string
// ): Promise<string | null> {
// const res = await drive.files.list({
// q: `name='${name}' and '${folderId}' in parents and trashed=false`,
// fields: "files(id)",
// pageSize: 1,
// });
//
// return res.data.files?.[0]?.id ?? null;
// }
import {drive_v3, google} from "googleapis";
import {GoogleDriveConfig} from "@/features/storages/providers/google-drive/types";
import Drive = drive_v3.Drive;
@@ -42,7 +9,6 @@ export async function getGoogleDriveClient(config: GoogleDriveConfig): Promise<D
const oauth2Client = new google.auth.OAuth2(
config.clientId,
config.clientSecret,
// config.redirectUri
baseUrl
);
@@ -77,8 +77,6 @@ export async function getGoogleDrive(
const fileId = await resolveFilePath(client, input.data.path, config.folderId);
if (!fileId) return {success: false, provider: "google-drive", error: "File not found"};
const res = await client.files.get(
{fileId, alt: "media", supportsAllDrives: true},
{responseType: "stream"}
+1 -3
View File
@@ -1,7 +1,7 @@
import {
StorageProviderKind,
StorageInput,
StorageResult, StorageMetaData,
StorageResult,
} from '../types';
import {uploadLocal, getLocal, deleteLocal, pingLocal} from './local';
@@ -39,8 +39,6 @@ const handlers: Record<StorageProviderKind, ProviderHandler> = {
delete: deleteGoogleDrive,
ping: pingGoogleDrive,
}
// gcs: null as any,
// azure: null as any,
};
export async function dispatchViaProvider(
+1 -5
View File
@@ -35,26 +35,22 @@ export async function uploadS3(
input: { data: StorageUploadInput, metadata?: StorageMetaData }
): Promise<StorageResult> {
const client = await getS3Client(config);
console.log(client);
await ensureBucket(config);
const key = `${BASE_DIR}${input.data.path}`;
const file = input.data.file;
console.log(key)
let uploadStream: Readable;
if (Buffer.isBuffer(file) || file instanceof Uint8Array) {
uploadStream = Readable.from(file);
} else if ((file as any).pipe) {
uploadStream = file;
} else {
throw new Error("Unsupported file type for streaming upload");
return {success: false, provider: "s3", error: "Unsupported file type for streaming upload"};
}
try {
const result = await client.putObject(config.bucketName, key, uploadStream, input.data.size);
console.log(result);
} catch (err: any) {
return {success: false, provider: "s3", error: err.message};
}