fixed delete
This commit is contained in:
@@ -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;
|
||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
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) {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user