tenantId(); $userId = $this->apiUserId(); // Check storage quota $tenant = Tenant::findOrFail($tenantId); $quotaCheck = $tenant->canUpload($file->getSize()); if (! $quotaCheck['allowed']) { throw new \Exception($quotaCheck['message']); } // Generate stored name $extension = $file->getClientOriginalExtension(); $storedName = self::generateStoredName($extension); // Build temp path: /tenants/{tenant_id}/temp/{year}/{month}/{stored_name} $date = now(); $tempPath = sprintf( '%d/temp/%s/%s/%s', $tenantId, $date->format('Y'), $date->format('m'), $storedName ); // Store file Storage::disk('tenant')->putFileAs( dirname($tempPath), $file, basename($tempPath) ); // Determine file type $mimeType = $file->getMimeType(); $fileType = $this->determineFileType($mimeType); // Create DB record $fileRecord = File::create([ 'tenant_id' => $tenantId, 'display_name' => $file->getClientOriginalName(), 'stored_name' => $storedName, 'file_path' => $tempPath, 'file_size' => $file->getSize(), 'mime_type' => $mimeType, 'file_type' => $fileType, 'is_temp' => true, 'folder_id' => null, 'description' => $description, 'uploaded_by' => $userId, 'created_by' => $userId, ]); // Increment tenant storage $tenant->incrementStorage($file->getSize()); return $fileRecord; } /** * Move files from temp to folder */ public function moveToFolder(array $fileIds, int $folderId, ?int $documentId = null, ?string $documentType = null): array { $tenantId = $this->tenantId(); $folder = Folder::where('tenant_id', $tenantId)->findOrFail($folderId); $movedFiles = []; DB::transaction(function () use ($fileIds, $folder, $documentId, $documentType, &$movedFiles) { foreach ($fileIds as $fileId) { $file = File::where('tenant_id', $this->tenantId()) ->where('is_temp', true) ->findOrFail($fileId); // Move file $file->moveToFolder($folder); // Update document reference if ($documentId && $documentType) { $file->update([ 'document_id' => $documentId, 'document_type' => $documentType, ]); } $movedFiles[] = $file->fresh(); } }); return $movedFiles; } /** * Get file by ID */ public function getFile(int $fileId): File { return File::where('tenant_id', $this->tenantId())->findOrFail($fileId); } /** * List files with filters */ public function listFiles(array $filters = []): \Illuminate\Pagination\LengthAwarePaginator { $query = File::where('tenant_id', $this->tenantId()) ->with(['folder', 'uploader']); // Filter by folder if (isset($filters['folder_id'])) { $query->where('folder_id', $filters['folder_id']); } // Filter by temp if (isset($filters['is_temp'])) { $query->where('is_temp', $filters['is_temp']); } // Filter by document if (isset($filters['document_id']) && isset($filters['document_type'])) { $query->forDocument($filters['document_id'], $filters['document_type']); } // Exclude soft deleted by default if (! isset($filters['with_trashed']) || ! $filters['with_trashed']) { $query->whereNull('deleted_at'); } return $query->orderBy('created_at', 'desc') ->paginate($filters['per_page'] ?? 20); } /** * Get trash files */ public function getTrash(): \Illuminate\Pagination\LengthAwarePaginator { return File::where('tenant_id', $this->tenantId()) ->onlyTrashed() // SoftDeletes: deleted_at IS NOT NULL인 항목만 ->with(['folder', 'uploader']) ->orderBy('deleted_at', 'desc') ->paginate(20); } /** * Soft delete file */ public function deleteFile(int $fileId): File { $userId = $this->apiUserId(); $file = $this->getFile($fileId); DB::transaction(function () use ($file, $userId) { // Soft delete $file->softDeleteFile($userId); // Log deletion DB::table('file_deletion_logs')->insert([ 'tenant_id' => $file->tenant_id, 'file_id' => $file->id, 'file_name' => $file->display_name, 'file_path' => $file->file_path, 'file_size' => $file->file_size, 'folder_id' => $file->folder_id, 'document_id' => $file->document_id, 'document_type' => $file->document_type, 'deleted_by' => $userId, 'deleted_at' => now(), 'deletion_type' => 'soft', ]); }); return $file->fresh(); } /** * Restore file from trash */ public function restoreFile(int $fileId): File { $file = File::where('tenant_id', $this->tenantId()) ->onlyTrashed() // SoftDeletes: 삭제된 항목만 조회 ->findOrFail($fileId); $file->restore(); $file->update(['deleted_by' => null]); return $file; } /** * Permanently delete file */ public function permanentDelete(int $fileId): void { $file = File::where('tenant_id', $this->tenantId()) ->onlyTrashed() // SoftDeletes: 삭제된 항목만 조회 ->findOrFail($fileId); DB::transaction(function () use ($file) { // Update deletion log DB::table('file_deletion_logs') ->where('file_id', $file->id) ->where('deletion_type', 'soft') ->update(['deletion_type' => 'permanent']); // Permanently delete $file->permanentDelete(); }); } /** * Create share link */ public function createShareLink(int $fileId, ?int $expiryHours = 24): FileShareLink { $file = $this->getFile($fileId); $userId = $this->apiUserId(); return FileShareLink::create([ 'file_id' => $file->id, 'tenant_id' => $file->tenant_id, 'expires_at' => now()->addHours($expiryHours), 'created_by' => $userId, ]); } /** * Get file by share token (no tenant context required) */ public static function getFileByShareToken(string $token): File { $link = FileShareLink::where('token', $token) ->valid() ->firstOrFail(); // Increment download count $link->incrementDownloadCount(request()->ip()); return $link->file; } /** * Get storage usage */ public function getStorageUsage(): array { $tenant = Tenant::findOrFail($this->tenantId()); // Folder usage $folderUsage = DB::table('files') ->where('tenant_id', $this->tenantId()) ->whereNull('deleted_at') ->whereNotNull('folder_id') ->selectRaw('folder_id, COUNT(*) as file_count, SUM(file_size) as total_size') ->groupBy('folder_id') ->get() ->map(function ($item) { $folder = Folder::find($item->folder_id); return [ 'folder_id' => $item->folder_id, 'folder_name' => $folder->folder_name ?? 'Unknown', 'file_count' => $item->file_count, 'total_size' => $item->total_size, 'formatted_size' => $this->formatBytes($item->total_size), ]; }); return [ 'storage_used' => $tenant->storage_used, 'storage_limit' => $tenant->storage_limit, 'storage_used_formatted' => $tenant->getStorageUsedFormatted(), 'storage_limit_formatted' => $tenant->getStorageLimitFormatted(), 'usage_percentage' => $tenant->getStorageUsagePercentage(), 'is_near_limit' => $tenant->isStorageNearLimit(), 'is_exceeded' => $tenant->isStorageExceeded(), 'grace_period_until' => $tenant->storage_grace_period_until, 'folder_usage' => $folderUsage, ]; } /** * Determine file type from MIME type */ private function determineFileType(string $mimeType): string { if (str_starts_with($mimeType, 'image/')) { return 'image'; } if (in_array($mimeType, [ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-excel', 'text/csv', ])) { return 'excel'; } if (in_array($mimeType, ['application/zip', 'application/x-rar-compressed'])) { return 'archive'; } return 'document'; } /** * Format bytes to human-readable */ private function formatBytes(int $bytes): string { $units = ['B', 'KB', 'MB', 'GB', 'TB']; $bytes = max($bytes, 0); $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); $pow = min($pow, count($units) - 1); $bytes /= (1 << (10 * $pow)); return round($bytes, 2).' '.$units[$pow]; } }