fixed delete

This commit is contained in:
Shaheed Azaad
2025-07-16 12:30:04 +02:00
parent 19ffd48ac0
commit dc2f68a2b4
5 changed files with 126 additions and 16 deletions

View File

@@ -2,8 +2,12 @@ import { S3Client } from '@aws-sdk/client-s3';
import { import {
S3_ENDPOINT, S3_ENDPOINT,
S3_ACCESS_KEY, S3_ACCESS_KEY,
S3_SECRET_KEY S3_SECRET_KEY,
S3_BUCKET
} from '$env/static/private'; } from '$env/static/private';
import { ListObjectsV2Command } from '@aws-sdk/client-s3';
import type { ListObjectsV2CommandOutput } from '@aws-sdk/client-s3';
import { HeadObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({ const s3 = new S3Client({
region: 'us-east-1', // MinIO ignores region but AWS SDK requires it region: 'us-east-1', // MinIO ignores region but AWS SDK requires it
@@ -15,4 +19,24 @@ const s3 = new S3Client({
forcePathStyle: true // needed for MinIO forcePathStyle: true // needed for MinIO
}); });
/**
* Returns the total storage used by a user (in bytes) by summing all S3 objects under their userId prefix.
*/
export async function getUserStorageUsage(userId: string): Promise<number> {
const BUCKET = S3_BUCKET!;
try {
const result = await s3.send(new HeadObjectCommand({
Bucket: BUCKET,
Key: userId,
}));
return result.ContentLength || 0;
} catch (err: any) {
// If the file does not exist, return 0
if (err.name === 'NotFound' || err.$metadata?.httpStatusCode === 404) {
return 0;
}
throw err;
}
}
export default s3; export default s3;

View File

@@ -14,9 +14,14 @@ const s3 = new S3Client({
}); });
const BUCKET = S3_BUCKET!; const BUCKET = S3_BUCKET!;
export async function GET({ params }) { export async function GET({ params, locals, url }) {
if (!locals.user) {
return json({ error: 'Unauthorized' }, { status: 401 });
}
const { id } = params; const { id } = params;
const prefix = `experiments/${id}/`; const userId = locals.user.id;
const type = url?.searchParams?.get('type') || 'experiment_files';
const prefix = `${userId}/${id}/${type}/`;
const result = await s3.send(new ListObjectsV2Command({ Bucket: BUCKET, Prefix: prefix })); const result = await s3.send(new ListObjectsV2Command({ Bucket: BUCKET, Prefix: prefix }));
const files = (result.Contents || []).map(obj => ({ const files = (result.Contents || []).map(obj => ({
key: obj.Key, key: obj.Key,
@@ -27,15 +32,18 @@ export async function GET({ params }) {
return json({ files }); return json({ files });
} }
export async function POST({ params, request }) { export async function POST({ params, request, locals, url }) {
console.log(params); if (!locals.user) {
return json({ error: 'Unauthorized' }, { status: 401 });
}
const { id } = params; const { id } = params;
const userId = locals.user.id;
const type = url?.searchParams?.get('type') || 'experiment_files';
const data = await request.formData(); const data = await request.formData();
const file = data.get('file'); const file = data.get('file');
const relativePath = data.get('relativePath'); const relativePath = data.get('relativePath');
if (!file || typeof file === 'string') throw error(400, 'No file uploaded'); if (!file || typeof file === 'string') throw error(400, 'No file uploaded');
const key = `experiments/${id}/${relativePath}`; const key = `${userId}/${id}/${type}/${relativePath}`;
console.log(key);
await s3.send(new PutObjectCommand({ await s3.send(new PutObjectCommand({
Bucket: BUCKET, Bucket: BUCKET,
Key: key, Key: key,
@@ -45,13 +53,18 @@ export async function POST({ params, request }) {
return json({ success: true, key }); return json({ success: true, key });
} }
export async function DELETE({ params, url }) { export async function DELETE({ params, url, locals }) {
if (!locals.user) {
return json({ error: 'Unauthorized' }, { status: 401 });
}
const { id } = params; const { id } = params;
const userId = locals.user.id;
const type = url?.searchParams?.get('type') || 'experiment_files';
const key = url.searchParams.get('key'); const key = url.searchParams.get('key');
const prefix = url.searchParams.get('prefix'); const prefix = url.searchParams.get('prefix');
if (prefix) { if (prefix) {
// Delete all objects with this prefix // Delete all objects with this prefix
const fullPrefix = `experiments/${id}/${prefix}`; const fullPrefix = `${userId}/${id}/${type}/${prefix}`;
const result = await s3.send(new ListObjectsV2Command({ Bucket: BUCKET, Prefix: fullPrefix })); const result = await s3.send(new ListObjectsV2Command({ Bucket: BUCKET, Prefix: fullPrefix }));
const objects = (result.Contents || []).map(obj => ({ Key: obj.Key })); const objects = (result.Contents || []).map(obj => ({ Key: obj.Key }));
if (objects.length > 0) { if (objects.length > 0) {
@@ -65,6 +78,7 @@ export async function DELETE({ params, url }) {
return json({ success: true, deleted: objects.length }); return json({ success: true, deleted: objects.length });
} }
if (!key) throw error(400, 'Missing key or prefix'); if (!key) throw error(400, 'Missing key or prefix');
await s3.send(new DeleteObjectCommand({ Bucket: BUCKET, Key: key })); const fullKey = `${userId}/${id}/${type}/${key}`;
await s3.send(new DeleteObjectCommand({ Bucket: BUCKET, Key: fullKey }));
return json({ success: true }); return json({ success: true });
} }

View File

@@ -0,0 +1,10 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { getUserStorageUsage } from '$lib/server/s3';
export const GET: RequestHandler = async ({ locals }) => {
if (!locals.user) {
return json({ error: 'Unauthorized' }, { status: 401 });
}
const usage = await getUserStorageUsage(locals.user.id);
return json({ usage });
};

View File

@@ -67,7 +67,19 @@ async function uploadFiles(fileList: FileList) {
} }
async function deleteFile(key: string) { async function deleteFile(key: string) {
const url = `/api/experiment/${experimentId}/files?key=${encodeURIComponent(key)}`; // If key contains experiment_files/, strip everything before and including it
let relativeKey = key;
const expFilesIdx = key.indexOf('/experiment_files/');
if (expFilesIdx !== -1) {
relativeKey = key.substring(expFilesIdx + '/experiment_files/'.length);
} else {
// fallback: if key contains experimentId, strip up to and including it
const expIdIdx = key.indexOf(experimentId + '/');
if (expIdIdx !== -1) {
relativeKey = key.substring(expIdIdx + experimentId.length + 1);
}
}
const url = `/api/experiment/${experimentId}/files?key=${encodeURIComponent(relativeKey)}`;
const res = await fetch(url, { method: 'DELETE' }); const res = await fetch(url, { method: 'DELETE' });
if (res.ok) await fetchFiles(); if (res.ok) await fetchFiles();
} }
@@ -139,15 +151,22 @@ function copyLink() {
async function deleteFileOrFolder(key: string, isFolder: boolean) { async function deleteFileOrFolder(key: string, isFolder: boolean) {
console.log('deleteFileOrFolder called:', { key, isFolder, experimentId }); console.log('deleteFileOrFolder called:', { key, isFolder, experimentId });
// If folder, send a delete request with a prefix param
if (isFolder) { if (isFolder) {
const url = `/api/experiment/${experimentId}/files?prefix=${encodeURIComponent(key)}`; // For folders, use the same logic to get the relative prefix
console.log('Deleting folder with URL:', url); let relativePrefix = key;
const expFilesIdx = key.indexOf('/experiment_files/');
if (expFilesIdx !== -1) {
relativePrefix = key.substring(expFilesIdx + '/experiment_files/'.length);
} else {
const expIdIdx = key.indexOf(experimentId + '/');
if (expIdIdx !== -1) {
relativePrefix = key.substring(expIdIdx + experimentId.length + 1);
}
}
const url = `/api/experiment/${experimentId}/files?prefix=${encodeURIComponent(relativePrefix)}`;
const res = await fetch(url, { method: 'DELETE' }); const res = await fetch(url, { method: 'DELETE' });
console.log('Folder delete response:', res.status, res.ok);
if (res.ok) await fetchFiles(); if (res.ok) await fetchFiles();
} else { } else {
console.log('Deleting file with key:', key);
await deleteFile(key); await deleteFile(key);
} }
} }

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/stores';
import { onMount } from 'svelte';
let email = ''; let email = '';
let currentPassword = ''; let currentPassword = '';
let newPassword = ''; let newPassword = '';
@@ -8,6 +9,30 @@ let confirmPassword = '';
// Use $page as a reactive value // Use $page as a reactive value
$: fullName = $page.data?.user?.username || ''; $: fullName = $page.data?.user?.username || '';
let storageUsed = 0;
const MAX_STORAGE = 1024 * 1024 * 1024; // 1GB in bytes
let loadingStorage = true;
onMount(async () => {
loadingStorage = true;
try {
const res = await fetch('/api/user-storage');
if (res.ok) {
const data = await res.json();
storageUsed = data.usage;
}
} finally {
loadingStorage = false;
}
});
function formatBytes(bytes: number) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
}
function handleEmailChange(e: Event) { function handleEmailChange(e: Event) {
e.preventDefault(); e.preventDefault();
alert('Email change submitted (dummy)'); alert('Email change submitted (dummy)');
@@ -25,6 +50,24 @@ function handlePasswordChange(e: Event) {
<h1 class="text-2xl font-bold mb-4">Profile</h1> <h1 class="text-2xl font-bold mb-4">Profile</h1>
<!-- Storage Usage Bar -->
<div class="mb-8 p-4 bg-muted rounded">
<div class="font-medium mb-2">Storage Usage</div>
{#if loadingStorage}
<div>Loading...</div>
{:else}
<div class="flex items-center gap-2 mb-1">
<div class="flex-1 h-4 bg-gray-200 rounded overflow-hidden">
<div class="h-4 bg-primary" style="width: {Math.min(100, (storageUsed / MAX_STORAGE) * 100)}%"></div>
</div>
<div class="text-sm text-muted-foreground whitespace-nowrap">{formatBytes(storageUsed)} / 1 GB</div>
</div>
{#if storageUsed > MAX_STORAGE}
<div class="text-red-600 text-xs mt-1">You have exceeded your storage limit!</div>
{/if}
{/if}
</div>
<div class="mb-8 p-4 bg-muted rounded"> <div class="mb-8 p-4 bg-muted rounded">
<div class="font-medium">Full Name</div> <div class="font-medium">Full Name</div>
<div class="text-lg">{fullName}</div> <div class="text-lg">{fullName}</div>