152 lines
3.5 KiB
TypeScript
152 lines
3.5 KiB
TypeScript
|
|
import api from './api';
|
||
|
|
|
||
|
|
export interface UploadedFile {
|
||
|
|
id: string;
|
||
|
|
name: string;
|
||
|
|
size: number;
|
||
|
|
type: string;
|
||
|
|
url: string;
|
||
|
|
uploadedAt: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface UploadProgress {
|
||
|
|
loaded: number;
|
||
|
|
total: number;
|
||
|
|
percentage: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface PresignedUrlResponse {
|
||
|
|
uploadUrl: string;
|
||
|
|
publicUrl: string;
|
||
|
|
key: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
type UploadFolder = 'avatars' | 'documents' | 'properties' | 'verification';
|
||
|
|
|
||
|
|
class UploadService {
|
||
|
|
private basePath = '/upload';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get presigned URL from backend
|
||
|
|
*/
|
||
|
|
private async getPresignedUrl(
|
||
|
|
filename: string,
|
||
|
|
contentType: string,
|
||
|
|
folder: UploadFolder = 'documents'
|
||
|
|
): Promise<PresignedUrlResponse> {
|
||
|
|
const response = await api.post<{ data: PresignedUrlResponse }>(
|
||
|
|
`${this.basePath}/presigned-url`,
|
||
|
|
{
|
||
|
|
filename,
|
||
|
|
contentType,
|
||
|
|
folder,
|
||
|
|
}
|
||
|
|
);
|
||
|
|
|
||
|
|
return response.data.data;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Upload file directly to S3 using presigned URL
|
||
|
|
*/
|
||
|
|
private async uploadToS3(
|
||
|
|
uploadUrl: string,
|
||
|
|
file: File,
|
||
|
|
onProgress?: (progress: UploadProgress) => void
|
||
|
|
): Promise<void> {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
const xhr = new XMLHttpRequest();
|
||
|
|
|
||
|
|
xhr.upload.addEventListener('progress', (event) => {
|
||
|
|
if (event.lengthComputable && onProgress) {
|
||
|
|
const percentage = Math.round((event.loaded * 100) / event.total);
|
||
|
|
onProgress({
|
||
|
|
loaded: event.loaded,
|
||
|
|
total: event.total,
|
||
|
|
percentage,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
xhr.addEventListener('load', () => {
|
||
|
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||
|
|
resolve();
|
||
|
|
} else {
|
||
|
|
reject(new Error(`Upload failed with status ${xhr.status}`));
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
xhr.addEventListener('error', () => {
|
||
|
|
reject(new Error('Upload failed'));
|
||
|
|
});
|
||
|
|
|
||
|
|
xhr.open('PUT', uploadUrl);
|
||
|
|
xhr.setRequestHeader('Content-Type', file.type);
|
||
|
|
xhr.send(file);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Upload a single file to S3
|
||
|
|
*/
|
||
|
|
async uploadFile(
|
||
|
|
file: File,
|
||
|
|
folder: UploadFolder = 'documents',
|
||
|
|
onProgress?: (progress: UploadProgress) => void
|
||
|
|
): Promise<UploadedFile> {
|
||
|
|
// Step 1: Get presigned URL from backend
|
||
|
|
const { uploadUrl, publicUrl, key } = await this.getPresignedUrl(
|
||
|
|
file.name,
|
||
|
|
file.type,
|
||
|
|
folder
|
||
|
|
);
|
||
|
|
|
||
|
|
// Step 2: Upload directly to S3
|
||
|
|
await this.uploadToS3(uploadUrl, file, onProgress);
|
||
|
|
|
||
|
|
// Step 3: Return uploaded file info
|
||
|
|
return {
|
||
|
|
id: key, // Use the S3 key as the ID
|
||
|
|
name: file.name,
|
||
|
|
size: file.size,
|
||
|
|
type: file.type,
|
||
|
|
url: publicUrl,
|
||
|
|
uploadedAt: new Date().toISOString(),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Upload multiple files to S3
|
||
|
|
*/
|
||
|
|
async uploadFiles(
|
||
|
|
files: File[],
|
||
|
|
folder: UploadFolder = 'documents',
|
||
|
|
onProgress?: (fileIndex: number, progress: UploadProgress) => void
|
||
|
|
): Promise<UploadedFile[]> {
|
||
|
|
const uploadedFiles: UploadedFile[] = [];
|
||
|
|
|
||
|
|
for (let i = 0; i < files.length; i++) {
|
||
|
|
const file = files[i];
|
||
|
|
const uploadedFile = await this.uploadFile(
|
||
|
|
file,
|
||
|
|
folder,
|
||
|
|
onProgress ? (progress) => onProgress(i, progress) : undefined
|
||
|
|
);
|
||
|
|
uploadedFiles.push(uploadedFile);
|
||
|
|
}
|
||
|
|
|
||
|
|
return uploadedFiles;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Delete a file from S3
|
||
|
|
*/
|
||
|
|
async deleteFile(fileKey: string): Promise<void> {
|
||
|
|
// The key needs to be URL encoded for path parameter
|
||
|
|
const encodedKey = encodeURIComponent(fileKey);
|
||
|
|
await api.delete(`${this.basePath}/${encodedKey}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export const uploadService = new UploadService();
|