lms/app/Services/LocalFileUploadService.php
2025-12-15 12:26:23 +01:00

143 lines
4.1 KiB
PHP

<?php
namespace App\Services;
use App\Models\ChunkedUpload;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Log;
class LocalFileUploadService
{
protected string $disk;
public function __construct()
{
$this->disk = 'public';
}
public function initiateUpload(string $filename, string $mimeType, int $fileSize, int $userId, array $metadata = [])
{
$extension = pathinfo($filename, PATHINFO_EXTENSION);
$filePath = 'lessons/' . Str::uuid() . '.' . $extension;
// Create directory if it doesn't exist
Storage::disk($this->disk)->makeDirectory(dirname($filePath));
$uploadRecord = ChunkedUpload::create([
'user_id' => $userId,
'filename' => $filePath,
'original_filename' => $filename,
'file_path' => $filePath,
'disk' => $this->disk,
'mime_type' => $mimeType,
'size' => $fileSize,
'upload_id' => (string) Str::uuid(),
'key' => $filePath,
'status' => 'initialized',
'chunks_completed' => 0,
'total_chunks' => 0,
'metadata' => $metadata,
]);
return $uploadRecord;
}
public function uploadPart($upload, int $partNumber, string $partContent): array
{
$chunkPath = $upload->key . '.part' . $partNumber;
Storage::disk($this->disk)->put($chunkPath, $partContent);
$upload->increment('chunks_completed');
return [
'PartNumber' => $partNumber,
'ETag' => md5($partContent),
// Don't include chunk_path in the response as it can be reconstructed
];
}
public function completeUpload($upload, array $parts): bool
{
try {
// Sort parts by part number
usort($parts, fn($a, $b) => $a['PartNumber'] <=> $b['PartNumber']);
// Combine all chunks
$finalPath = $upload->key;
$finalHandle = fopen(Storage::disk($this->disk)->path($finalPath), 'ab');
foreach ($parts as $part) {
// Handle both cases: when chunk_path is provided or needs to be constructed
$chunkPath = $part['chunk_path'] ?? ($upload->key . '.part' . $part['PartNumber']);
if (!Storage::disk($this->disk)->exists($chunkPath)) {
Log::error("Chunk file not found: " . $chunkPath);
continue;
}
$chunkContent = Storage::disk($this->disk)->get($chunkPath);
fwrite($finalHandle, $chunkContent);
Storage::disk($this->disk)->delete($chunkPath);
}
fclose($finalHandle);
// Generate the local file URL
$fileUrl = asset('storage/' . $upload->key);
$upload->update([
'status' => 'completed',
'file_url' => $fileUrl,
]);
return true;
} catch (\Exception $e) {
Log::error('Local file upload completion error: ' . $e->getMessage());
$upload->update(['status' => 'failed']);
throw $e;
}
}
public function abortUpload($upload): bool
{
try {
// Delete any partial chunks
$chunks = Storage::disk($this->disk)->files(dirname($upload->key));
foreach ($chunks as $chunk) {
if (str_starts_with($chunk, $upload->key . '.part')) {
Storage::disk($this->disk)->delete($chunk);
}
}
// Delete the final file if it exists
if (Storage::disk($this->disk)->exists($upload->key)) {
Storage::disk($this->disk)->delete($upload->key);
}
$upload->update(['status' => 'aborted']);
return true;
} catch (\Exception $e) {
Log::error('Local upload abort error: ' . $e->getMessage());
return false;
}
}
public function deleteFile(ChunkedUpload $upload): bool
{
// Clean up any remaining chunks
$this->abortUpload($upload);
if (Storage::disk($this->disk)->exists($upload->key)) {
Storage::disk($this->disk)->delete($upload->key);
Storage::disk($this->disk)->deleteDirectory(dirname($upload->key));
$upload->delete();
return true;
}
return false;
}
}