mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
feat: Working on backend storage providers.
This commit is contained in:
@@ -1,129 +1,51 @@
|
||||
"use server";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {dispatchViaProvider} from "./providers";
|
||||
import type {EventPayload, DispatchResult, EventKind} from "./types";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {db} from "@/db";
|
||||
import {notificationLog} from "@/db/schema/11_notification-log";
|
||||
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
||||
import {Json} from "drizzle-zod";
|
||||
|
||||
import {eq} from 'drizzle-orm';
|
||||
import * as drizzleDb from '@/db';
|
||||
import {db} from '@/db';
|
||||
import type {StorageInput, StorageProviderKind, StorageResult,} from './types';
|
||||
import {dispatchViaProvider} from "@/features/storages/providers";
|
||||
|
||||
export async function dispatchStorage(
|
||||
payload: EventPayload,
|
||||
input: StorageInput,
|
||||
policyId?: string,
|
||||
channelId?: string,
|
||||
organizationId?: string
|
||||
): Promise<DispatchResult> {
|
||||
): Promise<StorageResult> {
|
||||
try {
|
||||
let channel: NotificationChannel | null = null;
|
||||
if (!channelId) {
|
||||
|
||||
if (policyId) {
|
||||
const policyDb = await db.query.alertPolicy.findFirst({
|
||||
where: eq(drizzleDb.schemas.alertPolicy.id, policyId),
|
||||
with: {
|
||||
notificationChannel: true
|
||||
},
|
||||
});
|
||||
|
||||
if (!policyDb || !policyDb.notificationChannel) {
|
||||
return {
|
||||
success: false,
|
||||
channelId: "",
|
||||
provider: null,
|
||||
error: "Policy or associated channel not found",
|
||||
};
|
||||
}
|
||||
|
||||
if (!policyDb.enabled || !policyDb.notificationChannel.enabled) {
|
||||
return {
|
||||
success: false,
|
||||
channelId: policyDb.notificationChannel.id,
|
||||
provider: policyDb.notificationChannel.provider as any,
|
||||
error: "Policy or channel is disabled",
|
||||
};
|
||||
}
|
||||
|
||||
channel = {
|
||||
...policyDb.notificationChannel,
|
||||
config: policyDb.notificationChannel.config as Json,
|
||||
};
|
||||
}
|
||||
|
||||
if (channelId) {
|
||||
const fetchedChannel = await db.query.notificationChannel.findFirst({
|
||||
where: eq(drizzleDb.schemas.notificationChannel.id, channelId),
|
||||
});
|
||||
|
||||
if (!fetchedChannel) {
|
||||
return {
|
||||
success: false,
|
||||
channelId: channelId,
|
||||
provider: null,
|
||||
error: "Channel not found",
|
||||
};
|
||||
}
|
||||
|
||||
channel = {
|
||||
...fetchedChannel,
|
||||
config: fetchedChannel.config as Json,
|
||||
};
|
||||
}
|
||||
|
||||
if (!channel) {
|
||||
return {
|
||||
success: false,
|
||||
channelId: channelId || "",
|
||||
provider: null,
|
||||
error: "No valid channel to dispatch notification",
|
||||
error: 'No storage channel provided',
|
||||
};
|
||||
}
|
||||
|
||||
const channel = await db.query.storageChannel.findFirst({
|
||||
where: eq(drizzleDb.schemas.storageChannel.id, channelId),
|
||||
});
|
||||
|
||||
if (!channel.enabled) {
|
||||
if (!channel || !channel.enabled) {
|
||||
return {
|
||||
success: false,
|
||||
channelId: channelId || "",
|
||||
provider: null,
|
||||
error: "Channel not active",
|
||||
provider: channel?.provider as StorageProviderKind,
|
||||
error: 'Storage channel not found or disabled',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await dispatchViaProvider(
|
||||
channel.provider,
|
||||
return await dispatchViaProvider(
|
||||
channel.provider as StorageProviderKind,
|
||||
channel.config,
|
||||
{...payload, timestamp: payload.timestamp || new Date()},
|
||||
channel.id
|
||||
input
|
||||
);
|
||||
|
||||
const [log] = await db
|
||||
.insert(notificationLog)
|
||||
.values({
|
||||
channelId: channel.id,
|
||||
policyId: policyId || null,
|
||||
organizationId: organizationId || null,
|
||||
|
||||
provider: channel.provider,
|
||||
providerName: channel.name,
|
||||
event: payload.event as EventKind,
|
||||
|
||||
title: payload.title,
|
||||
message: payload.message,
|
||||
level: payload.level,
|
||||
payload: payload.data || null,
|
||||
success: result.success,
|
||||
error: result.success ? null : result.error,
|
||||
providerResponse: result.response || null,
|
||||
})
|
||||
.returning({id: notificationLog.id});
|
||||
|
||||
return {...result, channelId: channel.id};
|
||||
|
||||
} catch (err: any) {
|
||||
return {
|
||||
success: false,
|
||||
channelId: channelId || "",
|
||||
provider: null,
|
||||
error: err?.message || "Unexpected error during dispatch",
|
||||
error: err.message || 'Unexpected storage dispatch error',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import {DatabaseWith} from "@/db/schema/07_database";
|
||||
import {StorageChannel} from "@/db/schema/12_storage-channel";
|
||||
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
|
||||
|
||||
|
||||
export async function storeFileBackup(database: DatabaseWith, file: Buffer) {
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type {
|
||||
StorageProviderKind,
|
||||
StorageInput,
|
||||
StorageResult,
|
||||
} from '../types';
|
||||
|
||||
import {uploadLocal, getLocal, deleteLocal} from './local';
|
||||
|
||||
type ProviderHandler = {
|
||||
upload: (config: any, input: StorageInput & { action: 'upload' }) => Promise<StorageResult>;
|
||||
get: (config: any, input: StorageInput & { action: 'get' }) => Promise<StorageResult>;
|
||||
delete: (config: any, input: StorageInput & { action: 'delete' }) => Promise<StorageResult>;
|
||||
};
|
||||
|
||||
const handlers: Record<StorageProviderKind, ProviderHandler> = {
|
||||
local: {
|
||||
upload: uploadLocal,
|
||||
get: getLocal,
|
||||
delete: deleteLocal,
|
||||
},
|
||||
// s3: {
|
||||
// upload: uploadS3,
|
||||
// get: getS3,
|
||||
// delete: deleteS3,
|
||||
// },
|
||||
// gcs: null as any,
|
||||
// azure: null as any,
|
||||
};
|
||||
|
||||
export async function dispatchViaProvider(
|
||||
kind: StorageProviderKind,
|
||||
config: any,
|
||||
input: StorageInput
|
||||
): Promise<StorageResult> {
|
||||
const provider = handlers[kind];
|
||||
|
||||
if (!provider) {
|
||||
return {
|
||||
success: false,
|
||||
provider: kind,
|
||||
error: `Unsupported storage provider: ${kind}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return await provider[input.action](config, input as any);
|
||||
} catch (err: any) {
|
||||
return {
|
||||
success: false,
|
||||
provider: kind,
|
||||
error: err.message || 'Storage provider error',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
"use server"
|
||||
import {mkdir, writeFile, unlink, readFile} from 'fs/promises';
|
||||
import path from 'path';
|
||||
import {StorageDeleteInput, StorageGetInput, StorageResult, StorageUploadInput} from '../types';
|
||||
import fs from "node:fs";
|
||||
import {getServerUrl} from "@/utils/get-server-url";
|
||||
|
||||
const BASE_DIR = "/private/uploads/files/";
|
||||
|
||||
export async function uploadLocal(
|
||||
config: { baseDir?: string },
|
||||
input: { data: StorageUploadInput }
|
||||
): Promise<StorageResult> {
|
||||
const base = config.baseDir || BASE_DIR;
|
||||
const fullPath = path.join(process.cwd(), base, input.data.path);
|
||||
|
||||
await mkdir(fullPath, {recursive: true});
|
||||
await writeFile(fullPath, input.data.file);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
provider: 'local',
|
||||
url: path.join(fullPath),
|
||||
};
|
||||
}
|
||||
|
||||
export async function getLocal(
|
||||
config: { baseDir?: string },
|
||||
input: { data: StorageGetInput }
|
||||
): Promise<StorageResult> {
|
||||
const base = config.baseDir || BASE_DIR;
|
||||
const filePath = path.join(base, input.data.path)
|
||||
const fileName = path.basename(input.data.path);
|
||||
const file = await readFile(filePath);
|
||||
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error("File not found at:", filePath);
|
||||
return({
|
||||
success: false,
|
||||
provider: 'local',
|
||||
});
|
||||
}
|
||||
const crypto = require("crypto");
|
||||
const baseUrl = getServerUrl();
|
||||
|
||||
const expiresAt = Date.now() + 60 * 1000;
|
||||
const token = crypto.createHash("sha256").update(`${fileName}${expiresAt}`).digest("hex");
|
||||
|
||||
return {
|
||||
success: true,
|
||||
provider: 'local',
|
||||
file: file,
|
||||
url: `${baseUrl}/api/files/${fileName}?token=${token}&expires=${expiresAt}`,
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteLocal(
|
||||
config: { baseDir?: string },
|
||||
input: { data: StorageDeleteInput }
|
||||
): Promise<StorageResult> {
|
||||
const base = config.baseDir || BASE_DIR;
|
||||
const fullPath = path.join(process.cwd(), base, input.data.path);
|
||||
await unlink(fullPath);
|
||||
return {
|
||||
success: true,
|
||||
provider: 'local',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
export type StorageProviderKind =
|
||||
| 'local'
|
||||
// | 's3'
|
||||
;
|
||||
|
||||
export type StorageAction =
|
||||
| 'upload'
|
||||
| 'get'
|
||||
| 'delete';
|
||||
|
||||
export interface StorageUploadInput {
|
||||
path: string;
|
||||
file: Buffer | Uint8Array;
|
||||
contentType?: string;
|
||||
}
|
||||
|
||||
export interface StorageGetInput {
|
||||
path: string;
|
||||
signedUrl?: boolean;
|
||||
expiresInSeconds?: number;
|
||||
}
|
||||
|
||||
export interface StorageDeleteInput {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export type StorageInput =
|
||||
| { action: 'upload'; data: StorageUploadInput }
|
||||
| { action: 'get'; data: StorageGetInput }
|
||||
| { action: 'delete'; data: StorageDeleteInput };
|
||||
|
||||
export interface StorageResult {
|
||||
success: boolean;
|
||||
provider: StorageProviderKind | null;
|
||||
url?: string;
|
||||
file?: Buffer;
|
||||
error?: string;
|
||||
response?: any;
|
||||
}
|
||||
Reference in New Issue
Block a user