mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 02:01:10 +02:00
Projects included: - maerchenzauber (NestJS backend + Expo mobile + SvelteKit web + Astro landing) - manacore (Expo mobile + SvelteKit web + Astro landing) - manadeck (NestJS backend + Expo mobile + SvelteKit web) - memoro (Expo mobile + SvelteKit web + Astro landing) This commit preserves the current state before monorepo restructuring. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
190 lines
6.3 KiB
TypeScript
190 lines
6.3 KiB
TypeScript
import * as FileSystem from './fileSystemUtils';
|
|
import { supabaseAnonKey, supabaseUrl } from '../auth';
|
|
import { analyzeNetworkErrorSync } from '../errorHandling/utils/networkErrorUtils';
|
|
|
|
/**
|
|
* Enhanced upload result with network error information
|
|
*/
|
|
export interface UploadResult {
|
|
success: boolean;
|
|
filePath?: string;
|
|
error?: string;
|
|
isNetworkError?: boolean;
|
|
userMessage?: string;
|
|
technicalMessage?: string;
|
|
}
|
|
|
|
/**
|
|
* Service for cloud storage operations
|
|
*/
|
|
class CloudStorageService {
|
|
/**
|
|
* Validates that middleware and Supabase environments are consistent
|
|
* Throws an error if there's a mismatch (e.g., DEV middleware with PROD Supabase)
|
|
*/
|
|
private validateEnvironment(): void {
|
|
const middlewareUrl = process.env.EXPO_PUBLIC_MEMORO_MIDDLEWARE_URL || '';
|
|
|
|
// Determine environments based on URL patterns
|
|
const middlewareEnv = middlewareUrl.includes('-dev-') ? 'dev' : 'prod';
|
|
const supabaseEnv = supabaseUrl.includes('srinrsbpfeioudkntlyu') ? 'dev' : 'prod';
|
|
|
|
console.debug('Environment validation:', {
|
|
middlewareUrl,
|
|
supabaseUrl,
|
|
middlewareEnv,
|
|
supabaseEnv,
|
|
consistent: middlewareEnv === supabaseEnv
|
|
});
|
|
|
|
if (middlewareEnv !== supabaseEnv) {
|
|
const errorMsg = `CRITICAL: Environment mismatch detected!\n` +
|
|
`Middleware is ${middlewareEnv.toUpperCase()} but Supabase is ${supabaseEnv.toUpperCase()}.\n` +
|
|
`This will cause token validation failures.\n` +
|
|
`Please ensure both are pointing to the same environment in your .env file.`;
|
|
console.error(errorMsg);
|
|
throw new Error(errorMsg);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Uploads an audio file to Supabase Storage and creates a job for transcription
|
|
*/
|
|
async uploadAudioForProcessing({
|
|
userId,
|
|
filePath,
|
|
fileName,
|
|
}: any): Promise<UploadResult> {
|
|
try {
|
|
// Validate environment consistency before upload
|
|
this.validateEnvironment();
|
|
|
|
// Check if the file exists
|
|
const sourceFileInfo = await FileSystem.getFileInfo(filePath);
|
|
if (!sourceFileInfo.exists) {
|
|
throw new Error('Audio file does not exist');
|
|
}
|
|
|
|
// Create the path in storage
|
|
const storagePath = `${userId}/${fileName}`;
|
|
|
|
// For React Native, we use a direct approach with the file URI
|
|
const fileUri = filePath;
|
|
|
|
// Log file info for debugging
|
|
const fileInfo = await FileSystem.getFileInfo(fileUri);
|
|
console.debug('Preparing audio file for upload:', {
|
|
uri: fileUri,
|
|
name: fileName,
|
|
size: fileInfo.exists ? fileInfo.size : 0,
|
|
type: 'audio/m4a', // MIME type for M4A files
|
|
exists: fileInfo.exists
|
|
});
|
|
|
|
// Get the user's app token for authenticated requests with automatic refresh
|
|
const { tokenManager } = await import('~/features/auth/services/tokenManager');
|
|
const appToken = await tokenManager.getValidToken();
|
|
if (!appToken) {
|
|
throw new Error('No authenticated token found');
|
|
}
|
|
|
|
// Use the Supabase REST API directly for the upload
|
|
const uploadUrl = `${supabaseUrl}/storage/v1/object/user-uploads/${storagePath}`;
|
|
|
|
console.debug('Uploading file to Supabase Storage:', {
|
|
url: uploadUrl,
|
|
fileSize: fileInfo.exists && fileInfo.size ? this.formatFileSize(fileInfo.size) : 'unknown',
|
|
fileName,
|
|
hasToken: !!appToken,
|
|
tokenPreview: appToken ? `${appToken.substring(0, 20)}...` : 'none',
|
|
authHeader: `Bearer ${appToken.substring(0, 30)}...`
|
|
});
|
|
|
|
// Read file as binary data for upload
|
|
// React Native expects the file to be sent as a Blob or using FormData with proper URI
|
|
// We'll use fetch to read the file and then send it as binary
|
|
const fileBlob = await fetch(fileUri).then(r => r.blob());
|
|
|
|
// Create headers object with all required headers including Content-Type
|
|
const headers: Record<string, string> = {
|
|
'apikey': supabaseAnonKey,
|
|
'Authorization': `Bearer ${appToken}`,
|
|
'Content-Type': 'audio/m4a',
|
|
'x-upsert': 'true',
|
|
};
|
|
|
|
console.debug('Request headers being sent:', {
|
|
hasApikey: !!headers['apikey'],
|
|
hasAuthorization: !!headers['Authorization'],
|
|
hasContentType: !!headers['Content-Type'],
|
|
contentType: headers['Content-Type'],
|
|
authorizationPrefix: headers['Authorization']?.substring(0, 15)
|
|
});
|
|
|
|
const uploadResponse = await fetch(uploadUrl, {
|
|
method: 'POST',
|
|
headers: headers,
|
|
body: fileBlob,
|
|
});
|
|
|
|
if (!uploadResponse.ok) {
|
|
const errorText = await uploadResponse.text();
|
|
console.debug('Upload response error:', {
|
|
status: uploadResponse.status,
|
|
statusText: uploadResponse.statusText,
|
|
errorText
|
|
});
|
|
|
|
// Create an error object with status for network analysis
|
|
const uploadError = new Error(`Error during upload: ${errorText}`);
|
|
(uploadError as any).status = uploadResponse.status;
|
|
throw uploadError;
|
|
}
|
|
|
|
console.debug('Upload response successful:', {
|
|
status: uploadResponse.status,
|
|
statusText: uploadResponse.statusText
|
|
});
|
|
|
|
|
|
// Log successful upload
|
|
console.debug('File uploaded successfully to path:', storagePath);
|
|
|
|
return {
|
|
success: true,
|
|
filePath: storagePath
|
|
};
|
|
} catch (error: unknown) {
|
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
console.debug('Error during audio upload:', {
|
|
error: err.message,
|
|
stack: err.stack,
|
|
name: err.name
|
|
});
|
|
|
|
// Analyze if this is a network-related error
|
|
const networkErrorInfo = analyzeNetworkErrorSync(err);
|
|
|
|
return {
|
|
success: false,
|
|
error: err.message,
|
|
isNetworkError: networkErrorInfo.isNetworkError,
|
|
userMessage: networkErrorInfo.userMessage,
|
|
technicalMessage: networkErrorInfo.technicalMessage
|
|
};
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Helper function to format file size
|
|
*/
|
|
private formatFileSize(bytes: number): string {
|
|
if (bytes < 1024) return bytes + ' B';
|
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
|
|
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
export const cloudStorageService = new CloudStorageService();
|