mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
399a3e09de | ||
|
|
1a735bda42 | ||
|
|
991196cc30 | ||
|
|
5f48903a73 | ||
|
|
5dfe1b758e | ||
|
|
eff7b24b7a | ||
|
|
c328620f3d | ||
|
|
cdd01e695c | ||
|
|
614a9e057c |
@@ -4,10 +4,6 @@ on:
|
|||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
image_name:
|
image_name:
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
default: 'solucetechnologies/portabase'
|
|
||||||
image_name2:
|
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: 'portabase/portabase'
|
default: 'portabase/portabase'
|
||||||
@@ -28,70 +24,99 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
DOCKER_PASSWORD:
|
DOCKER_PASSWORD:
|
||||||
required: true
|
required: true
|
||||||
DOCKER_USERNAME_2:
|
|
||||||
required: true
|
|
||||||
DOCKER_PASSWORD_2:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
|
||||||
runs-on: ubuntu-latest
|
build:
|
||||||
permissions:
|
name: Build and push Docker images
|
||||||
packages: write
|
runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-latest' || matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' }}
|
||||||
contents: read
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform: [ linux/amd64, linux/arm64 ]
|
||||||
steps:
|
steps:
|
||||||
- name: Check out the repo
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set tags
|
- name: Login to Docker
|
||||||
id: set-tags
|
|
||||||
run: |
|
|
||||||
REF_NAME=${GITHUB_REF#refs/tags/}
|
|
||||||
TAGS="${{ inputs.image_name }}:$REF_NAME"
|
|
||||||
TAGS2="${{ inputs.image_name2 }}:$REF_NAME"
|
|
||||||
if [[ "${{ inputs.add_latest }}" == "true" ]]; then
|
|
||||||
TAGS="$TAGS,${{ inputs.image_name }}:latest"
|
|
||||||
TAGS2="$TAGS2,${{ inputs.image_name2 }}:latest"
|
|
||||||
fi
|
|
||||||
echo "tags=$TAGS" >> $GITHUB_OUTPUT
|
|
||||||
echo "tags2=$TAGS2" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Set up QEMU (enables multi-arch emulation)
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Log in to Docker Hub account 1
|
|
||||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Build and push to solucetechnologies/portabase
|
- name: Set image tag
|
||||||
|
id: set-tags
|
||||||
|
run: |
|
||||||
|
REF_NAME=${GITHUB_REF#refs/tags/}
|
||||||
|
if [ "${{ matrix.platform }}" = "linux/amd64" ]; then
|
||||||
|
IMAGE="${{ inputs.image_name }}:$REF_NAME-amd64"
|
||||||
|
else
|
||||||
|
IMAGE="${{ inputs.image_name }}:$REF_NAME-arm64"
|
||||||
|
fi
|
||||||
|
echo "image=$IMAGE" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build and push image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ${{ inputs.dockerfile }}
|
file: ${{ inputs.dockerfile }}
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: ${{ matrix.platform }}
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.set-tags.outputs.tags }}
|
tags: ${{ steps.set-tags.outputs.image }}
|
||||||
target: ${{ inputs.target }}
|
target: ${{ inputs.target }}
|
||||||
|
|
||||||
- name: Log in to Docker Hub account 2
|
- name: Prepare artifact name
|
||||||
|
id: artifact
|
||||||
|
run: |
|
||||||
|
platform=${{ matrix.platform }}
|
||||||
|
echo "safe_platform=${platform//\//-}" >> $GITHUB_OUTPUT
|
||||||
|
echo "${{ steps.set-tags.outputs.image }}" > image.txt
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
|
||||||
|
with:
|
||||||
|
name: image-${{ steps.artifact.outputs.safe_platform }}
|
||||||
|
path: image.txt
|
||||||
|
if-no-files-found: warn
|
||||||
|
compression-level: 6
|
||||||
|
overwrite: false
|
||||||
|
include-hidden-files: false
|
||||||
|
|
||||||
|
create-manifest:
|
||||||
|
name: Create multi-arch Docker manifest (Portabase)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
steps:
|
||||||
|
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
|
||||||
|
with:
|
||||||
|
name: image-linux-amd64
|
||||||
|
path: /tmp/digests/amd64
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
|
||||||
|
with:
|
||||||
|
name: image-linux-arm64
|
||||||
|
path: /tmp/digests/arm64
|
||||||
|
|
||||||
|
- name: Login to Docker
|
||||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME_2 }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD_2 }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Create and push manifest list
|
||||||
|
working-directory: /tmp/digests
|
||||||
|
run: |
|
||||||
|
DOCKER_IMAGES="$(cat amd64/image.txt) $(cat arm64/image.txt)"
|
||||||
|
REF_NAME=${GITHUB_REF#refs/tags/}
|
||||||
|
MANIFEST_IMAGE="${{ inputs.image_name }}:$REF_NAME"
|
||||||
|
|
||||||
|
docker buildx imagetools create $DOCKER_IMAGES -t $MANIFEST_IMAGE
|
||||||
|
docker buildx imagetools inspect $MANIFEST_IMAGE
|
||||||
|
|
||||||
|
if [ "${{ inputs.add_latest }}" = "true" ]; then
|
||||||
|
docker buildx imagetools create $DOCKER_IMAGES -t ${{ inputs.image_name }}:latest
|
||||||
|
docker buildx imagetools inspect ${{ inputs.image_name }}:latest
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- name: Build and push to portabase/portabase
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ${{ inputs.dockerfile }}
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.set-tags.outputs.tags2 }}
|
|
||||||
target: ${{ inputs.target }}
|
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ jobs:
|
|||||||
secrets:
|
secrets:
|
||||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
DOCKER_USERNAME_2: ${{ secrets.DOCKER_USERNAME_2 }}
|
|
||||||
DOCKER_PASSWORD_2: ${{ secrets.DOCKER_PASSWORD_2 }}
|
|
||||||
|
|
||||||
github_release:
|
github_release:
|
||||||
needs: docker_publish
|
needs: docker_publish
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ jobs:
|
|||||||
secrets:
|
secrets:
|
||||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
DOCKER_USERNAME_2: ${{ secrets.DOCKER_USERNAME_2 }}
|
|
||||||
DOCKER_PASSWORD_2: ${{ secrets.DOCKER_PASSWORD_2 }}
|
|
||||||
|
|
||||||
github_release:
|
github_release:
|
||||||
needs: docker_publish
|
needs: docker_publish
|
||||||
|
|||||||
+2
-2
@@ -26,5 +26,5 @@ keywords:
|
|||||||
- web-ui
|
- web-ui
|
||||||
- agent
|
- agent
|
||||||
license: Apache-2.0
|
license: Apache-2.0
|
||||||
version: 1.2.5-rc.7
|
version: 1.2.5-rc.12
|
||||||
date-released: "2026-02-01"
|
date-released: "2026-02-10"
|
||||||
|
|||||||
@@ -1,53 +1,52 @@
|
|||||||
import fs from "node:fs";
|
import { NextResponse } from "next/server";
|
||||||
import forge from "node-forge";
|
import { eq } from "drizzle-orm";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
|
||||||
|
export function withAgentCheck(handler: Function) {
|
||||||
|
return async (request: Request, context: { params: Promise<{ agentId: string }> }) => {
|
||||||
|
try {
|
||||||
|
const agentId = (await context.params).agentId;
|
||||||
|
|
||||||
|
const agent = await db.query.agent.findFirst({
|
||||||
|
where: eq(drizzleDb.schemas.agent.id, agentId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!agent) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Agent not found" },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler(request, { ...context, agent });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error in agent middleware:", err);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function decryptedDump(file: File, aesKeyHex: string, ivHex: string, fileExtension: string): Promise<File> {
|
export async function getDatabaseOrThrow(generatedId: string) {
|
||||||
const privateKeyPem = fs.readFileSync("private/keys/server_private.pem", "utf8");
|
const database = await db.query.database.findFirst({
|
||||||
const privateKey = forge.pki.privateKeyFromPem(privateKeyPem);
|
where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
|
||||||
|
with: {
|
||||||
// Decrypt AES key with RSA-OAEP
|
project: true,
|
||||||
const encryptedAesKey = forge.util.hexToBytes(aesKeyHex);
|
alertPolicies: true,
|
||||||
const aesKey = privateKey.decrypt(encryptedAesKey, "RSA-OAEP", {
|
storagePolicies: true
|
||||||
md: forge.md.sha256.create(),
|
}
|
||||||
mgf1: {md: forge.md.sha256.create()},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Read encrypted file content
|
if (!database) {
|
||||||
const encryptedBuffer = Buffer.from(await file.arrayBuffer());
|
throw NextResponse.json(
|
||||||
const iv = forge.util.hexToBytes(ivHex);
|
{ error: "Database associated with generatedId not found" },
|
||||||
|
{ status: 404 }
|
||||||
// AES decryption
|
);
|
||||||
const decipher = forge.cipher.createDecipher("AES-CBC", aesKey);
|
|
||||||
decipher.start({iv});
|
|
||||||
decipher.update(forge.util.createBuffer(encryptedBuffer.toString("binary")));
|
|
||||||
const success = decipher.finish();
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
throw new Error("Decryption failed");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptedBytes = decipher.output.getBytes();
|
return database;
|
||||||
const decryptedBuffer = Buffer.from(decryptedBytes, "binary");
|
|
||||||
|
|
||||||
// Return a File so you can use file.arrayBuffer() later
|
|
||||||
return new File(
|
|
||||||
[decryptedBuffer],
|
|
||||||
file.name.replace(/\.enc$/, fileExtension),
|
|
||||||
{type: "application/octet-stream"}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function getFileExtension(dbType: string) {
|
|
||||||
switch (dbType) {
|
|
||||||
case "postgresql":
|
|
||||||
return ".dump";
|
|
||||||
case "mysql":
|
|
||||||
return ".sql";
|
|
||||||
default:
|
|
||||||
return ".dump";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,89 +1,63 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
|
||||||
import {v4 as uuidv4} from "uuid";
|
|
||||||
import * as drizzleDb from "@/db";
|
|
||||||
import {db} from "@/db";
|
|
||||||
import {Backup} from "@/db/schema/07_database";
|
|
||||||
import {and, eq} from "drizzle-orm";
|
import {and, eq} from "drizzle-orm";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {db as dbClient, db} from "@/db";
|
||||||
|
import {getDatabaseOrThrow, withAgentCheck} from "./helpers";
|
||||||
|
import {Backup} from "@/db/schema/07_database";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
import {decryptedDump, getFileExtension} from "./helpers";
|
|
||||||
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
|
|
||||||
import {storeBackupFiles} from "@/features/storages/helpers";
|
|
||||||
import {eventEmitter} from "@/features/shared/event";
|
import {eventEmitter} from "@/features/shared/event";
|
||||||
|
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
|
||||||
|
import {EventKind} from "@/features/notifications/types";
|
||||||
|
|
||||||
export async function POST(
|
export type BodyPost = {
|
||||||
request: Request,
|
method: "manual" | "automatic"
|
||||||
{params}: { params: Promise<{ agentId: string }> }
|
generatedId: string
|
||||||
) {
|
}
|
||||||
|
|
||||||
|
export type BodyPatch = {
|
||||||
|
backupId: string
|
||||||
|
status: "success" | "failed"
|
||||||
|
size: number
|
||||||
|
generatedId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const POST = withAgentCheck(async (request: Request, {params, agent}: {
|
||||||
|
params: Promise<{ agentId: string }>,
|
||||||
|
agent: any
|
||||||
|
}) => {
|
||||||
try {
|
try {
|
||||||
const contentType = request.headers.get("Content-Type");
|
const body: BodyPost = await request.json();
|
||||||
|
const method = body.method
|
||||||
if (!contentType || !contentType.includes("multipart/form-data")) {
|
const database = await getDatabaseOrThrow(body.generatedId);
|
||||||
return NextResponse.json(
|
|
||||||
{error: "Unsupported or missing Content-Type"},
|
|
||||||
{status: 400}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
eventEmitter.emit('modification', {update: true});
|
|
||||||
|
|
||||||
const agentId = (await params).agentId;
|
|
||||||
const formData = await request.formData();
|
|
||||||
const aesKeyHex = formData.get("aes_key") as string;
|
|
||||||
const ivHex = formData.get("iv") as string;
|
|
||||||
const generatedId = formData.get("generatedId") as string | null;
|
|
||||||
const method = formData.get("method") as string | null;
|
|
||||||
|
|
||||||
|
|
||||||
if (!generatedId || !isUuidv4(generatedId)) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{error: "generatedId is not a valid UUID"},
|
|
||||||
{status: 400}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const agent = await db.query.agent.findFirst({
|
|
||||||
where: eq(drizzleDb.schemas.agent.id, agentId),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!agent) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{error: "Agent not found"},
|
|
||||||
{status: 404}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const database = await db.query.database.findFirst({
|
|
||||||
where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
|
|
||||||
with: {
|
|
||||||
project: true,
|
|
||||||
alertPolicies: true,
|
|
||||||
storagePolicies: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!database) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{error: "Database associated with generatedId not found"},
|
|
||||||
{status: 404}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let backup: Backup | null | undefined = null;
|
let backup: Backup | null | undefined = null;
|
||||||
|
|
||||||
if (method === "automatic") {
|
if (method === "automatic") {
|
||||||
[backup] = await db
|
|
||||||
.insert(drizzleDb.schemas.backup)
|
|
||||||
.values({
|
|
||||||
status: 'ongoing',
|
|
||||||
databaseId: database.id,
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
|
const ongoingBackup = await db.query.backup.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.backup.status, 'ongoing'),
|
||||||
|
eq(drizzleDb.schemas.backup.databaseId, database.id),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
if (!backup) {
|
if (!ongoingBackup) {
|
||||||
|
[backup] = await db
|
||||||
|
.insert(drizzleDb.schemas.backup)
|
||||||
|
.values({
|
||||||
|
status: 'ongoing',
|
||||||
|
databaseId: database.id,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
if (!backup) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: "Unable to create an automatic backup"},
|
||||||
|
{status: 500}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "Unable to create an automatic backup"},
|
{error: "A backup is already ongoing"},
|
||||||
{status: 500}
|
{status: 500}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -95,7 +69,6 @@ export async function POST(
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (!backup) {
|
if (!backup) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "Unable to find the corresponding backup"},
|
{error: "Unable to find the corresponding backup"},
|
||||||
@@ -104,66 +77,77 @@ export async function POST(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = formData.get("status") as string | null;
|
|
||||||
|
|
||||||
if (status === "success") {
|
eventEmitter.emit('modification', {update: true});
|
||||||
const file = formData.get("file") as File | null;
|
|
||||||
const extension = formData.get("extension") as string | null;
|
|
||||||
if (!aesKeyHex || !ivHex) {
|
|
||||||
return NextResponse.json({error: "Missing fields"}, {status: 400});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
if (!file) {
|
{
|
||||||
return NextResponse.json(
|
message: "Init backup success",
|
||||||
{error: "File is required for successful backup"},
|
backup: backup,
|
||||||
{status: 400}
|
},
|
||||||
);
|
{status: 200}
|
||||||
}
|
);
|
||||||
const fileSizeBytes = file.size;
|
|
||||||
// const fileExtension = '.' + (file.name.split('.').pop()?.toLowerCase() || '');
|
|
||||||
const fileExtension = extension ? extension : getFileExtension(database.dbms)
|
|
||||||
const decryptedFile = await decryptedDump(file, aesKeyHex, ivHex, fileExtension);
|
|
||||||
const uuid = uuidv4();
|
|
||||||
const fileName = `${uuid}${fileExtension}`;
|
|
||||||
const buffer = Buffer.from(await decryptedFile.arrayBuffer());
|
|
||||||
|
|
||||||
|
|
||||||
const storageResults = await storeBackupFiles(backup, database, buffer, fileName)
|
|
||||||
eventEmitter.emit('modification', {update: true});
|
|
||||||
|
|
||||||
await sendNotificationsBackupRestore(database, "success_backup");
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
message: "Backup successfully uploaded",
|
|
||||||
},
|
|
||||||
{status: 200}
|
|
||||||
);
|
|
||||||
} 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 NextResponse.json(
|
|
||||||
{
|
|
||||||
message: "Backup successfully updated with status failed",
|
|
||||||
},
|
|
||||||
{status: 200}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in POST handler:", error);
|
console.error("Error in POST for INIT backup:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "Internal server error"},
|
{error: "Internal server error"},
|
||||||
{status: 500}
|
{status: 500}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
|
||||||
|
params: Promise<{ agentId: string }>,
|
||||||
|
agent: any
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
const body: BodyPatch = await request.json();
|
||||||
|
|
||||||
|
const status = body.status
|
||||||
|
const backupId = body.backupId
|
||||||
|
const backupSize = body.size
|
||||||
|
|
||||||
|
const database = await getDatabaseOrThrow(body.generatedId);
|
||||||
|
|
||||||
|
const backup = await db.query.backup.findFirst({
|
||||||
|
where: eq(drizzleDb.schemas.backup.id, backupId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!backup) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: "No backup found"},
|
||||||
|
{status: 500}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [backupUpdated] = await dbClient
|
||||||
|
.update(drizzleDb.schemas.backup)
|
||||||
|
.set(withUpdatedAt({
|
||||||
|
status: status,
|
||||||
|
fileSize: backupSize
|
||||||
|
}))
|
||||||
|
.where(eq(drizzleDb.schemas.backup.id, backup.id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
|
||||||
|
eventEmitter.emit('modification', {update: true});
|
||||||
|
await sendNotificationsBackupRestore(database, `${status}_backup` as EventKind);
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
message: "Backup successfully updated",
|
||||||
|
backup: backupUpdated,
|
||||||
|
},
|
||||||
|
{status: 200}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
|
||||||
|
console.error("Error in PATCH backup:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: "Internal server error"},
|
||||||
|
{status: 500}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import {NextResponse} from "next/server";
|
||||||
|
import {and, eq} from "drizzle-orm";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {getDatabaseOrThrow, withAgentCheck} from "../../helpers";
|
||||||
|
import {isUuidv4} from "@/utils/verify-uuid";
|
||||||
|
import {eventEmitter} from "@/features/shared/event";
|
||||||
|
|
||||||
|
export type Body = {
|
||||||
|
generatedId: string
|
||||||
|
storageChannelId: string
|
||||||
|
backupId: string
|
||||||
|
}
|
||||||
|
export const POST = withAgentCheck(async (request: Request, {params, agent}: {
|
||||||
|
params: Promise<{ agentId: string }>,
|
||||||
|
agent: any
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
const body: Body = await request.json();
|
||||||
|
|
||||||
|
console.log("body", body);
|
||||||
|
|
||||||
|
const generatedId = body.generatedId;
|
||||||
|
const storageChannelId = body.storageChannelId;
|
||||||
|
const backupId = body.backupId;
|
||||||
|
|
||||||
|
if (!generatedId || !isUuidv4(generatedId)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: "generatedId is not a valid UUID"},
|
||||||
|
{status: 400}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const database = await getDatabaseOrThrow(generatedId);
|
||||||
|
|
||||||
|
const backup = await db.query.backup.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.backup.id, backupId),
|
||||||
|
eq(drizzleDb.schemas.backup.databaseId, database.id),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!backup) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: "Unable to find the corresponding backup"},
|
||||||
|
{status: 404}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [backupStorage] = await db
|
||||||
|
.insert(drizzleDb.schemas.backupStorage)
|
||||||
|
.values({
|
||||||
|
backupId: backup.id,
|
||||||
|
storageChannelId: storageChannelId,
|
||||||
|
status: "pending",
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
eventEmitter.emit('modification', {update: true});
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
message: "Backup storage successfully created",
|
||||||
|
backupStorage: backupStorage
|
||||||
|
},
|
||||||
|
{status: 200}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in POST for INIT backup:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: "Internal server error"},
|
||||||
|
{status: 500}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import {NextResponse} from "next/server";
|
||||||
|
import {and, eq} from "drizzle-orm";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {db as dbClient, db} from "@/db";
|
||||||
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
|
import {getDatabaseOrThrow, withAgentCheck} from "../../helpers";
|
||||||
|
import {eventEmitter} from "@/features/shared/event";
|
||||||
|
|
||||||
|
export type Body = {
|
||||||
|
generatedId: string
|
||||||
|
status: "success" | "failed"
|
||||||
|
backupStorageId: string
|
||||||
|
path: string
|
||||||
|
size: number
|
||||||
|
backupId: string
|
||||||
|
}
|
||||||
|
export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
|
||||||
|
params: Promise<{ agentId: string }>,
|
||||||
|
agent: any
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
const body: Body = await request.json();
|
||||||
|
const generatedId = body.generatedId;
|
||||||
|
const status = body.status;
|
||||||
|
const filePath = body.path;
|
||||||
|
const fileSize = body.size;
|
||||||
|
const backupStorageId = body.backupStorageId;
|
||||||
|
const backupId = body.backupId;
|
||||||
|
|
||||||
|
|
||||||
|
console.log("body", body);
|
||||||
|
|
||||||
|
const database = await getDatabaseOrThrow(generatedId);
|
||||||
|
|
||||||
|
const backup = await db.query.backup.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.backup.id, backupId),
|
||||||
|
eq(drizzleDb.schemas.backup.databaseId, database.id),
|
||||||
|
),
|
||||||
|
with: {
|
||||||
|
storages: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!backup) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: "Unable to find the corresponding backup"},
|
||||||
|
{status: 404}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [backupStorage] = await dbClient
|
||||||
|
.update(drizzleDb.schemas.backupStorage)
|
||||||
|
.set(withUpdatedAt({
|
||||||
|
status: status,
|
||||||
|
path: filePath,
|
||||||
|
size: fileSize
|
||||||
|
}))
|
||||||
|
.where(eq(drizzleDb.schemas.backupStorage.id, backupStorageId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
|
||||||
|
if (backup.storages.length > 0) {
|
||||||
|
const hasSuccessfulStorage = backup.storages.some(
|
||||||
|
(storage) => storage.status === "success"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasSuccessfulStorage && backup.status !== "success") {
|
||||||
|
await db
|
||||||
|
.update(drizzleDb.schemas.backup)
|
||||||
|
.set({
|
||||||
|
status: "success",
|
||||||
|
fileSize: fileSize,
|
||||||
|
})
|
||||||
|
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eventEmitter.emit('modification', {update: true});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
message: "Backup status successfully updated",
|
||||||
|
backupStorage: backupStorage
|
||||||
|
},
|
||||||
|
{status: 200}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in POST for INIT backup:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: "Internal server error"},
|
||||||
|
{status: 500}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
@@ -2,9 +2,9 @@ import {NextResponse} from "next/server";
|
|||||||
import {Body} from "./route";
|
import {Body} from "./route";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
import {isUuidv4} from "@/utils/verify-uuid";
|
||||||
import {Agent} from "@/db/schema/08_agent";
|
import {Agent} from "@/db/schema/08_agent";
|
||||||
import {Database} from "@/db/schema/07_database";
|
import {Database, DatabaseWith} from "@/db/schema/07_database";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db as dbClient} from "@/db";
|
import {db, db as dbClient} from "@/db";
|
||||||
import {and, eq, inArray} from "drizzle-orm";
|
import {and, eq, inArray} from "drizzle-orm";
|
||||||
import {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types";
|
import {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
@@ -14,9 +14,11 @@ import {dispatchStorage} from "@/features/storages/dispatch";
|
|||||||
export async function handleDatabases(body: Body, agent: Agent, lastContact: Date) {
|
export async function handleDatabases(body: Body, agent: Agent, lastContact: Date) {
|
||||||
const databasesResponse = [];
|
const databasesResponse = [];
|
||||||
|
|
||||||
const formatDatabase = (database: Database, backupAction: boolean, restoreAction: boolean, UrlBackup: string) => ({
|
const formatDatabase = (database: DatabaseWith, backupAction: boolean, restoreAction: boolean, UrlBackup: string | null, storages: PingDatabaseStorageChannels[], urlMeta: string | null) => ({
|
||||||
generatedId: database.agentDatabaseId,
|
generatedId: database.agentDatabaseId,
|
||||||
dbms: database.dbms,
|
dbms: database.dbms,
|
||||||
|
storages: storages,
|
||||||
|
encrypt: false,
|
||||||
data: {
|
data: {
|
||||||
backup: {
|
backup: {
|
||||||
action: backupAction,
|
action: backupAction,
|
||||||
@@ -25,6 +27,7 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
restore: {
|
restore: {
|
||||||
action: restoreAction,
|
action: restoreAction,
|
||||||
file: UrlBackup,
|
file: UrlBackup,
|
||||||
|
metaFile: urlMeta
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -32,12 +35,16 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
for (const db of body.databases) {
|
for (const db of body.databases) {
|
||||||
|
|
||||||
const existingDatabase = await dbClient.query.database.findFirst({
|
const existingDatabase = await dbClient.query.database.findFirst({
|
||||||
where: eq(drizzleDb.schemas.database.agentDatabaseId, db.generatedId)
|
where: eq(drizzleDb.schemas.database.agentDatabaseId, db.generatedId),
|
||||||
|
with: {
|
||||||
|
project: true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let backupAction: boolean = false
|
let backupAction: boolean = false
|
||||||
let restoreAction: boolean = false
|
let restoreAction: boolean = false
|
||||||
let urlBackup: string = ""
|
let urlBackup: string | null = null;
|
||||||
|
let urlMeta: string | null = null
|
||||||
|
|
||||||
if (!existingDatabase) {
|
if (!existingDatabase) {
|
||||||
if (!isUuidv4(db.generatedId)) {
|
if (!isUuidv4(db.generatedId)) {
|
||||||
@@ -63,8 +70,11 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|
||||||
if (databaseCreated) {
|
if (databaseCreated) {
|
||||||
databasesResponse.push(formatDatabase(databaseCreated, backupAction, restoreAction, urlBackup));
|
const storages = await getDatabaseStorageChannels(databaseCreated.id)
|
||||||
|
|
||||||
|
databasesResponse.push(formatDatabase(databaseCreated, backupAction, restoreAction, urlBackup, storages, null));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
@@ -78,7 +88,6 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
.where(eq(drizzleDb.schemas.database.id, existingDatabase.id))
|
.where(eq(drizzleDb.schemas.database.id, existingDatabase.id))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|
||||||
const activeBackup = await dbClient.query.backup.findFirst({
|
const activeBackup = await dbClient.query.backup.findFirst({
|
||||||
where: and(
|
where: and(
|
||||||
eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id),
|
eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id),
|
||||||
@@ -86,7 +95,6 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const restoration = await dbClient.query.restoration.findFirst({
|
const restoration = await dbClient.query.restoration.findFirst({
|
||||||
where: and(eq(drizzleDb.schemas.restoration.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.restoration.status, "waiting")),
|
where: and(eq(drizzleDb.schemas.restoration.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.restoration.status, "waiting")),
|
||||||
with: {
|
with: {
|
||||||
@@ -94,7 +102,6 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
if (activeBackup && activeBackup.status == "waiting") {
|
if (activeBackup && activeBackup.status == "waiting") {
|
||||||
backupAction = true
|
backupAction = true
|
||||||
|
|
||||||
@@ -107,13 +114,11 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
if (restoration) {
|
if (restoration) {
|
||||||
restoreAction = true
|
restoreAction = true
|
||||||
|
|
||||||
|
|
||||||
if (!restoration.backupStorage || restoration.backupStorage.status != "success" || !restoration.backupStorage.path) {
|
if (!restoration.backupStorage || restoration.backupStorage.status != "success" || !restoration.backupStorage.path) {
|
||||||
restoreAction = false
|
restoreAction = false
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const input: StorageInput = {
|
const input: StorageInput = {
|
||||||
action: "get",
|
action: "get",
|
||||||
data: {
|
data: {
|
||||||
@@ -126,12 +131,26 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const inputMeta: StorageInput = {
|
||||||
|
action: "get",
|
||||||
|
data: {
|
||||||
|
path: `${restoration.backupStorage.path}.meta`,
|
||||||
|
signedUrl: true,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
storageId: restoration.backupStorage.storageChannelId,
|
||||||
|
fileKind: "backups"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await dispatchStorage(input, undefined, restoration.backupStorage.storageChannelId);
|
const result = await dispatchStorage(input, undefined, restoration.backupStorage.storageChannelId);
|
||||||
|
const resultMeta = await dispatchStorage(inputMeta, undefined, restoration.backupStorage.storageChannelId);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
urlBackup = result.url ?? "";
|
urlBackup = result.url ?? null;
|
||||||
|
urlMeta = resultMeta.url ?? null
|
||||||
} else {
|
} else {
|
||||||
await dbClient
|
await dbClient
|
||||||
.update(drizzleDb.schemas.restoration)
|
.update(drizzleDb.schemas.restoration)
|
||||||
@@ -151,53 +170,78 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// const fileName = backupToRestore?.file
|
|
||||||
//
|
|
||||||
// let data: SafeActionResult<string, ZodString, readonly [], {
|
|
||||||
// _errors?: string[] | undefined;
|
|
||||||
// }, readonly [], ServerActionResult<string>, object> | undefined
|
|
||||||
//
|
|
||||||
// try {
|
|
||||||
//
|
|
||||||
// if (settings.storage == "local") {
|
|
||||||
// data = await getFileUrlPresignedLocal({fileName: fileName!})
|
|
||||||
// } else if (settings.storage == "s3") {
|
|
||||||
//
|
|
||||||
// data = await getFileUrlPreSignedS3Action(`backups/${backupToRestore?.database.project?.slug}/${fileName}`);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (data?.data?.success) {
|
|
||||||
// urlBackup = data.data.value ?? "";
|
|
||||||
// } else {
|
|
||||||
// await dbClient
|
|
||||||
// .update(drizzleDb.schemas.restoration)
|
|
||||||
// .set({status: "failed"})
|
|
||||||
// .where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
|
||||||
//
|
|
||||||
// // @ts-ignore
|
|
||||||
// const errorMessage = data?.data?.actionError?.message || "Failed to get presigned URL";
|
|
||||||
// console.error("Restoration failed: ", errorMessage);
|
|
||||||
//
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
// } catch (err) {
|
|
||||||
// console.error("Restoration crashed unexpectedly:", err);
|
|
||||||
// await dbClient
|
|
||||||
// .update(drizzleDb.schemas.restoration)
|
|
||||||
// .set({status: "failed"})
|
|
||||||
// .where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
|
||||||
//
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
await dbClient
|
await dbClient
|
||||||
.update(drizzleDb.schemas.restoration)
|
.update(drizzleDb.schemas.restoration)
|
||||||
.set({status: "ongoing"})
|
.set({status: "ongoing"})
|
||||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
||||||
}
|
}
|
||||||
databasesResponse.push(formatDatabase(databaseUpdated, backupAction, restoreAction, urlBackup));
|
const storages = await getDatabaseStorageChannels(databaseUpdated.id)
|
||||||
|
databasesResponse.push(formatDatabase(databaseUpdated, backupAction, restoreAction, urlBackup, storages, urlMeta));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return databasesResponse;
|
return databasesResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type PingDatabaseStorageChannels = {
|
||||||
|
id: string;
|
||||||
|
config: any
|
||||||
|
provider: string
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDatabaseStorageChannels(databaseId: string): Promise<PingDatabaseStorageChannels[]> {
|
||||||
|
|
||||||
|
const database = await db.query.database.findFirst({
|
||||||
|
where: eq(drizzleDb.schemas.database.id, databaseId),
|
||||||
|
with: {
|
||||||
|
project: true,
|
||||||
|
retentionPolicy: true,
|
||||||
|
alertPolicies: true,
|
||||||
|
storagePolicies: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!database) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = await db.query.setting.findFirst({
|
||||||
|
where: eq(drizzleDb.schemas.setting.name, "system"),
|
||||||
|
with: {storageChannel: true},
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultStorageChannel: PingDatabaseStorageChannels[] = settings?.storageChannel
|
||||||
|
? [{
|
||||||
|
id: settings.storageChannel.id,
|
||||||
|
provider: settings.storageChannel.provider,
|
||||||
|
config: settings.storageChannel.config,
|
||||||
|
}]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
|
||||||
|
const enabledDatabaseStorageChannels = await Promise.all(
|
||||||
|
(database.storagePolicies ?? [])
|
||||||
|
.filter(p => p.enabled)
|
||||||
|
.map(async policy => {
|
||||||
|
const storageChannel = await db.query.storageChannel.findFirst({
|
||||||
|
where: eq(drizzleDb.schemas.storageChannel.id, policy.storageChannelId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!storageChannel) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: storageChannel.id,
|
||||||
|
config: storageChannel.config,
|
||||||
|
provider: storageChannel.provider,
|
||||||
|
} as PingDatabaseStorageChannels;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredChannels: PingDatabaseStorageChannels[] = enabledDatabaseStorageChannels.filter(
|
||||||
|
(c): c is PingDatabaseStorageChannels => c !== null
|
||||||
|
);
|
||||||
|
|
||||||
|
return filteredChannels.length > 0 ? filteredChannels : defaultStorageChannel;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,12 +26,14 @@ export async function POST(
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const agentId = (await params).agentId
|
const agentId = (await params).agentId
|
||||||
|
console.log(agentId)
|
||||||
const body: Body = await request.json();
|
const body: Body = await request.json();
|
||||||
const lastContact = new Date();
|
const lastContact = new Date();
|
||||||
let message: string
|
let message: string
|
||||||
|
|
||||||
if (!isUuidv4(agentId)) {
|
if (!isUuidv4(agentId)) {
|
||||||
message = "agentId is not a valid uuid"
|
message = "agentId is not a valid uuid"
|
||||||
|
console.error(message)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "agentId is not a valid uuid"},
|
{error: "agentId is not a valid uuid"},
|
||||||
{status: 500}
|
{status: 500}
|
||||||
@@ -66,6 +68,7 @@ export async function POST(
|
|||||||
databases: databasesResponse
|
databases: databasesResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return Response.json(response)
|
return Response.json(response)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in POST handler:', error);
|
console.error('Error in POST handler:', error);
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import {NextResponse} from "next/server";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const event = body.Event
|
||||||
|
const headers = event.HTTPRequest.Header
|
||||||
|
const uploadLength = headers["X-File-Size"]?.[0];
|
||||||
|
const uploadOffset = headers["Upload-Offset"]?.[0];
|
||||||
|
const status = headers["X-Status"]?.[0];
|
||||||
|
|
||||||
|
console.log(`Upload ID : ${event.Upload.ID} (${uploadOffset}/${uploadLength})`);
|
||||||
|
|
||||||
|
if (status === "success") {
|
||||||
|
if (
|
||||||
|
body.Type === "post-receive" &&
|
||||||
|
event.Upload.SizeIsDeferred === false &&
|
||||||
|
event.Upload.Offset === event.Upload.Size
|
||||||
|
) {
|
||||||
|
const id = event.Upload.ID;
|
||||||
|
const fileName = headers["X-File-Name"]?.[0];
|
||||||
|
const filePath = headers["X-File-Path"]?.[0];
|
||||||
|
|
||||||
|
if (!filePath) {
|
||||||
|
return NextResponse.json({error: "Missing X-File-Path"}, {status: 500});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const uploadDir = path.join(process.cwd(), "/private/uploads/");
|
||||||
|
|
||||||
|
const oldFilePath = path.join(uploadDir, "tmp", id);
|
||||||
|
const newFilePath = path.join(uploadDir, filePath);
|
||||||
|
|
||||||
|
fs.mkdirSync(path.dirname(newFilePath), {recursive: true});
|
||||||
|
|
||||||
|
let retries = 10;
|
||||||
|
while (!fs.existsSync(oldFilePath)) {
|
||||||
|
if (retries-- === 0) {
|
||||||
|
return NextResponse.json({error: `Upload file not found: ${oldFilePath}`}, {status: 500});
|
||||||
|
}
|
||||||
|
await new Promise(r => setTimeout(r, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.renameSync(oldFilePath, newFilePath);
|
||||||
|
|
||||||
|
const infoFilePath = `${oldFilePath}.info`;
|
||||||
|
if (fs.existsSync(infoFilePath)) {
|
||||||
|
fs.unlinkSync(infoFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadataHeaderB64 = headers["Upload-Metadata"]?.[0];
|
||||||
|
|
||||||
|
if (metadataHeaderB64) {
|
||||||
|
const metadataHeader = Buffer.from(metadataHeaderB64, "base64").toString("utf-8");
|
||||||
|
if (metadataHeader) {
|
||||||
|
const tomlContent = metadataHeader
|
||||||
|
.split(",")
|
||||||
|
.map((pair) => {
|
||||||
|
const [key, value] = pair.split(" ");
|
||||||
|
const escapedValue = value.replace(/"/g, '\\"');
|
||||||
|
return `${key} = "${escapedValue}"`;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
const metaFilePath = `${newFilePath}.meta`;
|
||||||
|
fs.writeFileSync(metaFilePath, tomlContent, "utf-8");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NextResponse.json({});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Hook error:", error);
|
||||||
|
return NextResponse.json({error: "Internal server error"}, {status: 500});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
dockerfile: docker/dockerfile/Dockerfile
|
dockerfile: docker/dockerfile/Dockerfile
|
||||||
target: prod
|
target: prod
|
||||||
# image: solucetechnologies/portabase:latest
|
image: portabase/portabase:1.2.5-rc.9
|
||||||
ports:
|
ports:
|
||||||
- '8887:80'
|
- '8887:80'
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -17,5 +17,19 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
|
tusd:
|
||||||
|
image: tusproject/tusd:v2.8.0
|
||||||
|
ports:
|
||||||
|
- "1080:8080"
|
||||||
|
command: >
|
||||||
|
-upload-dir /data/uploads/tmp
|
||||||
|
-hooks-http http://localhost:8887/api/tus/hooks
|
||||||
|
-max-size 21474836480
|
||||||
|
-base-path /tus/files/
|
||||||
|
extra_hosts:
|
||||||
|
- "localhost:host-gateway"
|
||||||
|
volumes:
|
||||||
|
- ./private/uploads/tmp:/data/uploads/tmp
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
|
|||||||
@@ -1,53 +1,65 @@
|
|||||||
FROM node:22-alpine AS base
|
#FROM node:22-bullseye AS base
|
||||||
|
FROM --platform=$BUILDPLATFORM node:22-bullseye AS base
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl \
|
||||||
|
ca-certificates \
|
||||||
|
bash \
|
||||||
|
tzdata \
|
||||||
|
libc6 \
|
||||||
|
build-essential \
|
||||||
|
g++ \
|
||||||
|
make \
|
||||||
|
autoconf \
|
||||||
|
automake \
|
||||||
|
libtool \
|
||||||
|
git \
|
||||||
|
nginx \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
||||||
|
FROM --platform=$BUILDPLATFORM golang:1.23-bookworm AS tusd-base
|
||||||
|
|
||||||
|
ARG TUSD_VERSION=2.8.0
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
RUN git clone https://github.com/tus/tusd.git . \
|
||||||
|
&& git checkout v${TUSD_VERSION}
|
||||||
|
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} \
|
||||||
|
go build -ldflags="-s -w" -o /tusddist/tusd ./cmd/tusd
|
||||||
|
|
||||||
|
FROM scratch AS tusd-dist
|
||||||
|
COPY --from=tusd-base /tusddist/tusd /tusd
|
||||||
|
|
||||||
RUN apk add --update --no-cache \
|
|
||||||
libc6-compat \
|
|
||||||
openssl \
|
|
||||||
tzdata
|
|
||||||
|
|
||||||
FROM base AS build-env
|
FROM base AS build-env
|
||||||
|
|
||||||
RUN apk add --update --no-cache \
|
|
||||||
build-base \
|
|
||||||
g++ \
|
|
||||||
make \
|
|
||||||
libtool \
|
|
||||||
autoconf \
|
|
||||||
automake
|
|
||||||
|
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
FROM build-env AS deps
|
FROM build-env AS deps
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
|
|
||||||
RUN pnpm i --frozen-lockfile
|
RUN pnpm i --frozen-lockfile
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
FROM build-env AS dev
|
FROM build-env AS dev
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY --from=tusd-base /usr/local/bin/tusd /usr/local/bin/tusd
|
||||||
|
WORKDIR /app
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
RUN chmod +x /app/docker/entrypoints/app-dev-entrypoint.sh
|
RUN chmod +x /app/docker/entrypoints/app-dev-entrypoint.sh
|
||||||
|
|
||||||
ENTRYPOINT ["sh","/app/docker/entrypoints/app-dev-entrypoint.sh"]
|
ENTRYPOINT ["sh","/app/docker/entrypoints/app-dev-entrypoint.sh"]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
FROM build-env AS builder
|
FROM build-env AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -55,57 +67,41 @@ COPY --from=deps /app/node_modules ./node_modules
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
RUN pnpm run build
|
RUN pnpm run build
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
FROM base AS prod
|
FROM base AS prod
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
ENV PORT=80
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
|
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
RUN adduser --system --uid 1001 nextjs
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
COPY --from=builder /app/public ./public
|
COPY --from=builder /app/public ./public
|
||||||
|
|
||||||
RUN mkdir .next
|
|
||||||
RUN chown nextjs:nodejs .next
|
|
||||||
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/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=1001:1001 /app/.next/standalone ./
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
COPY --from=builder --chown=1001:1001 /app/.next/static ./.next/static
|
||||||
COPY --chown=nextjs:nodejs src/db ./src/db
|
COPY --chown=1001:1001 src/db ./src/db
|
||||||
|
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
|
||||||
USER root
|
COPY --from=tusd-dist /tusd /usr/local/bin/tusd
|
||||||
|
RUN chmod +x /usr/local/bin/tusd
|
||||||
|
|
||||||
|
COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
|
||||||
|
RUN mkdir -p .next /app/private/uploads \
|
||||||
|
&& chown -R nextjs:nodejs .next /app/private /app/public
|
||||||
|
|
||||||
|
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
|
||||||
RUN chmod +x /app/app-prod-entrypoint.sh
|
RUN chmod +x /app/app-prod-entrypoint.sh
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
#USER nextjs
|
||||||
ENV PORT=80
|
|
||||||
|
|
||||||
ENV HOSTNAME="0.0.0.0"
|
|
||||||
|
|
||||||
USER nextjs
|
|
||||||
|
|
||||||
ENTRYPOINT ["sh","/app/app-prod-entrypoint.sh"]
|
ENTRYPOINT ["sh","/app/app-prod-entrypoint.sh"]
|
||||||
|
|||||||
@@ -7,8 +7,14 @@ else
|
|||||||
echo "[WARN] No TZ provided, using default container timezone"
|
echo "[WARN] No TZ provided, using default container timezone"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
pnpm run start
|
mkdir -p /app/private/uploads/tmp
|
||||||
#node server.js
|
echo "▶ Starting tusd server..."
|
||||||
|
tusd --base-path /tus/files/ --upload-dir /app/private/uploads/tmp --hooks-http http://127.0.0.1:3000/api/tus/hooks --port 1080 --max-size 21474836480 &
|
||||||
|
|
||||||
|
echo "▶ Starting Next.js server..."
|
||||||
|
PORT=3000 node server.js &
|
||||||
|
|
||||||
|
echo "▶ Starting nginx..."
|
||||||
|
exec nginx -g "daemon off;"
|
||||||
|
|
||||||
exec "$@"
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
events {}
|
||||||
|
|
||||||
|
http {
|
||||||
|
client_max_body_size 20G;
|
||||||
|
ignore_invalid_headers off;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
location /tus/ {
|
||||||
|
proxy_pass http://127.0.0.1:1080/tus/;
|
||||||
|
|
||||||
|
proxy_pass_request_headers on;
|
||||||
|
|
||||||
|
proxy_request_buffering off;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import type {NextConfig} from "next";
|
import type {NextConfig} from "next";
|
||||||
import {PORTABASE_DEFAULT_SETTINGS} from "./portabase.config";
|
import {PORTABASE_DEFAULT_SETTINGS} from "./portabase.config";
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV === "development";
|
||||||
|
|
||||||
|
|
||||||
function buildCSPHeader(): string {
|
function buildCSPHeader(): string {
|
||||||
const {CSP} = PORTABASE_DEFAULT_SETTINGS.SECURITY;
|
const {CSP} = PORTABASE_DEFAULT_SETTINGS.SECURITY;
|
||||||
@@ -47,6 +49,17 @@ const nextConfig: NextConfig = {
|
|||||||
},
|
},
|
||||||
proxyClientMaxBodySize: '10gb',
|
proxyClientMaxBodySize: '10gb',
|
||||||
},
|
},
|
||||||
|
async rewrites() {
|
||||||
|
if (!isDev) return [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: "/tus/:path*",
|
||||||
|
destination: "http://localhost:1080/tus/:path*",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
async headers() {
|
async headers() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
+4
-3
@@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "portabase",
|
"name": "portabase",
|
||||||
"version": "1.2.5-rc.7",
|
"version": "1.2.5-rc.12",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "NODE_ENV=development tsx watch server/server.ts",
|
"dev": "next dev --turbopack -p 8887",
|
||||||
"build": "next build --experimental-build-mode compile",
|
"build": "next build --experimental-build-mode compile",
|
||||||
"start": "NODE_ENV=production tsx server/server.ts",
|
"start": "next start",
|
||||||
|
|
||||||
|
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"email": "email dev --dir ./src/components/emails",
|
"email": "email dev --dir ./src/components/emails",
|
||||||
|
|||||||
@@ -53,11 +53,13 @@ function checkRouteExists(pathname: string) {
|
|||||||
const routePatterns = [
|
const routePatterns = [
|
||||||
/^\/api\/agent\/[^/]+\/status\/?$/,
|
/^\/api\/agent\/[^/]+\/status\/?$/,
|
||||||
/^\/api\/agent\/[^/]+\/backup\/?$/,
|
/^\/api\/agent\/[^/]+\/backup\/?$/,
|
||||||
|
/^\/api\/agent\/[^/]+\/backup\/upload\/init\/?$/,
|
||||||
|
/^\/api\/agent\/[^/]+\/backup\/upload\/status\/?$/,
|
||||||
/^\/api\/agent\/[^/]+\/restore\/?$/,
|
/^\/api\/agent\/[^/]+\/restore\/?$/,
|
||||||
/^\/api\/files\/images\/[^/]+\/?$/,
|
/^\/api\/files\/images\/[^/]+\/?$/,
|
||||||
/^\/api\/files\/backups\/?$/,
|
/^\/api\/files\/backups\/?$/,
|
||||||
|
/^\/api\/tus\/hooks\/?$/,
|
||||||
/^\/api\/events\/?$/,
|
/^\/api\/events\/?$/,
|
||||||
/^\/api\/init\/?$/,
|
|
||||||
/^\/api\/config\/?$/,
|
/^\/api\/config\/?$/,
|
||||||
/^\/api\/google\/drive\/callback\/?$/,
|
/^\/api\/google\/drive\/callback\/?$/,
|
||||||
];
|
];
|
||||||
|
|||||||
+1
-1
@@ -82,7 +82,7 @@ export const StorageS3Form = ({form}: StorageS3FormProps) => {
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.useSSL"
|
name="config.ssl"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Use SSL</FormLabel>
|
<FormLabel>Use SSL</FormLabel>
|
||||||
|
|||||||
@@ -4,13 +4,11 @@ import {ServerActionResult} from "@/types/action-type";
|
|||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {Backup} from "@/db/schema/07_database";
|
import {Backup} from "@/db/schema/07_database";
|
||||||
import {getFileExtension} from "../../../../../../app/api/agent/[agentId]/backup/helpers";
|
|
||||||
import {v4 as uuidv4} from "uuid";
|
import {v4 as uuidv4} from "uuid";
|
||||||
import {eq} from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
import {uploadLocalPrivate, uploadS3Private} from "@/features/upload/private/upload.action";
|
|
||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
import {env} from "@/env.mjs";
|
|
||||||
import {storeBackupFiles} from "@/features/storages/helpers";
|
import {storeBackupFiles} from "@/features/storages/helpers";
|
||||||
|
import {getFileExtension} from "@/features/api/upload/helpers/file";
|
||||||
|
|
||||||
|
|
||||||
export const uploadBackupAction = userAction
|
export const uploadBackupAction = userAction
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import {boolean, pgTable, uuid} from "drizzle-orm/pg-core";
|
import {boolean, pgTable, uuid} from "drizzle-orm/pg-core";
|
||||||
import {timestamps} from "@/db/schema/00_common";
|
import {timestamps} from "@/db/schema/00_common";
|
||||||
import {relations} from "drizzle-orm";
|
import {relations} from "drizzle-orm";
|
||||||
import {database} from "@/db/schema/07_database";
|
import {Backup, Database, database, Restoration, RetentionPolicy} from "@/db/schema/07_database";
|
||||||
import {createSelectSchema} from "drizzle-zod";
|
import {createSelectSchema} from "drizzle-zod";
|
||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
import {storageChannel} from "@/db/schema/12_storage-channel";
|
import {StorageChannel, storageChannel} from "@/db/schema/12_storage-channel";
|
||||||
|
import {Agent} from "@/db/schema/08_agent";
|
||||||
|
import {Project} from "@/db/schema/06_project";
|
||||||
|
import {AlertPolicy} from "@/db/schema/10_alert-policy";
|
||||||
|
|
||||||
export const storagePolicy = pgTable('storage_policy', {
|
export const storagePolicy = pgTable('storage_policy', {
|
||||||
id: uuid('id').defaultRandom().primaryKey(),
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
@@ -31,3 +34,8 @@ export const storagePolicyRelations = relations(storagePolicy, ({one}) => ({
|
|||||||
|
|
||||||
export const storagePolicySchema = createSelectSchema(storagePolicy);
|
export const storagePolicySchema = createSelectSchema(storagePolicy);
|
||||||
export type StoragePolicy = z.infer<typeof storagePolicySchema>;
|
export type StoragePolicy = z.infer<typeof storagePolicySchema>;
|
||||||
|
|
||||||
|
|
||||||
|
export type StoragePolicyWith = StoragePolicy & {
|
||||||
|
storageChannel: StorageChannel;
|
||||||
|
};
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export default async function uploadTempFileToProviders(
|
|||||||
? await dispatchStorage(input, policy.id)
|
? await dispatchStorage(input, policy.id)
|
||||||
: await dispatchStorage(input, undefined, policy.storageChannelId);
|
: await dispatchStorage(input, undefined, policy.storageChannelId);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
console.error(err);
|
||||||
result = {success: false, provider: null, error: err.message};
|
result = {success: false, provider: null, error: err.message};
|
||||||
}
|
}
|
||||||
await db.update(drizzleDb.schemas.backupStorage)
|
await db.update(drizzleDb.schemas.backupStorage)
|
||||||
|
|||||||
+173
-101
@@ -13,117 +13,189 @@ import {getFileExtension, saveStreamToTempFile} from "@/features/api/upload/help
|
|||||||
|
|
||||||
const router: Router = express.Router();
|
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,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
router.post("/:agentId", async (req: Request, res: Response) => {
|
router.post("/:agentId", async (req: Request, res: Response) => {
|
||||||
try {
|
const agentId = req.params.agentId as string | undefined;
|
||||||
const agentId = req.params.agentId as string | undefined;
|
const generatedId = req.headers["x-generated-id"] as string | undefined;
|
||||||
const generatedId = req.headers["x-generated-id"] as string | undefined;
|
const status = req.headers["x-status"] as string | undefined;
|
||||||
const status = req.headers["x-status"] as string | undefined;
|
const encryptedAesKeyHex = req.headers["x-aes-key"] as string | undefined;
|
||||||
const encryptedAesKeyHex = req.headers["x-aes-key"] as string | undefined;
|
const ivHex = req.headers["x-iv"] as string | undefined;
|
||||||
const ivHex = req.headers["x-iv"] as string | undefined;
|
const method = (req.headers["x-method"] as string) ?? "manual";
|
||||||
const method = (req.headers["x-method"] as string) ?? "manual";
|
const extension = req.headers["x-extension"] as string | undefined;
|
||||||
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"});
|
||||||
|
}
|
||||||
|
|
||||||
if (!generatedId || !encryptedAesKeyHex || !ivHex || !agentId || !status) {
|
const agent = await db.query.agent.findFirst({ where: eq(drizzleDb.schemas.agent.id, agentId) });
|
||||||
return res.status(400).json({error: "Missing required headers/params"});
|
if (!agent) return res.status(404).json({error: "Agent not found"});
|
||||||
}
|
|
||||||
|
|
||||||
if (!isUuidv4(generatedId)) {
|
const database = await db.query.database.findFirst({
|
||||||
return res.status(400).json({error: "generatedId is not a valid UUID"});
|
where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
|
||||||
}
|
with: { project: true, storagePolicies: true },
|
||||||
|
});
|
||||||
|
if (!database) return res.status(404).json({error: "Database not found"});
|
||||||
|
|
||||||
const agent = await db.query.agent.findFirst({
|
let backup: Backup | null = null;
|
||||||
where: eq(drizzleDb.schemas.agent.id, agentId),
|
if (method === "automatic") {
|
||||||
});
|
[backup] = await db.insert(drizzleDb.schemas.backup).values({ status: "ongoing", databaseId: database.id }).returning();
|
||||||
|
} else {
|
||||||
if (!agent) {
|
backup = await db.query.backup.findFirst({
|
||||||
return res.status(404).json({error: "Agent not found"});
|
where: and(
|
||||||
}
|
eq(drizzleDb.schemas.backup.databaseId, database.id),
|
||||||
|
eq(drizzleDb.schemas.backup.status, "ongoing")
|
||||||
|
),
|
||||||
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,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (!backup) return res.status(404).json({error: "Backup not found"});
|
||||||
|
|
||||||
|
res.status(202).json({success: true, message: "Backup received, processing in background"});
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
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) throw new Error("Unable to save tmp backup file");
|
||||||
|
|
||||||
|
await uploadTempFileToProviders(backup!, database, tmpPath, fileName);
|
||||||
|
} 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");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Background backup processing failed:", err);
|
||||||
|
try {
|
||||||
|
await db.update(drizzleDb.schemas.backup)
|
||||||
|
.set(withUpdatedAt({status: 'failed'}))
|
||||||
|
.where(eq(drizzleDb.schemas.backup.id, backup!.id));
|
||||||
|
} catch (_) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ type S3Config = {
|
|||||||
secretKey: string;
|
secretKey: string;
|
||||||
bucketName: string;
|
bucketName: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
useSSL?: boolean;
|
ssl?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getS3Client(config: S3Config) {
|
async function getS3Client(config: S3Config) {
|
||||||
@@ -17,7 +17,7 @@ async function getS3Client(config: S3Config) {
|
|||||||
accessKey: config.accessKey,
|
accessKey: config.accessKey,
|
||||||
secretKey: config.secretKey,
|
secretKey: config.secretKey,
|
||||||
port: config.port ?? 443,
|
port: config.port ?? 443,
|
||||||
useSSL: config.useSSL ?? true,
|
useSSL: config.ssl ?? true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user