aethex-forge/client/lib/ethos-storage.ts
2025-11-13 03:58:48 +00:00

158 lines
4.2 KiB
TypeScript

import { supabase } from "./supabase";
const BUCKET_NAME = "ethos-tracks";
export interface UploadProgress {
bytesUploaded: number;
bytesTotal: number;
percentComplete: number;
}
export const ethosStorage = {
/**
* Upload a track file to Supabase Storage
* @param file The audio file to upload
* @param userId The ID of the user uploading the track
* @param onProgress Callback for upload progress
* @returns The path to the uploaded file in storage
*/
async uploadTrackFile(
file: File,
userId: string,
onProgress?: (progress: UploadProgress) => void,
): Promise<string> {
try {
// Ensure bucket exists and is accessible
const bucketList = await supabase.storage.listBuckets();
const bucketExists = bucketList.data?.some((b) => b.name === BUCKET_NAME);
if (!bucketExists) {
// Bucket doesn't exist, we'll let the upload fail with a helpful error
throw new Error(
`Storage bucket "${BUCKET_NAME}" does not exist. Please contact support.`,
);
}
// Create a unique file path
const timestamp = Date.now();
const sanitizedFileName = file.name.replace(/[^a-zA-Z0-9._-]/g, "_");
const filePath = `${userId}/${timestamp}-${sanitizedFileName}`;
// Upload file
const { data, error } = await supabase.storage
.from(BUCKET_NAME)
.upload(filePath, file, {
cacheControl: "3600",
upsert: false,
});
if (error) {
console.error("Storage upload error:", error);
throw new Error(`Failed to upload file: ${error.message}`);
}
if (!data) {
throw new Error("Upload succeeded but no file path returned");
}
return data.path;
} catch (error) {
console.error("Track upload error:", error);
throw error;
}
},
/**
* Get a public URL for a track file
* @param filePath The path to the file in storage
* @returns The public URL for the file
*/
getPublicUrl(filePath: string): string {
const { data } = supabase.storage.from(BUCKET_NAME).getPublicUrl(filePath);
return data.publicUrl;
},
/**
* Download a track file
* @param filePath The path to the file in storage
* @returns The file data
*/
async downloadTrackFile(filePath: string): Promise<Blob> {
const { data, error } = await supabase.storage
.from(BUCKET_NAME)
.download(filePath);
if (error) {
throw new Error(`Failed to download file: ${error.message}`);
}
if (!data) {
throw new Error("Download succeeded but no data returned");
}
return data;
},
/**
* Delete a track file
* @param filePath The path to the file in storage
*/
async deleteTrackFile(filePath: string): Promise<void> {
const { error } = await supabase.storage
.from(BUCKET_NAME)
.remove([filePath]);
if (error) {
throw new Error(`Failed to delete file: ${error.message}`);
}
},
/**
* Get file metadata (size, created_at, etc.)
* @param filePath The path to the file in storage
*/
async getFileMetadata(filePath: string) {
try {
const { data, error } = await supabase.storage.from(BUCKET_NAME).info();
if (error) {
throw error;
}
return data;
} catch (error) {
console.error("Failed to get file metadata:", error);
throw error;
}
},
};
/**
* Get audio file duration in seconds
* @param file The audio file
* @returns Duration in seconds
*/
export async function getAudioDuration(file: File): Promise<number> {
return new Promise((resolve, reject) => {
const audioContext = new (window.AudioContext ||
(window as any).webkitAudioContext)();
const fileReader = new FileReader();
fileReader.onload = async (e) => {
try {
const arrayBuffer = e.target?.result as ArrayBuffer;
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
resolve(audioBuffer.duration);
} catch (error) {
reject(new Error("Failed to decode audio file"));
}
};
fileReader.onerror = () => {
reject(new Error("Failed to read audio file"));
};
fileReader.readAsArrayBuffer(file);
});
}