diff --git a/app/Http/Controllers/Apis/Protected/Main/OAuth2ChunkedFilesApiController.php b/app/Http/Controllers/Apis/Protected/Main/OAuth2ChunkedFilesApiController.php index b30e80ced..ecf92cc34 100644 --- a/app/Http/Controllers/Apis/Protected/Main/OAuth2ChunkedFilesApiController.php +++ b/app/Http/Controllers/Apis/Protected/Main/OAuth2ChunkedFilesApiController.php @@ -12,8 +12,10 @@ * limitations under the License. **/ use Illuminate\Http\JsonResponse; +use Illuminate\Http\Response; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Storage; +use OpenApi\Attributes as OA; use Pion\Laravel\ChunkUpload\Exceptions\UploadMissingFileException; use Pion\Laravel\ChunkUpload\Handler\AbstractHandler; use Pion\Laravel\ChunkUpload\Receiver\FileReceiver; @@ -33,6 +35,63 @@ class OAuth2ChunkedFilesApiController extends UploadController * @throws UploadMissingFileException * */ + #[OA\Post( + path: "/api/public/v1/files/upload", + description: "Upload files using chunked upload mechanism. Supports large file uploads by splitting them into smaller chunks. The endpoint handles both complete uploads and chunked progress updates. Files are organized by MIME type and date in the storage system.", + summary: 'Upload file with chunked upload support', + operationId: 'uploadChunkedFile', + tags: ['Files'], + requestBody: new OA\RequestBody( + required: true, + content: new OA\MediaType( + mediaType: 'multipart/form-data', + schema: new OA\Schema( + required: ['file'], + properties: [ + new OA\Property( + property: 'file', + type: 'string', + format: 'binary', + description: 'File to upload (can be a chunk of a larger file)' + ), + new OA\Property( + property: 'resumableChunkNumber', + type: 'integer', + description: 'Current chunk number (for resumable.js library compatibility)', + example: 1 + ), + new OA\Property( + property: 'resumableTotalChunks', + type: 'integer', + description: 'Total number of chunks (for resumable.js library compatibility)', + example: 5 + ), + new OA\Property( + property: 'resumableIdentifier', + type: 'string', + description: 'Unique identifier for the file upload session (for resumable.js library compatibility)', + example: '12345-myfile-jpg' + ), + ] + ) + ) + ), + responses: [ + new OA\Response( + response: 200, + description: 'Success - Upload in progress (chunk uploaded successfully)', + content: new OA\JsonContent(ref: '#/components/schemas/ChunkedFileUploadProgressResponse') + ), + new OA\Response( + response: 201, + description: 'Success - Upload complete (all chunks received and file saved)', + content: new OA\JsonContent(ref: '#/components/schemas/ChunkedFileUploadCompleteResponse') + ), + new OA\Response(response: Response::HTTP_BAD_REQUEST, description: "Bad Request - Invalid file or missing required parameters"), + new OA\Response(response: Response::HTTP_UNPROCESSABLE_ENTITY, description: "Unprocessable Entity - Upload missing file exception"), + new OA\Response(response: Response::HTTP_INTERNAL_SERVER_ERROR, description: "Server Error - Upload failed or storage error") + ] + )] public function uploadFile(FileReceiver $receiver) { // check if the upload is success, throw exception or return response you need @@ -57,4 +116,4 @@ public function uploadFile(FileReceiver $receiver) ]); } -} \ No newline at end of file +} diff --git a/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php b/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php index 4a3068c54..7b519c86e 100644 --- a/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php +++ b/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php @@ -743,6 +743,7 @@ public function getAllByPage(PagingInfo $paging_info, Filter $filter = null, Ord $start = time(); Log::debug(sprintf('DoctrineSummitAttendeeTicketRepository::getAllByPage')); $total = $this->getFastCount($filter, $order); + if(!$total) return new PagingResponse(0, $paging_info->getPerPage(), $paging_info->getCurrentPage(), 0, []); $ids = $this->getAllIdsByPage($paging_info, $filter, $order); $query = $this->getEntityManager()->createQueryBuilder() ->select('e, a, o, tt, pc, b, bt, a_c, m') @@ -762,6 +763,10 @@ public function getAllByPage(PagingInfo $paging_info, Filter $filter = null, Ord $byId = []; foreach ($rows as $e) $byId[$e->getId()] = $e; + $rows = $query->getQuery()->getResult(); + $byId = []; + foreach ($rows as $e) $byId[$e->getId()] = $e; + $data = []; foreach ($ids as $id) { if (isset($byId[$id])) $data[] = $byId[$id]; diff --git a/app/Repositories/Summit/DoctrineSummitEventRepository.php b/app/Repositories/Summit/DoctrineSummitEventRepository.php index c5607d575..96feaaa6a 100644 --- a/app/Repositories/Summit/DoctrineSummitEventRepository.php +++ b/app/Repositories/Summit/DoctrineSummitEventRepository.php @@ -98,20 +98,12 @@ protected function getBaseEntity() return SummitEvent::class; } - /** - * @param PagingInfo $paging_info - * @param Filter|null $filter - * @param Order|null $order - * @return PagingResponse - */ - public function getAllByPage(PagingInfo $paging_info, Filter $filter = null, Order $order = null) - { + private function prepareRegularQuery($query, Filter $filter = null, Order $order = null){ $current_track_id = 0; $current_member_id = 0; if (!is_null($filter)) { - Log::debug(sprintf("DoctrineSummitEventRepository::getAllByPage filter %s", $filter)); // check for dependant filtering $track_id_filter = $filter->getUniqueFilter('track_id'); if (!is_null($track_id_filter)) { @@ -122,12 +114,7 @@ public function getAllByPage(PagingInfo $paging_info, Filter $filter = null, Ord $current_member_id = intval($current_member_id_filter->getValue()); } } - - $query = $this->getEntityManager()->createQueryBuilder() - ->select("e") - ->distinct(true) - ->from($this->getBaseEntity(), "e") - ->leftJoin(Presentation::class, 'p', 'WITH', 'e.id = p.id'); + $query = $query->leftJoin(Presentation::class, 'p', 'WITH', 'e.id = p.id'); if (is_null($order) || !$order->hasOrder("votes_count")) { $query = $query @@ -167,12 +154,19 @@ public function getAllByPage(PagingInfo $paging_info, Filter $filter = null, Ord } } - $shouldPerformRandomOrderingByPage = false; - if (!is_null($order)) { - if ($order->hasOrder("page_random")) { - $shouldPerformRandomOrderingByPage = true; - $order->removeOrder("page_random"); + $can_view_private_events = self::isCurrentMemberOnGroup(IGroup::SummitAdministrators); + + if (!$can_view_private_events) { + $idx = 1; + foreach (self::$forbidden_classes as $forbidden_class) { + $query = $query + ->andWhere("not e INSTANCE OF :forbidden_class" . $idx); + $query->setParameter("forbidden_class" . $idx, $forbidden_class); + $idx++; } + } + + if (!is_null($order)) { $order->apply2Query($query, $this->getOrderMappings()); if (!$order->hasOrder('id')) { $query = $query->addOrderBy("e.id", 'ASC'); @@ -184,28 +178,90 @@ public function getAllByPage(PagingInfo $paging_info, Filter $filter = null, Ord $query = $query->addOrderBy("e.id", 'ASC'); } - $can_view_private_events = self::isCurrentMemberOnGroup(IGroup::SummitAdministrators); + return $query; + } + /** + * @param Filter|null $filter + * @param Order|null $order + * @return int + */ + public function getFastCount(Filter $filter = null, Order $order = null){ - if (!$can_view_private_events) { - $idx = 1; - foreach (self::$forbidden_classes as $forbidden_class) { - $query = $query - ->andWhere("not e INSTANCE OF :forbidden_class" . $idx); - $query->setParameter("forbidden_class" . $idx, $forbidden_class); - $idx++; - } - } + $query = $this->getEntityManager() + ->createQueryBuilder() + ->select('COUNT(DISTINCT e.id)') + ->from($this->getBaseEntity(), "e") + ->distinct(false); + + $query = $this->prepareRegularQuery($query, $filter, null); + + return (int) $query->getQuery()->getSingleScalarResult(); + } + + /** + * @param PagingInfo $paging_info + * @param Filter|null $filter + * @param Order|null $order + * @return array + */ + public function getAllIdsByPage(PagingInfo $paging_info, Filter $filter = null, Order $order = null):array { + + $query = $this->getEntityManager() + ->createQueryBuilder() + ->distinct(true) + ->select("e.id") + ->from($this->getBaseEntity(), "e"); + + $query = $this->prepareRegularQuery($query, $filter, $order); $query = $query ->setFirstResult($paging_info->getOffset()) ->setMaxResults($paging_info->getPerPage()); - $paginator = new Paginator($query, $fetchJoinCollection = true); - $total = $paginator->count(); + $res = $query->getQuery()->getArrayResult(); + return array_column($res, 'id'); + } + /** + * @param PagingInfo $paging_info + * @param Filter|null $filter + * @param Order|null $order + * @return PagingResponse + */ + public function getAllByPage(PagingInfo $paging_info, Filter $filter = null, Order $order = null) + { + + $start = time(); + $shouldPerformRandomOrderingByPage = false; + if (!is_null($order)) { + if ($order->hasOrder("page_random")) { + $shouldPerformRandomOrderingByPage = true; + $order->removeOrder("page_random"); + } + } + Log::debug(sprintf('DoctrineSummitEventRepository::getAllByPage')); + $total = $this->getFastCount($filter, null); + if(!$total) return new PagingResponse(0, $paging_info->getPerPage(), $paging_info->getCurrentPage(), 0, []); + $ids = $this->getAllIdsByPage($paging_info, $filter, $order); + + $query = $this->getEntityManager()->createQueryBuilder() + ->select("e") + ->from($this->getBaseEntity(), "e"); + + $query = $this->prepareRegularQuery($query, $filter, $order) + ->andWhere('e.id IN (:ids)') + ->setParameter('ids', $ids); + + $rows = $query->getQuery()->getResult(); + $byId = []; + foreach ($rows as $e) $byId[$e->getId()] = $e; + $data = []; + foreach ($ids as $id) { + if (isset($byId[$id])) $data[] = $byId[$id]; + } - foreach ($paginator as $entity) - $data[] = $entity; + $end = time() - $start; + Log::debug(sprintf('DoctrineSummitEventRepository::getAllByPage %s seconds', $end)); if ($shouldPerformRandomOrderingByPage) shuffle($data);