fixed delete
This commit is contained in:
@@ -2,8 +2,12 @@ import { S3Client } from '@aws-sdk/client-s3';
|
||||
import {
|
||||
S3_ENDPOINT,
|
||||
S3_ACCESS_KEY,
|
||||
S3_SECRET_KEY
|
||||
S3_SECRET_KEY,
|
||||
S3_BUCKET
|
||||
} 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({
|
||||
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
|
||||
});
|
||||
|
||||
/**
|
||||
* 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;
|
||||
@@ -14,9 +14,14 @@ const s3 = new S3Client({
|
||||
});
|
||||
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 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 files = (result.Contents || []).map(obj => ({
|
||||
key: obj.Key,
|
||||
@@ -27,15 +32,18 @@ export async function GET({ params }) {
|
||||
return json({ files });
|
||||
}
|
||||
|
||||
export async function POST({ params, request }) {
|
||||
console.log(params);
|
||||
export async function POST({ params, request, locals, url }) {
|
||||
if (!locals.user) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
const { id } = params;
|
||||
const userId = locals.user.id;
|
||||
const type = url?.searchParams?.get('type') || 'experiment_files';
|
||||
const data = await request.formData();
|
||||
const file = data.get('file');
|
||||
const relativePath = data.get('relativePath');
|
||||
if (!file || typeof file === 'string') throw error(400, 'No file uploaded');
|
||||
const key = `experiments/${id}/${relativePath}`;
|
||||
console.log(key);
|
||||
const key = `${userId}/${id}/${type}/${relativePath}`;
|
||||
await s3.send(new PutObjectCommand({
|
||||
Bucket: BUCKET,
|
||||
Key: key,
|
||||
@@ -45,13 +53,18 @@ export async function POST({ params, request }) {
|
||||
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 userId = locals.user.id;
|
||||
const type = url?.searchParams?.get('type') || 'experiment_files';
|
||||
const key = url.searchParams.get('key');
|
||||
const prefix = url.searchParams.get('prefix');
|
||||
if (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 objects = (result.Contents || []).map(obj => ({ Key: obj.Key }));
|
||||
if (objects.length > 0) {
|
||||
@@ -65,6 +78,7 @@ export async function DELETE({ params, url }) {
|
||||
return json({ success: true, deleted: objects.length });
|
||||
}
|
||||
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 });
|
||||
}
|
||||
10
src/routes/api/user-storage.ts
Normal file
10
src/routes/api/user-storage.ts
Normal 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 });
|
||||
};
|
||||
@@ -67,7 +67,19 @@ async function uploadFiles(fileList: FileList) {
|
||||
}
|
||||
|
||||
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' });
|
||||
if (res.ok) await fetchFiles();
|
||||
}
|
||||
@@ -139,15 +151,22 @@ function copyLink() {
|
||||
|
||||
async function deleteFileOrFolder(key: string, isFolder: boolean) {
|
||||
console.log('deleteFileOrFolder called:', { key, isFolder, experimentId });
|
||||
// If folder, send a delete request with a prefix param
|
||||
if (isFolder) {
|
||||
const url = `/api/experiment/${experimentId}/files?prefix=${encodeURIComponent(key)}`;
|
||||
console.log('Deleting folder with URL:', url);
|
||||
// For folders, use the same logic to get the relative prefix
|
||||
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' });
|
||||
console.log('Folder delete response:', res.status, res.ok);
|
||||
if (res.ok) await fetchFiles();
|
||||
} else {
|
||||
console.log('Deleting file with key:', key);
|
||||
await deleteFile(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
let email = '';
|
||||
let currentPassword = '';
|
||||
let newPassword = '';
|
||||
@@ -8,6 +9,30 @@ let confirmPassword = '';
|
||||
// Use $page as a reactive value
|
||||
$: 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) {
|
||||
e.preventDefault();
|
||||
alert('Email change submitted (dummy)');
|
||||
@@ -25,6 +50,24 @@ function handlePasswordChange(e: Event) {
|
||||
|
||||
<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="font-medium">Full Name</div>
|
||||
<div class="text-lg">{fullName}</div>
|
||||
|
||||
Reference in New Issue
Block a user