added download dunctionality

This commit is contained in:
biersoeckli
2024-11-28 15:48:04 +00:00
parent 62d5c1c43c
commit 8b9cb5c0c7
6 changed files with 172 additions and 4 deletions

View File

@@ -0,0 +1,35 @@
import { FsUtils } from "@/server/utils/fs.utils";
import { PathUtils } from "@/server/utils/path.utils";
import { NextRequest, NextResponse } from "next/server";
import path from "path";
import fs from 'fs/promises';
export const dynamic = 'force-dynamic' // defaults to auto
export async function GET(request: NextRequest) {
try {
const requestUrl = new URL(request.url);
const fileName = requestUrl.searchParams.get('fileName');
if (!fileName) {
throw new Error('No file name provided.');
}
const dirOfTempDoanloadedData = PathUtils.tempVolumeDownloadPath;
const tarPath = path.join(dirOfTempDoanloadedData, fileName);
if (!await FsUtils.fileExists(tarPath)) {
throw new Error(`File ${fileName} does not exist.`);
}
const buffer = await fs.readFile(tarPath);
return new NextResponse(buffer, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="data.tar.gz"`,
},
});
} catch (error) {
console.error('Error while downloading data:', error);
return new Response((error as Error)?.message ?? 'An unknown error occured.', { status: 500 });
}
}

View File

@@ -1,12 +1,13 @@
'use server'
import { appVolumeEditZodModel } from "@/shared/model/volume-edit.model";
import { SuccessActionResult } from "@/shared/model/server-action-error-return.model";
import { ServerActionResult, SuccessActionResult } from "@/shared/model/server-action-error-return.model";
import appService from "@/server/services/app.service";
import { getAuthUserSession, saveFormAction, simpleAction } from "@/server/utils/action-wrapper.utils";
import { z } from "zod";
import { ServiceException } from "@/shared/model/service.exception.model";
import pvcStatusService from "@/server/services/pvc.status.service";
import pvcService from "@/server/services/pvc.service";
const actionAppVolumeEditZodModel = appVolumeEditZodModel.merge(z.object({
appId: z.string(),
@@ -39,3 +40,9 @@ export const getPvcUsage = async (pvcName: string, pvcNamespace: string) =>
await getAuthUserSession();
return await pvcStatusService.getPvcUsageByName(pvcName, pvcNamespace);
});
export const downloadPvcData = async (volumeId: string) =>
simpleAction(async () => {
await getAuthUserSession();
return await pvcService.downloadPvcData(volumeId); // returns the download path on the server
}) as Promise<ServerActionResult<any, string>>;

View File

@@ -4,10 +4,10 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
import { AppExtendedModel } from "@/shared/model/app-extended.model";
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import { EditIcon, TrashIcon } from "lucide-react";
import { Download, EditIcon, TrashIcon } from "lucide-react";
import DialogEditDialog from "./storage-edit-overlay";
import { Toast } from "@/frontend/utils/toast.utils";
import { deleteVolume } from "./actions";
import { deleteVolume, downloadPvcData } from "./actions";
export default function StorageList({ app }: {
@@ -40,6 +40,13 @@ export default function StorageList({ app }: {
<DialogEditDialog appId={app.id} volume={volume}>
<Button variant="ghost"><EditIcon /></Button>
</DialogEditDialog>
<Button variant="ghost" onClick={() => Toast.fromAction(() => downloadPvcData(volume.id)).then(x => {
if (x.status === 'success' && x.data) {
window.open('/api/volume-data-download?fileName=' + x.data);
}
})}>
<Download />
</Button>
<Button variant="ghost" onClick={() => Toast.fromAction(() => deleteVolume(volume.id))}>
<TrashIcon />
</Button>

View File

@@ -2,7 +2,13 @@ import { PodsInfoModel } from "@/shared/model/pods-info.model";
import k3s from "../adapter/kubernetes-api.adapter";
import { ServiceException } from "@/shared/model/service.exception.model";
import setupPodService from "./setup-services/setup-pod.service";
import fs from 'fs';
import fsPromises from 'fs/promises';
import * as tar from 'tar';
import stream from 'stream';
import { tmpdir } from 'os';
import { randomUUID } from 'crypto';
import * as k8s from '@kubernetes/client-node';
class PodService {
@@ -24,6 +30,71 @@ class PodService {
async getPodsForApp(projectId: string, appId: string): Promise<PodsInfoModel[]> {
return setupPodService.getPodsForApp(projectId, appId);
}
/**
* Copied out of Kubernetes SDK because for whatever reason
* cp.fromPod is not working using the sdk bcause of an error with some buffer
* Source: https://github.com/kubernetes-client/javascript/blob/master/src/cp.ts
*
*
* @param {string} namespace - The namespace of the pod to exec the command inside.
* @param {string} podName - The name of the pod to exec the command inside.
* @param {string} containerName - The name of the container in the pod to exec the command inside.
* @param {string} srcPath - The source path in the pod
* @param {string} zipOutputPath - The target path in local
* @param {string} [cwd] - The directory that is used as the parent in the pod when downloading
*/
public async cpFromPod(
namespace: string,
podName: string,
containerName: string,
srcPath: string,
zipOutputPath: string,
cwd?: string,
): Promise<void> {
const command = ['tar', 'zcf', '-'];
if (cwd) {
command.push('-C', cwd);
}
command.push(srcPath);
const writerStream = fs.createWriteStream(zipOutputPath);
const stderrStream = new stream.PassThrough();
return new Promise<void>((resolve, reject) => {
const exec = new k8s.Exec(k3s.getKubeConfig());
exec
.exec(
namespace,
podName,
containerName,
command,
writerStream,
stderrStream,
null,
false,
async ({ status }) => {
try {
writerStream.close();
if (status === 'Failure' /* || stderrStream.size()*/) {
return reject(
new Error(
`Error from cpFromPod - details: \n ${stderrStream.read().toString()}`,
),
);
}
/*await tar.x({
file: tmpFileName,
cwd: tgtPath,
});*/
resolve();
} catch (e) {
reject(e);
}
},
)
.catch(reject);
});
}
}
const podService = new PodService();

View File

@@ -6,11 +6,47 @@ import { AppVolume } from "@prisma/client";
import { KubeObjectNameUtils } from "../utils/kube-object-name.utils";
import { Constants } from "../../shared/utils/constants";
import { MemoryCalcUtils } from "../utils/memory-caluclation.utils";
import { FsUtils } from "../utils/fs.utils";
import { PathUtils } from "../utils/path.utils";
import * as k8s from '@kubernetes/client-node';
import dataAccess from "../adapter/db.client";
import podService from "./pod.service";
import path from "path";
class PvcService {
static readonly SHARED_PVC_NAME = 'qs-shared-pvc';
async downloadPvcData(volumeId: string) {
const volume = await dataAccess.client.appVolume.findFirstOrThrow({
where: {
id: volumeId
},
include: {
app: true
}
});
const pod = await podService.getPodsForApp(volume.app.projectId, volume.app.id);
if (pod.length === 0) {
throw new ServiceException(`No pod found for volume id ${volumeId} in app ${volume.app.id}`);
}
const firstPod = pod[0];
const downloadPath = PathUtils.volumeDownloadZipPath(volumeId);
await FsUtils.createDirIfNotExistsAsync(PathUtils.tempVolumeDownloadPath, true);
await FsUtils.deleteDirIfExistsAsync(downloadPath, true);
console.log(`Downloading data from pod ${firstPod.podName} ${volume.containerMountPath} to ${downloadPath}`);
await podService.cpFromPod(volume.app.projectId, firstPod.podName, firstPod.containerName, volume.containerMountPath, downloadPath);
const fileName = path.basename(downloadPath);
return fileName;
}
async doesAppConfigurationIncreaseAnyPvcSize(app: AppExtendedModel) {
const existingPvcs = await this.getAllPvcForApp(app.projectId, app.id);

View File

@@ -9,6 +9,10 @@ export class PathUtils {
return path.join(this.tempDataRoot, 'git');
}
static get tempVolumeDownloadPath() {
return path.join(this.tempDataRoot, 'volume-downloads');
}
static gitRootPathForApp(appId: string): string {
return path.join(PathUtils.gitRootPath, this.convertIdToFolderFriendlyName(appId));
}
@@ -21,6 +25,14 @@ export class PathUtils {
return path.join(this.deploymentLogsPath, `${deploymentId}.log`);
}
static volumeDownloadFolder(volumeId: string): string {
return path.join(this.tempVolumeDownloadPath, `${volumeId}-data`);
}
static volumeDownloadZipPath(volumeId: string): string {
return path.join(this.tempVolumeDownloadPath, `${volumeId}.tar.gz`);
}
private static convertIdToFolderFriendlyName(id: string): string {
// remove all special characters
return id.replace(/[^a-zA-Z0-9]/g, '_');