chore: working on backup api for new storage system.

This commit is contained in:
charlesgauthereau
2026-01-15 21:27:36 +01:00
parent dd1f791b3e
commit 670f6a1974
16 changed files with 437 additions and 102 deletions
@@ -3,7 +3,7 @@ import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/
import {Metadata} from "next"; import {Metadata} from "next";
import {ChannelsSection} from "@/components/wrappers/dashboard/admin/channels/channels-section"; import {ChannelsSection} from "@/components/wrappers/dashboard/admin/channels/channels-section";
import {db} from "@/db"; import {db} from "@/db";
import {desc, isNotNull, isNull, not} from "drizzle-orm"; import {desc, eq, isNotNull, isNull, not} from "drizzle-orm";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {StorageChannelWith} from "@/db/schema/12_storage-channel"; import {StorageChannelWith} from "@/db/schema/12_storage-channel";
import {ChannelAddEditModal} from "@/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal"; import {ChannelAddEditModal} from "@/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal";
@@ -29,6 +29,10 @@ export default async function RoutePage(props: PageParams<{}>) {
}, },
}); });
const settings = await db.query.setting.findFirst({
where: eq(drizzleDb.schemas.setting.name, "system"),
});
return ( return (
<Page> <Page>
@@ -39,7 +43,7 @@ export default async function RoutePage(props: PageParams<{}>) {
</PageActions> </PageActions>
</PageHeader> </PageHeader>
<PageContent> <PageContent>
<ChannelsSection kind={"storage"} organizations={organizations} channels={storageChannels}/> <ChannelsSection defaultStorageChannelId={settings?.defaultStorageChannelId} kind={"storage"} organizations={organizations} channels={storageChannels}/>
</PageContent> </PageContent>
</Page> </Page>
); );
@@ -1,8 +1,5 @@
import fs from "node:fs"; import fs from "node:fs";
import forge from "node-forge"; import forge from "node-forge";
import {EventPayload} from "@/features/notifications/types";
import {dispatchNotification} from "@/features/notifications/dispatch";
import {Database, DatabaseWith} from "@/db/schema/07_database";
export async function decryptedDump(file: File, aesKeyHex: string, ivHex: string, fileExtension: string): Promise<File> { export async function decryptedDump(file: File, aesKeyHex: string, ivHex: string, fileExtension: string): Promise<File> {
+34 -25
View File
@@ -11,6 +11,7 @@ import {env} from "@/env.mjs";
import {withUpdatedAt} from "@/db/utils"; 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";
export async function POST( export async function POST(
request: Request, request: Request,
@@ -25,6 +26,7 @@ export async function POST(
{status: 400} {status: 400}
); );
} }
eventEmitter.emit('modification', {update: true}); eventEmitter.emit('modification', {update: true});
const agentId = (await params).agentId; const agentId = (await params).agentId;
@@ -56,7 +58,8 @@ export async function POST(
where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId), where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
with: { with: {
project: true, project: true,
alertPolicies: true alertPolicies: true,
storagePolicies: true
} }
}); });
@@ -125,35 +128,41 @@ export async function POST(
const uuid = uuidv4(); const uuid = uuidv4();
const fileName = `${uuid}${fileExtension}`; const fileName = `${uuid}${fileExtension}`;
const buffer = Buffer.from(await decryptedFile.arrayBuffer()); const buffer = Buffer.from(await decryptedFile.arrayBuffer());
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
if (!settings) {
throw new Error("System settings not found.");
}
let success: boolean, message: string, filePath: string;
const result =
settings.storage === "local"
? await uploadLocalPrivate(fileName, buffer)
: await uploadS3Private(`${database.project?.slug}/${fileName}`, buffer, env.S3_BUCKET_NAME!);
({success, message, filePath} = result); // const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
// if (!settings) {
// throw new Error("System settings not found.");
// }
if (!success) { // let success: boolean, message: string, filePath: string;
return NextResponse.json( //
{error: message}, // const result =
{status: 500} // settings.storage === "local"
); // ? await uploadLocalPrivate(fileName, buffer)
} // : await uploadS3Private(`${database.project?.slug}/${fileName}`, buffer, env.S3_BUCKET_NAME!);
//
// ({success, message, filePath} = result);
//
// if (!success) {
// return NextResponse.json(
// {error: message},
// {status: 500}
// );
// }
await db await storeBackupFiles(backup, database, buffer, fileName)
.update(drizzleDb.schemas.backup)
.set(withUpdatedAt({ //
file: fileName, // await db
fileSize: fileSizeBytes, // .update(drizzleDb.schemas.backup)
status: 'success', // .set(withUpdatedAt({
})) // file: fileName,
.where(eq(drizzleDb.schemas.backup.id, backup.id)); // fileSize: fileSizeBytes,
// status: 'success',
// }))
// .where(eq(drizzleDb.schemas.backup.id, backup.id));
eventEmitter.emit('modification', {update: true}); eventEmitter.emit('modification', {update: true});
@@ -42,6 +42,7 @@ export const ChannelAddEditModal = ({
}: ChannelAddModalProps) => { }: ChannelAddModalProps) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const [openInternal, setOpen] = useState(open); const [openInternal, setOpen] = useState(open);
const isLocalSystem = channel?.provider == "local";
const isCreate = !Boolean(channel); const isCreate = !Boolean(channel);
@@ -82,46 +83,65 @@ export const ChannelAddEditModal = ({
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div> <div>
{adminView ? <>
<Tabs className="flex flex-col flex-1" defaultValue="configuration"> {!isLocalSystem ? (
<TabsList className="grid w-full grid-cols-2"> <>
<TabsTrigger value="configuration">Configuration</TabsTrigger> {adminView ?
<TabsTrigger value="organizations">Organizations</TabsTrigger> <Tabs className="flex flex-col flex-1" defaultValue="configuration">
</TabsList> <TabsList className="grid w-full grid-cols-2">
<TabsContent className="h-full justify-between" value="configuration"> <TabsTrigger value="configuration">Configuration</TabsTrigger>
<ChannelForm <TabsTrigger value="organizations">Organizations</TabsTrigger>
kind={kind} </TabsList>
adminView={adminView} <TabsContent className="h-full justify-between" value="configuration">
defaultValues={channel} <ChannelForm
organization={organization} kind={kind}
onSuccessAction={() => { adminView={adminView}
onOpenChangeAction?.(false) defaultValues={channel}
setOpen(false); organization={organization}
}} onSuccessAction={() => {
/> onOpenChangeAction?.(false)
</TabsContent> setOpen(false);
<TabsContent className="h-full justify-between" value="organizations"> }}
/>
</TabsContent>
<TabsContent className="h-full justify-between" value="organizations">
<ChannelOrganisationForm
defaultValues={channel}
kind={kind}
organizations={organizations}
/>
</TabsContent>
</Tabs>
:
<>
<ChannelForm
kind={kind}
adminView={adminView}
defaultValues={channel}
organization={organization}
onSuccessAction={() => {
onOpenChangeAction?.(false)
setOpen(false);
}}
/>
</>
}
</>
)
:
<>
<ChannelOrganisationForm <ChannelOrganisationForm
defaultValues={channel} defaultValues={channel}
kind={kind} kind={kind}
organizations={organizations} organizations={organizations}
/> />
</TabsContent> </>
</Tabs> }
:
<>
<ChannelForm </>
kind={kind}
adminView={adminView}
defaultValues={channel}
organization={organization}
onSuccessAction={() => {
onOpenChangeAction?.(false)
setOpen(false);
}}
/>
</>
}
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -33,6 +33,7 @@ export const EditChannelButton = ({
}: EditChannelButtonProps) => { }: EditChannelButtonProps) => {
const router = useRouter(); const router = useRouter();
const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const isLocalSystem = channel.provider == "local";
const mutation = useMutation({ const mutation = useMutation({
@@ -73,10 +74,12 @@ export const EditChannelButton = ({
return ( return (
<> <>
<Switch checked={channel.enabled} onCheckedChange={async () => {
await mutation.mutateAsync(!channel.enabled) {!isLocalSystem && (
}} <Switch checked={channel.enabled} onCheckedChange={async () => {
/> await mutation.mutateAsync(!channel.enabled)
}}/>
)}
<ChannelAddEditModal <ChannelAddEditModal
kind={kind} kind={kind}
organizations={organizations} organizations={organizations}
@@ -21,13 +21,17 @@ export type ChannelCardProps = {
organizations?: OrganizationWithMembers[]; organizations?: OrganizationWithMembers[];
adminView?: boolean; adminView?: boolean;
kind?: ChannelKind; kind?: ChannelKind;
defaultStorageChannelId?: string | null | undefined
}; };
export const ChannelCard = (props: ChannelCardProps) => { export const ChannelCard = (props: ChannelCardProps) => {
const {data, organization, kind, adminView} = props; const {data, organization, kind, adminView, defaultStorageChannelId} = props;
const isMobile = useIsMobile() const isMobile = useIsMobile()
const isDefaultSystemStorage = defaultStorageChannelId === data.id;
const isOwned = data.organizationId ? true : !organization; const isOwned = data.organizationId ? true : !organization;
const isLocalSystem = data.provider == "local"; const isLocalSystem = data.provider == "local";
@@ -47,11 +51,16 @@ export const ChannelCard = (props: ChannelCardProps) => {
<Badge variant="secondary" className="text-xs font-mono"> <Badge variant="secondary" className="text-xs font-mono">
{data.provider} {data.provider}
</Badge> </Badge>
{isDefaultSystemStorage && (
<Badge variant="secondary" className="text-xs font-mono">
default
</Badge>
)}
</div> </div>
</div> </div>
{kind && ( {kind && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{(isOwned && !isLocalSystem) && ( {(isOwned) && (
<> <>
<EditChannelButton <EditChannelButton
organizations={props.organizations} organizations={props.organizations}
@@ -60,11 +69,13 @@ export const ChannelCard = (props: ChannelCardProps) => {
channel={data} channel={data}
kind={kind} kind={kind}
/> />
<DeleteChannelButton {!isLocalSystem && (
kind={kind} <DeleteChannelButton
organizationId={organization?.id} kind={kind}
channelId={data.id} organizationId={organization?.id}
/> channelId={data.id}
/>
)}
</> </>
)} )}
</div> </div>
@@ -3,6 +3,7 @@ import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/compon
import {Input} from "@/components/ui/input"; import {Input} from "@/components/ui/input";
import {Separator} from "@/components/ui/separator"; import {Separator} from "@/components/ui/separator";
import {PasswordInput} from "@/components/ui/password-input"; import {PasswordInput} from "@/components/ui/password-input";
import {Switch} from "@/components/ui/switch";
type StorageS3FormProps = { type StorageS3FormProps = {
@@ -59,12 +60,43 @@ export const StorageS3Form = ({form}: StorageS3FormProps) => {
<FormItem> <FormItem>
<FormLabel>Bucket name</FormLabel> <FormLabel>Bucket name</FormLabel>
<FormControl> <FormControl>
<PasswordInput {...field} placeholder="portabase-dev"/> <Input {...field} placeholder="portabase-dev"/>
</FormControl> </FormControl>
<FormMessage/> <FormMessage/>
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="config.port"
render={({ field }) => (
<FormItem>
<FormLabel>Port</FormLabel>
<FormControl>
<Input {...field} type="number" placeholder="443" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.useSSL"
render={({ field }) => (
<FormItem>
<FormLabel>Use SSL</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</> </>
) )
} }
@@ -10,15 +10,17 @@ import {ChannelAddEditModal} from "@/components/wrappers/dashboard/admin/channel
import {ChannelKind, getChannelTextBasedOnKind} from "@/components/wrappers/dashboard/admin/channels/helpers/common"; import {ChannelKind, getChannelTextBasedOnKind} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
type ChannelsSectionProps = { type ChannelsSectionProps = {
channels: NotificationChannelWith[] | StorageChannelWith[] channels: NotificationChannelWith[] | StorageChannelWith[],
organizations: OrganizationWithMembers[] organizations: OrganizationWithMembers[],
kind: ChannelKind; kind: ChannelKind,
defaultStorageChannelId?: string | null | undefined
} }
export const ChannelsSection = ({ export const ChannelsSection = ({
organizations, organizations,
channels, channels,
kind kind,
defaultStorageChannelId
}: ChannelsSectionProps) => { }: ChannelsSectionProps) => {
const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isAddModalOpen, setIsAddModalOpen] = useState(false);
@@ -41,6 +43,7 @@ export const ChannelsSection = ({
adminView={true} adminView={true}
organizations={organizations} organizations={organizations}
kind={kind} kind={kind}
defaultStorageChannelId={defaultStorageChannelId}
/> />
</div> </div>
) : ( ) : (
+3 -1
View File
@@ -14,6 +14,7 @@ import * as alertPolicy from "./schema/10_alert-policy";
import * as notificationLog from "./schema/11_notification-log"; import * as notificationLog from "./schema/11_notification-log";
import * as storageChannel from "./schema/12_storage-channel"; import * as storageChannel from "./schema/12_storage-channel";
import * as storagePolicy from "@/db/schema/13_storage-policy"; import * as storagePolicy from "@/db/schema/13_storage-policy";
import * as backupStorage from "@/db/schema/14_storage-backup";
import {Pool} from "pg"; import {Pool} from "pg";
@@ -44,7 +45,8 @@ export const schemas = {
...alertPolicy, ...alertPolicy,
...notificationLog, ...notificationLog,
...storageChannel, ...storageChannel,
...storagePolicy ...storagePolicy,
...backupStorage
}; };
export const db = drizzle({ export const db = drizzle({
+11
View File
@@ -4,6 +4,12 @@ import {createSelectSchema} from "drizzle-zod";
import {z} from "zod"; import {z} from "zod";
import {timestamps} from "@/db/schema/00_common"; import {timestamps} from "@/db/schema/00_common";
import {storageChannel} from "@/db/schema/12_storage-channel"; import {storageChannel} from "@/db/schema/12_storage-channel";
import {relations} from "drizzle-orm";
import {agent} from "@/db/schema/08_agent";
import {project} from "@/db/schema/06_project";
import {alertPolicy} from "@/db/schema/10_alert-policy";
import {storagePolicy} from "@/db/schema/13_storage-policy";
import {backup, database, restoration, retentionPolicy} from "@/db/schema/07_database";
export const setting = pgTable("settings", { export const setting = pgTable("settings", {
id: uuid("id").primaryKey().defaultRandom(), id: uuid("id").primaryKey().defaultRandom(),
@@ -23,5 +29,10 @@ export const setting = pgTable("settings", {
...timestamps ...timestamps
}); });
export const settingRelations = relations(setting, ({one, many}) => ({
storageChannel: one(storageChannel, {fields: [setting.defaultStorageChannelId], references: [storageChannel.id]}),
}));
export const settingSchema = createSelectSchema(setting); export const settingSchema = createSelectSchema(setting);
export type Setting = z.infer<typeof settingSchema>; export type Setting = z.infer<typeof settingSchema>;
+60 -8
View File
@@ -5,6 +5,8 @@ import * as drizzleDb from '@/db';
import {db} from '@/db'; import {db} from '@/db';
import type {StorageInput, StorageProviderKind, StorageResult,} from './types'; import type {StorageInput, StorageProviderKind, StorageResult,} from './types';
import {dispatchViaProvider} from "@/features/storages/providers"; import {dispatchViaProvider} from "@/features/storages/providers";
import {StorageChannel} from "@/db/schema/12_storage-channel";
import {Json} from "drizzle-zod";
export async function dispatchStorage( export async function dispatchStorage(
input: StorageInput, input: StorageInput,
@@ -13,34 +15,84 @@ export async function dispatchStorage(
organizationId?: string organizationId?: string
): Promise<StorageResult> { ): Promise<StorageResult> {
try { try {
if (!channelId) {
let channel: StorageChannel | null = null;
if (policyId) {
const policyDb = await db.query.storagePolicy.findFirst({
where: eq(drizzleDb.schemas.storagePolicy.id, policyId),
with: {
storageChannel: true
},
});
if (!policyDb || !policyDb.storageChannel) {
return {
success: false,
provider: null,
error: "Policy or associated channel not found",
};
}
if (!policyDb.enabled || !policyDb.storageChannel.enabled) {
return {
success: false,
provider: policyDb.storageChannel.provider as any,
error: "Policy or channel is disabled",
};
}
channel = {
...policyDb.storageChannel,
config: policyDb.storageChannel.config as Json,
};
}
if (channelId) {
const fetchedChannel = await db.query.storageChannel.findFirst({
where: eq(drizzleDb.schemas.storageChannel.id, channelId),
});
if (!fetchedChannel) {
return {
success: false,
provider: null,
error: "Channel not found",
};
}
channel = {
...fetchedChannel,
config: fetchedChannel.config as Json,
};
}
if (!channel) {
return { return {
success: false, success: false,
provider: null, provider: null,
error: 'No storage channel provided', error: "No valid channel to dispatch on storage",
}; };
} }
const channel = await db.query.storageChannel.findFirst({
where: eq(drizzleDb.schemas.storageChannel.id, channelId),
});
if (!channel || !channel.enabled) { if (!channel.enabled) {
return { return {
success: false, success: false,
provider: channel?.provider as StorageProviderKind, provider: null,
error: 'Storage channel not found or disabled', error: "Channel not active",
}; };
} }
return await dispatchViaProvider( return await dispatchViaProvider(
channel.provider as StorageProviderKind, channel.provider as StorageProviderKind,
channel.config, channel.config,
input input
); );
} catch (err: any) { } catch (err: any) {
return { return {
success: false, success: false,
+101 -5
View File
@@ -1,10 +1,106 @@
import {DatabaseWith} from "@/db/schema/07_database"; import { Backup, DatabaseWith } from "@/db/schema/07_database";
import {StorageChannel} from "@/db/schema/12_storage-channel"; import { dispatchStorage } from "@/features/storages/dispatch";
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers"; import type { StorageInput, StorageResult } from "@/features/storages/types";
import * as drizzleDb from "@/db";
import { withUpdatedAt } from "@/db/utils";
import { eq } from "drizzle-orm";
import { db } from "@/db";
import { createHash } from "crypto";
function computeChecksum(buffer: Buffer): string {
return createHash("sha256").update(buffer).digest("hex");
}
export async function storeBackupFiles(
backup: Backup,
database: DatabaseWith,
file: Buffer,
fileName: string
): Promise<StorageResult[]> {
const settings = await db.query.setting.findFirst({
where: eq(drizzleDb.schemas.setting.name, "system"),
with: { storageChannel: true },
});
const defaultPolicy = settings?.storageChannel
? [{
id: null,
storageChannelId: settings.storageChannel.id,
enabled: true
}]
: [];
console.log(database.storagePolicies);
const policies = (database.storagePolicies?.filter(p => p.enabled) || defaultPolicy);
console.log("Policies", policies);
export async function storeFileBackup(database: DatabaseWith, file: Buffer) { if (!policies.length) {
return [];
}
const path = `${database.project?.slug}/${fileName}`;
const size = file.length;
const checksum = computeChecksum(file);
const results = await Promise.all(
policies.map(async policy => {
const [backupStorage] = await db
.insert(drizzleDb.schemas.backupStorage)
.values({
backupId: backup.id,
storageChannelId: policy.storageChannelId,
status: "pending",
path,
size,
checksum,
})
.returning();
} const input: StorageInput = {
action: "upload",
data: { path, file },
};
// const result = await dispatchStorage(input, policy.id);
let result: StorageResult;
try {
if (policy.id){
result = await dispatchStorage(input, policy.id);
}else{
result = await dispatchStorage(input, undefined, policy.storageChannelId);
}
} catch (err) {
console.error(err);
result = {
success: false,
provider: null,
error: err instanceof Error ? err.message : "Unknown error"
};
}
await db
.update(drizzleDb.schemas.backupStorage)
.set(withUpdatedAt({ status: result.success ? "success" : "failed" }))
.where(eq(drizzleDb.schemas.backupStorage.id, backupStorage.id));
return result;
})
);
console.log(results);
const backupStatus = results.some(r => r.success) ? "success" : "failed";
await db
.update(drizzleDb.schemas.backup)
.set(withUpdatedAt({ status: backupStatus }))
.where(eq(drizzleDb.schemas.backup.id, backup.id));
return results;
}
+6 -5
View File
@@ -5,6 +5,7 @@ import type {
} from '../types'; } from '../types';
import {uploadLocal, getLocal, deleteLocal} from './local'; import {uploadLocal, getLocal, deleteLocal} from './local';
import {deleteS3, getS3, uploadS3} from "@/features/storages/providers/s3";
type ProviderHandler = { type ProviderHandler = {
upload: (config: any, input: StorageInput & { action: 'upload' }) => Promise<StorageResult>; upload: (config: any, input: StorageInput & { action: 'upload' }) => Promise<StorageResult>;
@@ -18,11 +19,11 @@ const handlers: Record<StorageProviderKind, ProviderHandler> = {
get: getLocal, get: getLocal,
delete: deleteLocal, delete: deleteLocal,
}, },
// s3: { s3: {
// upload: uploadS3, upload: uploadS3,
// get: getS3, get: getS3,
// delete: deleteS3, delete: deleteS3,
// }, },
// gcs: null as any, // gcs: null as any,
// azure: null as any, // azure: null as any,
}; };
+3 -1
View File
@@ -14,7 +14,9 @@ export async function uploadLocal(
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);
await mkdir(fullPath, {recursive: true}); const dir = path.dirname(fullPath);
await mkdir(dir, { recursive: true });
await writeFile(fullPath, input.data.file); await writeFile(fullPath, input.data.file);
return { return {
+92
View File
@@ -0,0 +1,92 @@
import * as Minio from "minio";
import {StorageDeleteInput, StorageGetInput, StorageResult, StorageUploadInput} from "../types";
type S3Config = {
endPointUrl: string;
accessKey: string;
secretKey: string;
bucketName: string;
port?: number;
useSSL?: boolean;
};
async function getS3Client(config: S3Config) {
return new Minio.Client({
endPoint: config.endPointUrl,
accessKey: config.accessKey,
secretKey: config.secretKey,
port: config.port ?? 443,
useSSL: config.useSSL ?? true,
});
}
const BASE_DIR = "backups/";
async function ensureBucket(config: S3Config) {
const client = await getS3Client(config);
const exists = await client.bucketExists(config.bucketName);
if (!exists) await client.makeBucket(config.bucketName);
}
export async function uploadS3(config: S3Config, input: { data: StorageUploadInput }): Promise<StorageResult> {
const client = await getS3Client(config);
await ensureBucket(config);
const key = `${BASE_DIR}${input.data.path}`;
try {
await client.statObject(config.bucketName, key);
return {success: false, provider: "s3", error: "File already exists"};
} catch {
// continue if not found
}
await client.putObject(config.bucketName, key, input.data.file as Buffer);
return {
success: true,
provider: "s3",
};
}
export async function getS3(config: S3Config, input: { data: StorageGetInput }): Promise<StorageResult> {
const client = await getS3Client(config);
// const key = input.data.path;
const key = `${BASE_DIR}${input.data.path}`;
try {
await client.statObject(config.bucketName, key);
} catch {
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 chunks: Buffer[] = [];
for await (const chunk of fileStream) chunks.push(chunk as Buffer);
const buffer = Buffer.concat(chunks);
return {
success: true,
provider: "s3",
file: buffer,
url: presignedUrl,
};
}
export async function deleteS3(config: S3Config, input: { data: StorageDeleteInput }): Promise<StorageResult> {
const client = await getS3Client(config);
// const key = input.data.path;
const key = `${BASE_DIR}${input.data.path}`;
try {
await client.removeObject(config.bucketName, key);
return {success: true, provider: "s3"};
} catch (err: any) {
return {success: false, provider: "s3", error: err.message};
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
export type StorageProviderKind = export type StorageProviderKind =
| 'local' | 'local'
// | 's3' | 's3'
; ;
export type StorageAction = export type StorageAction =