mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Compare commits
15
Commits
1.2.4
...
1.2.5-rc.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24d90bb750 | ||
|
|
c2726e6b18 | ||
|
|
a746ce9169 | ||
|
|
c00b967179 | ||
|
|
f6e749ecf9 | ||
|
|
8f5e0686c8 | ||
|
|
b9f0d9279b | ||
|
|
cd69f0c82f | ||
|
|
884746f3a5 | ||
|
|
9c4d862297 | ||
|
|
cc87cf68b5 | ||
|
|
7c5d57ddc1 | ||
|
|
86d7ab9864 | ||
|
|
f201e4593a | ||
|
|
71129e556e |
+2
-2
@@ -26,5 +26,5 @@ keywords:
|
|||||||
- web-ui
|
- web-ui
|
||||||
- agent
|
- agent
|
||||||
license: Apache-2.0
|
license: Apache-2.0
|
||||||
version: 1.2.4
|
version: 1.2.5-rc.7
|
||||||
date-released: "2026-01-29"
|
date-released: "2026-02-01"
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
import {isUuidv4} from "@/utils/verify-uuid";
|
||||||
import {v4 as uuidv4} from "uuid";
|
import {v4 as uuidv4} from "uuid";
|
||||||
import {eventEmitter} from "../../../events/route";
|
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {Backup} from "@/db/schema/07_database";
|
import {Backup} from "@/db/schema/07_database";
|
||||||
@@ -10,6 +9,7 @@ import {withUpdatedAt} from "@/db/utils";
|
|||||||
import {decryptedDump, getFileExtension} from "./helpers";
|
import {decryptedDump, getFileExtension} from "./helpers";
|
||||||
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
|
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
|
||||||
import {storeBackupFiles} from "@/features/storages/helpers";
|
import {storeBackupFiles} from "@/features/storages/helpers";
|
||||||
|
import {eventEmitter} from "@/features/shared/event";
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -35,8 +35,6 @@ export async function POST(
|
|||||||
const method = formData.get("method") as string | null;
|
const method = formData.get("method") as string | null;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (!generatedId || !isUuidv4(generatedId)) {
|
if (!generatedId || !isUuidv4(generatedId)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "generatedId is not a valid UUID"},
|
{error: "generatedId is not a valid UUID"},
|
||||||
@@ -116,7 +114,6 @@ export async function POST(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "File is required for successful backup"},
|
{error: "File is required for successful backup"},
|
||||||
@@ -132,9 +129,6 @@ export async function POST(
|
|||||||
const buffer = Buffer.from(await decryptedFile.arrayBuffer());
|
const buffer = Buffer.from(await decryptedFile.arrayBuffer());
|
||||||
|
|
||||||
|
|
||||||
console.log(extension)
|
|
||||||
|
|
||||||
|
|
||||||
const storageResults = await storeBackupFiles(backup, database, buffer, fileName)
|
const storageResults = await storeBackupFiles(backup, database, buffer, fileName)
|
||||||
eventEmitter.emit('modification', {update: true});
|
eventEmitter.emit('modification', {update: true});
|
||||||
|
|
||||||
@@ -173,4 +167,3 @@ export async function POST(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
import {isUuidv4} from "@/utils/verify-uuid";
|
||||||
import {eventEmitter} from "../../../events/route";
|
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {and, eq} from "drizzle-orm";
|
import {and, eq} from "drizzle-orm";
|
||||||
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
|
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
|
||||||
|
import {eventEmitter} from "@/features/shared/event";
|
||||||
|
|
||||||
export type BodyResultRestore = {
|
export type BodyResultRestore = {
|
||||||
generatedId: string
|
generatedId: string
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {handleDatabases} from "./helpers";
|
import {handleDatabases} from "./helpers";
|
||||||
import {eventEmitter} from "../../../events/route";
|
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {EDbmsSchema} from "@/db/schema/types";
|
import {EDbmsSchema} from "@/db/schema/types";
|
||||||
import {eq} from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
import {isUuidv4} from "@/utils/verify-uuid";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
|
import {eventEmitter} from "@/features/shared/event";
|
||||||
|
|
||||||
export type databaseAgent = {
|
export type databaseAgent = {
|
||||||
name: string,
|
name: string,
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import {EventEmitter} from 'events';
|
|||||||
import {auth} from "@/lib/auth/auth";
|
import {auth} from "@/lib/auth/auth";
|
||||||
import {headers} from "next/headers";
|
import {headers} from "next/headers";
|
||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
|
import {eventEmitter} from "@/features/shared/event";
|
||||||
|
|
||||||
export const eventEmitter = new EventEmitter();
|
// export const eventEmitter = new EventEmitter();
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
|
|
||||||
|
|||||||
@@ -35,10 +35,9 @@ export async function GET(
|
|||||||
const result = await dispatchStorage(input, undefined, storageId);
|
const result = await dispatchStorage(input, undefined, storageId);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return NextResponse.json({error: "Enable to get file from privided storage channel, an error occurred !"})
|
return NextResponse.json({error: "Enable to get file from provided storage channel, an error occurred !"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const fileName = path.basename(pathFromUrl);
|
const fileName = path.basename(pathFromUrl);
|
||||||
|
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
@@ -58,14 +57,14 @@ export async function GET(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.file || !Buffer.isBuffer(result.file)) {
|
if (!result.file || !(result.file instanceof Readable)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "Invalid file payload"},
|
{error: "Invalid file payload"},
|
||||||
{status: 500}
|
{status: 500}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileStream = Readable.from(result.file);
|
const fileStream = Readable.from(result.file as Readable);
|
||||||
|
|
||||||
const stream = new ReadableStream({
|
const stream = new ReadableStream({
|
||||||
start(controller) {
|
start(controller) {
|
||||||
|
|||||||
@@ -26,20 +26,6 @@ export async function GET(
|
|||||||
return NextResponse.json({error: "Missing storageId in search params"}, {status: 404})
|
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 ext = fileName.split(".").pop()?.toLowerCase();
|
||||||
const contentType =
|
const contentType =
|
||||||
ext === "png"
|
ext === "png"
|
||||||
@@ -69,7 +55,7 @@ export async function GET(
|
|||||||
|
|
||||||
const result = await dispatchStorage(input, undefined, storageId);
|
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);
|
console.error(`An error occurred while getting file :`, result);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "Invalid file payload"},
|
{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({
|
const stream = new ReadableStream({
|
||||||
start(controller) {
|
start(controller) {
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ name: portabase-prod
|
|||||||
services:
|
services:
|
||||||
|
|
||||||
app:
|
app:
|
||||||
# build:
|
build:
|
||||||
# context: .
|
context: .
|
||||||
# dockerfile: docker/dockerfile/Dockerfile
|
dockerfile: docker/dockerfile/Dockerfile
|
||||||
# target: prod
|
target: prod
|
||||||
image: solucetechnologies/portabase:latest
|
# image: solucetechnologies/portabase:latest
|
||||||
ports:
|
ports:
|
||||||
- '8887:80'
|
- '8887:80'
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ RUN pnpm run build
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
FROM base AS prod
|
FROM base AS prod
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -70,6 +69,9 @@ WORKDIR /app
|
|||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
|
|
||||||
|
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
RUN adduser --system --uid 1001 nextjs
|
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/private
|
||||||
RUN chown -R nextjs:nodejs /app/public
|
RUN chown -R nextjs:nodejs /app/public
|
||||||
|
|
||||||
|
|
||||||
COPY --from=builder /app/next.config.ts ./
|
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/portabase.config.ts ./
|
||||||
COPY --from=builder /app/drizzle.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/standalone ./
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
COPY --chown=nextjs:nodejs src/db ./src/db
|
COPY --chown=nextjs:nodejs src/db ./src/db
|
||||||
|
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
COPY ./docker/entrypoints/app-prod-entrypoint.sh /app/app-prod-entrypoint.sh
|
COPY ./docker/entrypoints/app-prod-entrypoint.sh /app/app-prod-entrypoint.sh
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ else
|
|||||||
echo "[WARN] No TZ provided, using default container timezone"
|
echo "[WARN] No TZ provided, using default container timezone"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
pnpm run start
|
||||||
node server.js
|
#node server.js
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ const nextConfig: NextConfig = {
|
|||||||
bodySizeLimit: "10gb",
|
bodySizeLimit: "10gb",
|
||||||
},
|
},
|
||||||
proxyClientMaxBodySize: '10gb',
|
proxyClientMaxBodySize: '10gb',
|
||||||
|
|
||||||
},
|
},
|
||||||
async headers() {
|
async headers() {
|
||||||
return [
|
return [
|
||||||
|
|||||||
+9
-5
@@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "portabase",
|
"name": "portabase",
|
||||||
"version": "1.2.4",
|
"version": "1.2.5-rc.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack -p 8887",
|
"dev": "NODE_ENV=development tsx watch server/server.ts",
|
||||||
"build": "next build --experimental-build-mode compile",
|
"build": "next build --experimental-build-mode compile",
|
||||||
"start": "next start",
|
"start": "NODE_ENV=production tsx server/server.ts",
|
||||||
|
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"email": "email dev --dir ./src/components/emails",
|
"email": "email dev --dir ./src/components/emails",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
@@ -47,6 +48,7 @@
|
|||||||
"@t3-oss/env-nextjs": "^0.13.4",
|
"@t3-oss/env-nextjs": "^0.13.4",
|
||||||
"@tanstack/react-query": "^5.76.1",
|
"@tanstack/react-query": "^5.76.1",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
|
"@types/express": "^5.0.6",
|
||||||
"@types/nodemailer": "^6.4.17",
|
"@types/nodemailer": "^6.4.17",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@zenstackhq/runtime": "2.14.2",
|
"@zenstackhq/runtime": "2.14.2",
|
||||||
@@ -62,12 +64,13 @@
|
|||||||
"drizzle-orm": "^0.43.1",
|
"drizzle-orm": "^0.43.1",
|
||||||
"drizzle-zod": "^0.7.1",
|
"drizzle-zod": "^0.7.1",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
|
"express": "^5.2.1",
|
||||||
"googleapis": "^170.1.0",
|
"googleapis": "^170.1.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.553.0",
|
"lucide-react": "^0.553.0",
|
||||||
"minio": "^8.0.5",
|
"minio": "^8.0.5",
|
||||||
"motion": "^12.23.24",
|
"motion": "^12.23.24",
|
||||||
"next": "16.0.10",
|
"next": "16.1.5",
|
||||||
"next-safe-action": "^7.10.8",
|
"next-safe-action": "^7.10.8",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
@@ -111,8 +114,9 @@
|
|||||||
"@types/react-dom": "^19.1.5",
|
"@types/react-dom": "^19.1.5",
|
||||||
"@zenstackhq/openapi": "^2.14.2",
|
"@zenstackhq/openapi": "^2.14.2",
|
||||||
"@zenstackhq/tanstack-query": "^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",
|
"drizzle-kit": "^0.31.1",
|
||||||
|
"esbuild": "^0.27.2",
|
||||||
"eslint": "^9.39.0",
|
"eslint": "^9.39.0",
|
||||||
"eslint-config-next": "^16.0.1",
|
"eslint-config-next": "^16.0.1",
|
||||||
"eslint-plugin-tailwindcss": "^3.18.0",
|
"eslint-plugin-tailwindcss": "^3.18.0",
|
||||||
|
|||||||
Generated
+1677
-2239
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
|||||||
|
import express from "express";
|
||||||
|
import router from "../src/features/api/router";
|
||||||
|
|
||||||
|
export function mountApi(app: express.Express) {
|
||||||
|
app.use("/services/v1", router);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import next from "next";
|
||||||
|
import express from "express";
|
||||||
|
import {mountApi} from "./api";
|
||||||
|
import http from "http";
|
||||||
|
|
||||||
|
const port = Number(process.env.PORT) || 8887;
|
||||||
|
const dev = process.env.NODE_ENV !== "production";
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
const nextApp = next({
|
||||||
|
dev,
|
||||||
|
hostname: "0.0.0.0",
|
||||||
|
turbopack: dev,
|
||||||
|
port
|
||||||
|
});
|
||||||
|
|
||||||
|
const handle = nextApp.getRequestHandler();
|
||||||
|
|
||||||
|
await nextApp.prepare();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
req.setTimeout(0);
|
||||||
|
res.setTimeout(0);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
mountApi(app);
|
||||||
|
|
||||||
|
app.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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const server = http.createServer(app);
|
||||||
|
|
||||||
|
server.setTimeout(0);
|
||||||
|
server.headersTimeout = 0;
|
||||||
|
server.requestTimeout = 0;
|
||||||
|
server.keepAliveTimeout = 0;
|
||||||
|
|
||||||
|
server.listen(port, "0.0.0.0", () => {
|
||||||
|
console.log(`NEXT APP → http://localhost:${port}`);
|
||||||
|
console.log(`API → http://localhost:${port}/services/v1`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
start();
|
||||||
@@ -75,7 +75,7 @@ export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => {
|
|||||||
if (inner?.success) {
|
if (inner?.success) {
|
||||||
toast.success(inner.actionSuccess?.message);
|
toast.success(inner.actionSuccess?.message);
|
||||||
if (action === "download") {
|
if (action === "download") {
|
||||||
console.log(inner.value)
|
|
||||||
const url = inner.value
|
const url = inner.value
|
||||||
if (typeof url === "string") {
|
if (typeof url === "string") {
|
||||||
window.open(url, "_self");
|
window.open(url, "_self");
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export const downloadBackupAction = userAction.schema(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(input)
|
|
||||||
|
|
||||||
const result = await dispatchStorage(input, undefined, backupStorage.storageChannelId);
|
const result = await dispatchStorage(input, undefined, backupStorage.storageChannelId);
|
||||||
console.log(result);
|
console.log(result);
|
||||||
|
|||||||
@@ -106,8 +106,7 @@ export const ChannelPoliciesForm = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
console.log(policiesToUpdate);
|
|
||||||
console.log(policiesToAdd);
|
|
||||||
|
|
||||||
const promises = kind === "notification"
|
const promises = kind === "notification"
|
||||||
? [
|
? [
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import {Request, Response, NextFunction} from "express";
|
||||||
|
|
||||||
|
export function loggingMiddleware(req: Request, res: Response, next: NextFunction) {
|
||||||
|
console.log(`[API - SERVICES - V1] Received ${req.method} request : ${req.url} at ${new Date().toISOString()}`);
|
||||||
|
next();
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import express, { Router } from "express";
|
||||||
|
import uploadRouter from "./upload";
|
||||||
|
import { loggingMiddleware } from "@/features/api/middleware";
|
||||||
|
|
||||||
|
const router: Router = express.Router();
|
||||||
|
|
||||||
|
router.use(loggingMiddleware);
|
||||||
|
router.use("/upload", uploadRouter);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import {Backup, DatabaseWith} from "@/db/schema/07_database";
|
||||||
|
import {dispatchStorage} from "@/features/storages/dispatch";
|
||||||
|
import type {StorageInput, StorageResult} from "@/features/storages/types";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {eq} from "drizzle-orm";
|
||||||
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
export default async function uploadTempFileToProviders(
|
||||||
|
backup: Backup,
|
||||||
|
database: DatabaseWith,
|
||||||
|
// inputStream: NodeJS.ReadableStream,
|
||||||
|
tmpPath: string,
|
||||||
|
fileName: string
|
||||||
|
): Promise<StorageResult[]> {
|
||||||
|
|
||||||
|
const stats = fs.statSync(tmpPath);
|
||||||
|
const fileSize = stats.size;
|
||||||
|
console.log(tmpPath);
|
||||||
|
|
||||||
|
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));
|
||||||
|
fs.existsSync(tmpPath) && fs.unlinkSync(tmpPath);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const storagePath = `backups/${database.project?.slug}/${fileName}`;
|
||||||
|
|
||||||
|
const results = await Promise.all(
|
||||||
|
policies.map(async (policy) => {
|
||||||
|
const fileStream = fs.createReadStream(tmpPath);
|
||||||
|
|
||||||
|
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: fileStream, size: fileSize},
|
||||||
|
};
|
||||||
|
|
||||||
|
let result: StorageResult;
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = policy.id
|
||||||
|
? await dispatchStorage(input, policy.id)
|
||||||
|
: await dispatchStorage(input, undefined, policy.storageChannelId);
|
||||||
|
} catch (err: any) {
|
||||||
|
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;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const backupStatus = results.some(r => r.success) ? "success" : "failed";
|
||||||
|
await db.update(drizzleDb.schemas.backup)
|
||||||
|
.set(withUpdatedAt({
|
||||||
|
fileSize: fileSize,
|
||||||
|
status: backupStatus
|
||||||
|
}))
|
||||||
|
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import {promisify} from "util";
|
||||||
|
import {pipeline} from "stream";
|
||||||
|
|
||||||
|
const pipelineAsync = promisify(pipeline);
|
||||||
|
const TMP_DIR = path.join(process.cwd(), "private/uploads/tmp");
|
||||||
|
fs.mkdirSync(TMP_DIR, { recursive: true });
|
||||||
|
|
||||||
|
export async function saveStreamToTempFile(stream: NodeJS.ReadableStream, fileName: string): Promise<string> {
|
||||||
|
const tmpPath = path.join(TMP_DIR, fileName);
|
||||||
|
const writeStream = fs.createWriteStream(tmpPath);
|
||||||
|
await pipelineAsync(stream, writeStream);
|
||||||
|
return tmpPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function getFileExtension(dbType: string) {
|
||||||
|
switch (dbType) {
|
||||||
|
case "postgresql":
|
||||||
|
return ".dump";
|
||||||
|
case "mysql":
|
||||||
|
return ".sql";
|
||||||
|
default:
|
||||||
|
return ".dump";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import express, {Request, Response, Router} from "express";
|
||||||
|
import {v4 as uuidv4} from "uuid";
|
||||||
|
import {and, eq} from "drizzle-orm";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {isUuidv4} from "@/utils/verify-uuid";
|
||||||
|
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();
|
||||||
|
|
||||||
|
router.post("/:agentId", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const agentId = req.params.agentId as string | undefined;
|
||||||
|
const generatedId = req.headers["x-generated-id"] as string | undefined;
|
||||||
|
const status = req.headers["x-status"] as string | undefined;
|
||||||
|
const encryptedAesKeyHex = req.headers["x-aes-key"] as string | undefined;
|
||||||
|
const ivHex = req.headers["x-iv"] as string | undefined;
|
||||||
|
const method = (req.headers["x-method"] as string) ?? "manual";
|
||||||
|
const extension = req.headers["x-extension"] as string | undefined;
|
||||||
|
|
||||||
|
|
||||||
|
if (!generatedId || !encryptedAesKeyHex || !ivHex || !agentId || !status) {
|
||||||
|
return res.status(400).json({error: "Missing required headers/params"});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isUuidv4(generatedId)) {
|
||||||
|
return res.status(400).json({error: "generatedId is not a valid UUID"});
|
||||||
|
}
|
||||||
|
|
||||||
|
const agent = await db.query.agent.findFirst({
|
||||||
|
where: eq(drizzleDb.schemas.agent.id, agentId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!agent) {
|
||||||
|
return res.status(404).json({error: "Agent not found"});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const database = await db.query.database.findFirst({
|
||||||
|
where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
|
||||||
|
with: {
|
||||||
|
project: true,
|
||||||
|
storagePolicies: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!database) {
|
||||||
|
return res.status(404).json({error: "Database not found"});
|
||||||
|
}
|
||||||
|
|
||||||
|
let backup: Backup | null | undefined = null;
|
||||||
|
|
||||||
|
if (method === "automatic") {
|
||||||
|
[backup] = await db
|
||||||
|
.insert(drizzleDb.schemas.backup)
|
||||||
|
.values({
|
||||||
|
status: "ongoing",
|
||||||
|
databaseId: database.id,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
} else {
|
||||||
|
backup = await db.query.backup.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.backup.databaseId, database.id),
|
||||||
|
eq(drizzleDb.schemas.backup.status, "ongoing")
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!backup) {
|
||||||
|
return res.status(404).json({error: "Backup not found"});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (status === "success") {
|
||||||
|
|
||||||
|
const decipher = createDecryptionStream(encryptedAesKeyHex, ivHex);
|
||||||
|
|
||||||
|
const fileExt = extension || getFileExtension(database.dbms);
|
||||||
|
const fileName = `${uuidv4()}${fileExt}`;
|
||||||
|
|
||||||
|
const decryptedStream = req.pipe(decipher);
|
||||||
|
|
||||||
|
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 {
|
||||||
|
await db
|
||||||
|
.update(drizzleDb.schemas.backup)
|
||||||
|
.set(withUpdatedAt({status: 'failed'}))
|
||||||
|
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
||||||
|
|
||||||
|
eventEmitter.emit('modification', {update: true});
|
||||||
|
|
||||||
|
await sendNotificationsBackupRestore(database, "error_backup");
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
error: "Backup successfully updated with status failed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("Upload error:", err);
|
||||||
|
return res.status(500).json({
|
||||||
|
error: "Upload failed",
|
||||||
|
detail: err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { EventEmitter } from "events";
|
||||||
|
|
||||||
|
export const eventEmitter = new EventEmitter();
|
||||||
@@ -11,7 +11,7 @@ import * as drizzleDb from "@/db";
|
|||||||
import {withUpdatedAt} from "@/db/utils";
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
import {eq} from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import crypto, {createHash} from "crypto";
|
import {createHash} from "crypto";
|
||||||
import {getServerUrl} from "@/utils/get-server-url";
|
import {getServerUrl} from "@/utils/get-server-url";
|
||||||
import path from "path";
|
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 {drive_v3, google} from "googleapis";
|
||||||
import {GoogleDriveConfig} from "@/features/storages/providers/google-drive/types";
|
import {GoogleDriveConfig} from "@/features/storages/providers/google-drive/types";
|
||||||
import Drive = drive_v3.Drive;
|
import Drive = drive_v3.Drive;
|
||||||
@@ -42,7 +9,6 @@ export async function getGoogleDriveClient(config: GoogleDriveConfig): Promise<D
|
|||||||
const oauth2Client = new google.auth.OAuth2(
|
const oauth2Client = new google.auth.OAuth2(
|
||||||
config.clientId,
|
config.clientId,
|
||||||
config.clientSecret,
|
config.clientSecret,
|
||||||
// config.redirectUri
|
|
||||||
baseUrl
|
baseUrl
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -24,18 +24,27 @@ export async function uploadGoogleDrive(
|
|||||||
? await ensureFolderPath(client, folderPath, config.folderId)
|
? await ensureFolderPath(client, folderPath, config.folderId)
|
||||||
: config.folderId;
|
: config.folderId;
|
||||||
|
|
||||||
|
|
||||||
const existing = await findFileByName(client, fileName, folderId);
|
const existing = await findFileByName(client, fileName, folderId);
|
||||||
if (existing) return {success: false, provider: "google-drive", error: "File already exists"};
|
if (existing) return {success: false, provider: "google-drive", error: "File already exists"};
|
||||||
|
|
||||||
|
let fileStream: Readable;
|
||||||
|
const file = input.data.file;
|
||||||
|
if (Buffer.isBuffer(file) || file instanceof Uint8Array) {
|
||||||
|
fileStream = Readable.from(file);
|
||||||
|
} else if ((file as any).pipe) {
|
||||||
|
fileStream = file as Readable;
|
||||||
|
} else {
|
||||||
|
throw new Error("Unsupported file type for streaming upload");
|
||||||
|
}
|
||||||
|
|
||||||
await client.files.create({
|
await client.files.create({
|
||||||
requestBody: {name: fileName, parents: [folderId]},
|
requestBody: {name: fileName, parents: [folderId]},
|
||||||
media: {body: Readable.from(input.data.file as Buffer)},
|
media: {body: fileStream},
|
||||||
fields: "id",
|
fields: "id",
|
||||||
supportsAllDrives: true,
|
supportsAllDrives: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (input.data.url) {
|
if (input.data.url) {
|
||||||
const url = await generateFileUrl(input);
|
const url = await generateFileUrl(input);
|
||||||
if (!url) {
|
if (!url) {
|
||||||
@@ -70,9 +79,11 @@ export async function getGoogleDrive(
|
|||||||
|
|
||||||
const res = await client.files.get(
|
const res = await client.files.get(
|
||||||
{fileId, alt: "media", supportsAllDrives: true},
|
{fileId, alt: "media", supportsAllDrives: true},
|
||||||
{responseType: "arraybuffer"}
|
{responseType: "stream"}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const stream = res.data as Readable;
|
||||||
|
|
||||||
|
|
||||||
if (input.data.signedUrl) {
|
if (input.data.signedUrl) {
|
||||||
const url = await generateFileUrl(input);
|
const url = await generateFileUrl(input);
|
||||||
@@ -88,7 +99,7 @@ export async function getGoogleDrive(
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
provider: "google-drive",
|
provider: "google-drive",
|
||||||
file: Buffer.from(res.data as ArrayBuffer),
|
file: stream,
|
||||||
url: url,
|
url: url,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -96,7 +107,7 @@ export async function getGoogleDrive(
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
provider: "google-drive",
|
provider: "google-drive",
|
||||||
file: Buffer.from(res.data as ArrayBuffer),
|
file: stream,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
// export type GoogleDriveConfig = {
|
|
||||||
// clientEmail: string;
|
|
||||||
// privateKey: string;
|
|
||||||
// folderId: string;
|
|
||||||
// };
|
|
||||||
|
|
||||||
|
|
||||||
export type GoogleDriveConfig = {
|
export type GoogleDriveConfig = {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
StorageProviderKind,
|
StorageProviderKind,
|
||||||
StorageInput,
|
StorageInput,
|
||||||
StorageResult, StorageMetaData,
|
StorageResult,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
import {uploadLocal, getLocal, deleteLocal, pingLocal} from './local';
|
import {uploadLocal, getLocal, deleteLocal, pingLocal} from './local';
|
||||||
@@ -39,8 +39,6 @@ const handlers: Record<StorageProviderKind, ProviderHandler> = {
|
|||||||
delete: deleteGoogleDrive,
|
delete: deleteGoogleDrive,
|
||||||
ping: pingGoogleDrive,
|
ping: pingGoogleDrive,
|
||||||
}
|
}
|
||||||
// gcs: null as any,
|
|
||||||
// azure: null as any,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function dispatchViaProvider(
|
export async function dispatchViaProvider(
|
||||||
|
|||||||
@@ -1,95 +1,109 @@
|
|||||||
"use server"
|
"use server"
|
||||||
import {mkdir, writeFile, unlink, readFile} from 'fs/promises';
|
import {mkdir, unlink} from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {StorageDeleteInput, StorageGetInput, StorageMetaData, StorageResult, StorageUploadInput} from '../types';
|
import {StorageDeleteInput, StorageGetInput, StorageMetaData, StorageResult, StorageUploadInput} from '../types';
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import {getServerUrl} from "@/utils/get-server-url";
|
|
||||||
import {generateFileUrl} from "@/features/storages/helpers";
|
import {generateFileUrl} from "@/features/storages/helpers";
|
||||||
|
import {Readable} from "node:stream";
|
||||||
|
|
||||||
const BASE_DIR = "/private/uploads/";
|
const BASE_DIR = "/private/uploads/";
|
||||||
|
|
||||||
export async function uploadLocal(
|
export async function uploadLocal(
|
||||||
config: { baseDir?: string },
|
config: { baseDir?: string },
|
||||||
input: { data: StorageUploadInput, metadata?: StorageMetaData }
|
input: { data: StorageUploadInput; metadata?: StorageMetaData }
|
||||||
): Promise<StorageResult> {
|
): Promise<StorageResult> {
|
||||||
const base = config.baseDir || BASE_DIR;
|
const base = config.baseDir || BASE_DIR;
|
||||||
const fullPath = path.join(process.cwd(), base, input.data.path);
|
const fullPath = path.join(process.cwd(), base, input.data.path);
|
||||||
|
|
||||||
const dir = path.dirname(fullPath);
|
const dir = path.dirname(fullPath);
|
||||||
|
|
||||||
await mkdir(dir, {recursive: true});
|
await mkdir(dir, { recursive: true });
|
||||||
await writeFile(fullPath, input.data.file);
|
|
||||||
|
|
||||||
|
try {
|
||||||
if (input.data.url) {
|
const file = input.data.file;
|
||||||
const url = await generateFileUrl(input);
|
if (Buffer.isBuffer(file)) {
|
||||||
if (!url) {
|
await fs.promises.writeFile(fullPath, input.data.file);
|
||||||
return {
|
} else if (file instanceof Readable) {
|
||||||
success: false,
|
await new Promise<void>((resolve, reject) => {
|
||||||
provider: "local",
|
const writable = fs.createWriteStream(fullPath);
|
||||||
response: "Unable to get url file"
|
file.pipe(writable);
|
||||||
};
|
writable.on("finish", resolve);
|
||||||
|
writable.on("error", reject);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return { success: false, provider: "local", error: "Unsupported file type. Must be Buffer or ReadableStream" };
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
success: true,
|
if (input.data.url) {
|
||||||
provider: 'local',
|
const url = await generateFileUrl(input);
|
||||||
url: url
|
if (!url) {
|
||||||
};
|
return { success: false, provider: "local", response: "Unable to get URL" };
|
||||||
|
}
|
||||||
|
return { success: true, provider: "local", url };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, provider: "local" };
|
||||||
|
} catch (err: any) {
|
||||||
|
try { await unlink(fullPath); } catch {}
|
||||||
|
return { success: false, provider: "local", error: err.message || "Upload failed" };
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
provider: 'local',
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLocal(
|
export async function getLocal(
|
||||||
config: { baseDir?: string },
|
config: { baseDir?: string },
|
||||||
input: { data: StorageGetInput, metadata: StorageMetaData }
|
input: { data: StorageGetInput; metadata: StorageMetaData }
|
||||||
): Promise<StorageResult> {
|
): Promise<StorageResult> {
|
||||||
const base = config.baseDir || BASE_DIR;
|
const base = config.baseDir || BASE_DIR;
|
||||||
const filePath = path.join(process.cwd(), base, input.data.path)
|
const filePath = path.join(process.cwd(), base, input.data.path);
|
||||||
const fileName = path.basename(input.data.path);
|
|
||||||
|
|
||||||
const file = await readFile(filePath);
|
|
||||||
|
|
||||||
if (!fs.existsSync(filePath)) {
|
if (!fs.existsSync(filePath)) {
|
||||||
console.error("File not found at:", filePath);
|
console.error("File not found at:", filePath);
|
||||||
return ({
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
provider: 'local',
|
provider: "local",
|
||||||
});
|
error: "File not found",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileStream: fs.ReadStream | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fileStream = fs.createReadStream(filePath);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("Error creating read stream:", err);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
provider: "local",
|
||||||
|
error: err.message,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.data.signedUrl) {
|
if (input.data.signedUrl) {
|
||||||
const url = await generateFileUrl(input);
|
const url = await generateFileUrl(input);
|
||||||
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
provider: "local",
|
provider: "local",
|
||||||
response: "Unable to get url file"
|
error: "Unable to generate signed URL",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
provider: "local",
|
provider: "local",
|
||||||
file: file,
|
file: fileStream,
|
||||||
url: url,
|
url,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
provider: "local",
|
provider: "local",
|
||||||
file: file,
|
file: fileStream,
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export async function deleteLocal(
|
export async function deleteLocal(
|
||||||
config: { baseDir?: string },
|
config: { baseDir?: string },
|
||||||
input: { data: StorageDeleteInput, metadata?: StorageMetaData }
|
input: { data: StorageDeleteInput, metadata?: StorageMetaData }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as Minio from "minio";
|
import * as Minio from "minio";
|
||||||
import {StorageDeleteInput, StorageGetInput, StorageMetaData, StorageResult, StorageUploadInput} from "../types";
|
import {StorageDeleteInput, StorageGetInput, StorageMetaData, StorageResult, StorageUploadInput} from "../types";
|
||||||
import {generateFileUrl} from "@/features/storages/helpers";
|
import {Readable} from "node:stream";
|
||||||
|
|
||||||
type S3Config = {
|
type S3Config = {
|
||||||
endPointUrl: string;
|
endPointUrl: string;
|
||||||
@@ -32,72 +32,66 @@ async function ensureBucket(config: S3Config) {
|
|||||||
|
|
||||||
export async function uploadS3(
|
export async function uploadS3(
|
||||||
config: S3Config,
|
config: S3Config,
|
||||||
input: { data: StorageUploadInput, metadata?: StorageMetaData }
|
input: { data: StorageUploadInput, metadata?: StorageMetaData }
|
||||||
): Promise<StorageResult> {
|
): Promise<StorageResult> {
|
||||||
const client = await getS3Client(config);
|
const client = await getS3Client(config);
|
||||||
await ensureBucket(config);
|
await ensureBucket(config);
|
||||||
|
|
||||||
const key = `${BASE_DIR}${input.data.path}`;
|
const key = `${BASE_DIR}${input.data.path}`;
|
||||||
|
const file = input.data.file;
|
||||||
|
|
||||||
|
let uploadStream: Readable;
|
||||||
|
if (Buffer.isBuffer(file) || file instanceof Uint8Array) {
|
||||||
|
uploadStream = Readable.from(file);
|
||||||
|
} else if ((file as any).pipe) {
|
||||||
|
uploadStream = file;
|
||||||
|
} else {
|
||||||
|
return {success: false, provider: "s3", error: "Unsupported file type for streaming upload"};
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.statObject(config.bucketName, key);
|
const result = await client.putObject(config.bucketName, key, uploadStream, input.data.size);
|
||||||
return {success: false, provider: "s3", error: "File already exists"};
|
} catch (err: any) {
|
||||||
} catch {
|
return {success: false, provider: "s3", error: err.message};
|
||||||
// continue if not found
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.putObject(config.bucketName, key, input.data.file as Buffer);
|
return {success: true, provider: "s3"};
|
||||||
|
|
||||||
|
|
||||||
if (input.data.url) {
|
|
||||||
const url = await generateFileUrl(input);
|
|
||||||
if (!url) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
provider: "s3",
|
|
||||||
response: "Unable to get url file"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
provider: 's3',
|
|
||||||
url: url
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
provider: 's3',
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getS3(config: S3Config, input: { data: StorageGetInput, metadata: StorageMetaData }): Promise<StorageResult> {
|
|
||||||
const client = await getS3Client(config);
|
|
||||||
|
|
||||||
|
export async function getS3(
|
||||||
|
config: S3Config,
|
||||||
|
input: { data: StorageGetInput, metadata: StorageMetaData }
|
||||||
|
): Promise<StorageResult> {
|
||||||
|
const client = await getS3Client(config);
|
||||||
const key = `${BASE_DIR}${input.data.path}`;
|
const key = `${BASE_DIR}${input.data.path}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.statObject(config.bucketName, key);
|
await client.statObject(config.bucketName, key);
|
||||||
} catch {
|
} catch {
|
||||||
return {success: false, provider: "s3", error: "File not found"};
|
return {success: false, provider: "s3", error: "File not found"};
|
||||||
}
|
}
|
||||||
|
|
||||||
const presignedUrl = await client.presignedGetObject(config.bucketName, key, 60);
|
|
||||||
|
|
||||||
const fileStream = await client.getObject(config.bucketName, key);
|
const fileStream = await client.getObject(config.bucketName, key);
|
||||||
const chunks: Buffer[] = [];
|
|
||||||
for await (const chunk of fileStream) chunks.push(chunk as Buffer);
|
let presignedUrl: string | undefined;
|
||||||
const buffer = Buffer.concat(chunks);
|
if (input.data.signedUrl) {
|
||||||
|
presignedUrl = await client.presignedGetObject(config.bucketName, key, input.data.expiresInSeconds ?? 60);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
provider: "s3",
|
provider: "s3",
|
||||||
file: buffer,
|
file: fileStream as unknown as Buffer | Readable,
|
||||||
url: presignedUrl,
|
url: presignedUrl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteS3(config: S3Config, input: { data: StorageDeleteInput, metadata?: StorageMetaData }): Promise<StorageResult> {
|
|
||||||
|
export async function deleteS3(config: S3Config, input: {
|
||||||
|
data: StorageDeleteInput,
|
||||||
|
metadata?: StorageMetaData
|
||||||
|
}): Promise<StorageResult> {
|
||||||
const client = await getS3Client(config);
|
const client = await getS3Client(config);
|
||||||
const key = `${BASE_DIR}${input.data.path}`;
|
const key = `${BASE_DIR}${input.data.path}`;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import {Readable} from "node:stream";
|
||||||
|
|
||||||
export type StorageProviderKind =
|
export type StorageProviderKind =
|
||||||
| 'local'
|
| 'local'
|
||||||
| 's3'
|
| 's3'
|
||||||
@@ -20,9 +22,10 @@ export type StorageMetaData = {
|
|||||||
|
|
||||||
export interface StorageUploadInput {
|
export interface StorageUploadInput {
|
||||||
path: string;
|
path: string;
|
||||||
file: Buffer | Uint8Array;
|
file: Readable | Buffer | Uint8Array;
|
||||||
url?: boolean;
|
url?: boolean;
|
||||||
contentType?: string;
|
contentType?: string;
|
||||||
|
size?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StorageGetInput {
|
export interface StorageGetInput {
|
||||||
@@ -37,7 +40,7 @@ export interface StorageDeleteInput {
|
|||||||
|
|
||||||
export type StorageInput =
|
export type StorageInput =
|
||||||
| { action: 'upload'; data: StorageUploadInput, metadata?: StorageMetaData }
|
| { action: 'upload'; data: StorageUploadInput, metadata?: StorageMetaData }
|
||||||
| { action: 'get'; data: StorageGetInput, metadata: StorageMetaData}
|
| { action: 'get'; data: StorageGetInput, metadata: StorageMetaData }
|
||||||
| { action: 'delete'; data: StorageDeleteInput, metadata?: StorageMetaData }
|
| { action: 'delete'; data: StorageDeleteInput, metadata?: StorageMetaData }
|
||||||
| { action: 'ping'; };
|
| { action: 'ping'; };
|
||||||
|
|
||||||
@@ -45,7 +48,7 @@ export interface StorageResult {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
provider: StorageProviderKind | null;
|
provider: StorageProviderKind | null;
|
||||||
url?: string;
|
url?: string;
|
||||||
file?: Buffer;
|
file?: Buffer | Readable;
|
||||||
error?: string;
|
error?: string;
|
||||||
response?: any;
|
response?: any;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -517,7 +517,6 @@ export const getActiveMember = async () => {
|
|||||||
const member = await auth.api.getActiveMember({
|
const member = await auth.api.getActiveMember({
|
||||||
headers: await headers(),
|
headers: await headers(),
|
||||||
});
|
});
|
||||||
console.log(member);
|
|
||||||
|
|
||||||
return member as MemberWithUser;
|
return member as MemberWithUser;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import nodemailer from "nodemailer";
|
import nodemailer from "nodemailer";
|
||||||
|
import {Server} from "./types"
|
||||||
|
|
||||||
export const createTransporter = (server: Server) => {
|
export const createTransporter = (server: Server) => {
|
||||||
const portNumber = Number(server.port);
|
const portNumber = Number(server.port);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {db} from "@/db";
|
|||||||
import {eq} from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {createTransporter} from "@/lib/email/helpers";
|
import {createTransporter} from "@/lib/email/helpers";
|
||||||
|
import {Payload} from "@/lib/email/types";
|
||||||
|
|
||||||
export const sendEmail = async (data: Payload) => {
|
export const sendEmail = async (data: Payload) => {
|
||||||
const settings = await db
|
const settings = await db
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
|
||||||
type Payload = {
|
export type Payload = {
|
||||||
to: string;
|
to: string;
|
||||||
from?: string;
|
from?: string;
|
||||||
subject: string;
|
subject: string;
|
||||||
html: any;
|
html: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Server = {
|
export type Server = {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
user: string;
|
user: string;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {enforceRetentionGFS} from "@/lib/tasks/database/retention-gsf";
|
|||||||
import {retentionPolicy} from "@/db/schema/07_database";
|
import {retentionPolicy} from "@/db/schema/07_database";
|
||||||
import {isNull} from "drizzle-orm";
|
import {isNull} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {eventEmitter} from "../../../../app/api/events/route";
|
import {eventEmitter} from "@/features/shared/event";
|
||||||
|
|
||||||
|
|
||||||
export const retentionCleanTask = async () => {
|
export const retentionCleanTask = async () => {
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ async function getS3Client() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const baseConfig = {
|
const baseConfig = {
|
||||||
endPoint: settings.s3EndPointUrl ?? "",
|
endPoint: settings?.s3EndPointUrl ?? "",
|
||||||
accessKey: settings.s3AccessKeyId ?? "",
|
accessKey: settings?.s3AccessKeyId ?? "",
|
||||||
secretKey: settings.s3SecretAccessKey ?? "",
|
secretKey: settings?.s3SecretAccessKey ?? "",
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Minio.Client({
|
return new Minio.Client({
|
||||||
|
|||||||
Reference in New Issue
Block a user