diff --git a/README.md b/README.md index 47343b79..e2a77e59 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,8 @@ First, clone the repository from GitHub using the following command: git clone https://github.com/XIMDEX/xdam.git ``` -## 3. Environment Configuration + +## 2. Environment Configuration Create and configure the .env file. To do this, first go to the backend folder. You can use the .env.example file as a base: ```bash cd backend @@ -36,6 +37,14 @@ cp .env.example .env nano .env ``` +## 3. Install Dependencies + +To install the necessary dependencies, run the following command: + +```bash +composer install +``` + ## 4. Database Migration After setting up your environment variables in the .env file, you need to run database migrations. This step creates the necessary tables in your database according to the defined schema. @@ -69,12 +78,6 @@ sudo apt install jpegoptim optipng pngquant gifsicle ffmpeg sudo npm install -g svgo ``` -Then, navigate to the backend directory and install Composer dependencies: -```bash -cd backend -composer install -php artisan migrate -``` ## 7. Final Configuration Steps @@ -84,4 +87,5 @@ Finally, execute the following commands: sudo php artisan solrCores:maintenance --action=ALL sudo php artisan optimize:clear sudo php artisan passport:install +php artisan db:seed ``` diff --git a/backend/.env.example b/backend/.env.example index f629f7b8..5d579bd0 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -61,6 +61,7 @@ SOLR_SCHEMAS_FOLDER="solr_schemas" SOLR_CORES_VERSION="" # This value will be an integer (2, 3, 4, etc.) # surrounded by "". If it's empty, it will take "" # by default. +SOLR_THUMBNAIL="false" # Allow to save thumbnails on solr. ##### VIDEO TOOLS ##### ### CAN BE OBTAINED EXECUTING which ffprobe and wich ffmpeg. THESE ARE REQUIRED @@ -107,4 +108,4 @@ DAM_FRONT_URL=http://localhost:3000 ############## SCORM ####################### SCORM_URL=https://scorm.mhe.ximdex.net/ SCORM_VERSION='' -SCORM_TOKEN='token_scorm' \ No newline at end of file +SCORM_TOKEN='token_scorm' diff --git a/backend/app/Http/Controllers/ResourceController.php b/backend/app/Http/Controllers/ResourceController.php index 398fbb89..79d61429 100755 --- a/backend/app/Http/Controllers/ResourceController.php +++ b/backend/app/Http/Controllers/ResourceController.php @@ -37,11 +37,14 @@ use App\Enums\AccessPermission; use App\Models\Copy; use App\Services\ExternalApis\ScormService; - +use App\Services\RenderService; use Illuminate\Support\Facades\Storage; use Symfony\Component\HttpFoundation\StreamedResponse; use Carbon\Carbon; +use Intervention\Image\Facades\Image; + + class ResourceController extends Controller { @@ -69,12 +72,17 @@ class ResourceController extends Controller * @var UserService */ private $userService; - + /** * @var ScormService */ private $scormService; + /** + * @var RenderService + */ + private $renderService; + /** * CategoryController constructor. * @param ResourceService $resourceService @@ -84,30 +92,36 @@ class ResourceController extends Controller * @param UserService $userService * @param ScormService $scormService */ - public function __construct(ResourceService $resourceService, MediaService $mediaService, - CDNService $cdnService, WorkspaceService $workspaceService, - UserService $userService, ScormService $scormService) - { + public function __construct( + ResourceService $resourceService, + MediaService $mediaService, + CDNService $cdnService, + WorkspaceService $workspaceService, + UserService $userService, + ScormService $scormService, + RenderService $renderService + ) { $this->resourceService = $resourceService; $this->mediaService = $mediaService; $this->cdnService = $cdnService; $this->workspaceService = $workspaceService; $this->userService = $userService; $this->scormService = $scormService; + $this->renderService = $renderService; } - public function resourcesSchema () + public function resourcesSchema() { $schemas = $this->resourceService->resourcesSchema(); return response()->json($schemas); } - public function lomesSchema () + public function lomesSchema() { return response()->json($this->resourceService->lomesSchema()); } - public function lomSchema () + public function lomSchema() { return response()->json($this->resourceService->lomSchema()); } @@ -179,12 +193,13 @@ public function update(DamResource $damResource, UpdateResourceRequest $request) * @param UpdateResourceRequest $request * @return \Illuminate\Http\JsonResponse|object */ - public function updateFromXeval(string $xevalId,UpdateResourceRequest $request){ - $damResource =$resource = DamResource::whereJsonContains('data->description', ['xeval_id' => $xevalId])->first(); - $resource = $this->resourceService->updateFromXeval( $damResource, $request->all()); + public function updateFromXeval(string $xevalId, UpdateResourceRequest $request) + { + $damResource = $resource = DamResource::whereJsonContains('data->description', ['xeval_id' => $xevalId])->first(); + $resource = $this->resourceService->updateFromXeval($damResource, $request->all()); return (new ResourceResource($resource)) - ->response() - ->setStatusCode(Response::HTTP_OK); + ->response() + ->setStatusCode(Response::HTTP_OK); } /** @@ -198,12 +213,13 @@ public function store(StoreResourceRequest $request) ->setStatusCode(Response::HTTP_OK); } - public function duplicate(DamResource $damResource){ + public function duplicate(DamResource $damResource) + { $duplicated = false; try { $duplicatedResource = $this->resourceService->duplicateResource($damResource); - $this->resourceService->processDuplicateExtraData($duplicatedResource,$this->resourceService->getLomData($damResource),"lom" ); - $this->resourceService->processDuplicateExtraData($duplicatedResource,$this->resourceService->getLomesData($damResource),"lomes" ); + $this->resourceService->processDuplicateExtraData($duplicatedResource, $this->resourceService->getLomData($damResource), "lom"); + $this->resourceService->processDuplicateExtraData($duplicatedResource, $this->resourceService->getLomesData($damResource), "lomes"); $duplicated = true; $this->scormService->cloneBook($duplicatedResource->id); } catch (\Exception $exc) { @@ -221,13 +237,15 @@ public function duplicate(DamResource $damResource){ ->setStatusCode(Response::HTTP_OK); } - public function copyStatus($copy,Request $request ){ - $resource = $this->resourceService->duplicateUpdateStatus($copy,$request->all()); + public function copyStatus($copy, Request $request) + { + $resource = $this->resourceService->duplicateUpdateStatus($copy, $request->all()); return response(new JsonResource($resource)) ->setStatusCode(Response::HTTP_OK); } - public function copyGetStatus($copy){ + public function copyGetStatus($copy) + { $resource = $this->resourceService->getCopy($copy); return response(new JsonResource($resource)) ->setStatusCode(Response::HTTP_OK); @@ -241,7 +259,8 @@ public function storeBatch(Request $request) ->setStatusCode(Response::HTTP_OK); } - public function setLomesData(DamResource $damResource, Request $request) { + public function setLomesData(DamResource $damResource, Request $request) + { $resource = $this->resourceService->setLomesData($damResource, $request); return (new JsonResource($resource)) ->response() @@ -256,7 +275,8 @@ public function setLomData(DamResource $damResource, Request $request) ->setStatusCode(Response::HTTP_OK); } - public function getLomesData(DamResource $damResource) { + public function getLomesData(DamResource $damResource) + { $resource = $this->resourceService->getLomesData($damResource); return (new JsonResource($resource)) ->response() @@ -351,7 +371,7 @@ public function setTags(DamResource $damResource, setTagsRequest $request) public function addCategory(DamResource $damResource, Category $category) { $is_course = ($damResource->type === 'course'); - $resource = $is_course ? $this->resourceService->setOnlyOneCategoryTo($damResource, $category) :$this->resourceService->addCategoryTo($damResource, $category); + $resource = $is_course ? $this->resourceService->setOnlyOneCategoryTo($damResource, $category) : $this->resourceService->addCategoryTo($damResource, $category); return (new ResourceResource($resource)) ->response() ->setStatusCode(Response::HTTP_OK); @@ -377,110 +397,91 @@ public function deleteCategory(DamResource $damResource, Category $category) * @return \Symfony\Component\HttpFoundation\BinaryFileResponse * @throws \Exception */ - public function render($damUrl, $size = 'default') + public function render(Request $request, $damUrl, $size = 'default') { $mediaId = DamUrlUtil::decodeUrl($damUrl); - if (Cache::has("{$mediaId}__{$size}")) { - $response = Cache::get("{$mediaId}__$size"); - if (is_string($response)) { - Cache::delete("{$mediaId}__$size"); - } - } + //if (Cache::has("{$mediaId}__{$size}")) { + // $response = Cache::get("{$mediaId}__$size"); + // if (is_string($response)) { + // Cache::delete("{$mediaId}__$size"); + // } + //} $method = request()->method(); - return $this->renderResource($mediaId, $method, $size, null, false); + return $this->renderResource($request, $mediaId, $method, $size, null, false); } - private function renderResource($mediaId, $method = null, $size = null, $renderKey = null, $isCDN = false, $can_download = false) + private function renderResource(Request $request, $mediaId, $method = null, $size = null, $renderKey = null, $isCDN = false, $can_download = false) { $media = Media::findOrFail($mediaId); - $mediaFileName = explode('/', $media->getPath()); - $mediaFileName = $mediaFileName[count($mediaFileName) - 1]; - $size = ($size === null ? 'default' : $size); - if ($size === 'raw') $size = 'default'; + $absolutePath = $media->getPath(); + $mediaFileName = pathinfo($absolutePath, PATHINFO_FILENAME); + $path = explode(Storage::path(''), $absolutePath)[1]; + $originalSize = Storage::size($path); //file weight + $size = ($size === null ? 'default' : $size); //image variant $mimeType = $media->mime_type; $fileType = explode('/', $mimeType)[0]; + $response = null; if ($fileType == 'image' && $size === 'default') { - $path = explode('storage/app/', $media->getPath())[1]; - if (!Storage::exists($path)) { - abort(404); + + if ($this->renderService->checkAvif($request)) { // does the browser allow AVIF ? + $avifPath = $this->renderService->getConvertedPath($path, "avif"); + if (!$this->renderService->checkIfImageExists($avifPath)) { + $this->renderService->generateAvif($path); + $avifSize = Storage::size($avifPath); + //$this->renderService->logAvifConversion($mediaFileName, $originalSize, $avifSize); + } + //$file = Storage::get($avifPath); + $fileVariantPath = $avifPath; + $type = 'image/avif'; + } else { + // if it weights under 2 Mbytes returns original and do not create variant + if ($originalSize < 2000000) { + //$file = Storage::get($path); + $fileVariantPath = $path; + $type = $mimeType; + } else { + list($fileVariantPath, $type) = $this->handleSizedImageRendering($path, $media, $fileType, 'medium', $mimeType); + } } - $file = Storage::get($path); - $type = $mimeType; + $lastModified = Storage::lastModified($fileVariantPath); + $streamedResponse = $this->createStreamedResponse($fileVariantPath, $type, $mediaFileName); + //$cachedResponse = $this->createCachedResponse($file, $streamedResponse); + //Cache::put("{$mediaId}__$size", $cachedResponse); + $response = $streamedResponse; + + } else if ($fileType == 'image' && $size === 'raw') { + + //$file = Storage::get($path); $lastModified = Storage::lastModified($path); - - $response = new StreamedResponse(); - $response->setCallback(function () use ($file) { - echo $file; - }); - - $response->headers->set('Content-Type', $type); - $response->headers->set('Content-Length', strlen($file)); - $response->headers->set('Content-Disposition', sprintf('inline; filename="%s"', $mediaFileName)); - - $maxAge = 3600 * 24 * 7; - $response->headers->set('Cache-Control', 'public, max-age=' . $maxAge . ', immutable'); - $response->headers->set('Expires', gmdate('D, d M Y H:i:s', time() + $maxAge) . ' GMT'); - - $response_cache = new Response( - $file, - Response::HTTP_OK, - $response->headers->all() - ); - - Cache::put("{$mediaId}__$size", $response_cache); - return $response; - } - - if ($fileType == 'video' || $fileType == 'image') { + $streamedResponse = $this->createStreamedResponse($path, $mimeType, $mediaFileName); + //$cachedResponse = $this->createCachedResponse($file, $streamedResponse); + //Cache::put("{$mediaId}__$size", $cachedResponse); + $response = $streamedResponse; + + } else if ($fileType == 'image' && $size) { + + list($fileVariantPath, $type) = $this->handleSizedImageRendering($path, $media, $fileType, $size, $mimeType); + $lastModified = Storage::lastModified($path); + $streamedResponse = $this->createStreamedResponse($fileVariantPath, $type, $mediaFileName); + //$cachedResponse = $this->createCachedResponse($file, $streamedResponse); + $response = $streamedResponse; + + } else if ($fileType == 'video') { + $sizeValue = $this->getResourceSize($fileType, $size); $availableSizes = $this->getAvailableResourceSizes(); - /** - * @var \Intervention\Image\Image $compressed - */ + $compressed = $this->mediaService->preview($media, $availableSizes[$fileType], $size, $sizeValue); - - if ($fileType == 'image' || ($fileType == 'video' && in_array($size, ['medium', 'small', 'thumbnail']))) { - $response = response()->file($compressed->basePath()); - $response->headers->set('Content-Disposition', sprintf('inline; filename="%s"', $mediaFileName)); - - $maxAge = 3600 * 24 * 7; - $response->headers->set('Cache-Control', 'public, max-age=' . $maxAge . ', immutable'); - $response->headers->set('Expires', gmdate('D, d M Y H:i:s', time() + $maxAge) . ' GMT'); - - // Añadir ETag - $etag = md5_file($compressed->basePath()); - $response->setEtag($etag); - - // Añadir Last-Modified - $lastModified = filemtime($compressed->basePath()); - $response->setLastModified(Carbon::createFromTimestamp($lastModified)); - - if ($fileType == 'image') { - $response_cache = $compressed->response('jpeg', $availableSizes[$fileType]['sizes'][$size] === 'raw' ? 100 : $availableSizes[$fileType]['qualities'][$size]); - - $response_cache->headers->set('Content-Disposition', sprintf('inline; filename="%s"', $mediaFileName)); - - $maxAge = 3600 * 24 * 7; - $response_cache->headers->set('Cache-Control', 'public, max-age=' . $maxAge . ', immutable'); - $response_cache->headers->set('Expires', gmdate('D, d M Y H:i:s', time() + $maxAge) . ' GMT'); - - // Añadir ETag - $etag = md5_file($compressed->basePath()); - $response_cache->setEtag($etag); - - // Añadir Last-Modified - $lastModified = filemtime($compressed->basePath()); - $response_cache->setLastModified(Carbon::createFromTimestamp($lastModified)); - Cache::put("{$mediaId}__$size", $response_cache); - } - return $response; + if (in_array($size, ['medium', 'small', 'thumbnail'])) { + $response = $this->handleImageAndVideoResponse($fileType, $size, $compressed, $mediaFileName, $mediaId, $availableSizes); + }else{ + $response = response()->file($compressed); } - return response()->file($compressed); } else if ($mimeType == 'application/pdf' && $renderKey == null && $isCDN) { $route = Route::getCurrentRoute(); $routeParams = $route->parameters(); @@ -493,7 +494,7 @@ private function renderResource($mediaId, $method = null, $size = null, $renderK 'url' => base64_encode($url . '?key=' . $key . '&dx=' . ($can_download ? 1 : 0)) ]); } else if ($mimeType == 'application/pdf' && $renderKey != null && $isCDN) { - + if ($this->mediaService->checkRendererKey($renderKey, $method)) { // file lo hace con streaming de datos // $response = response()->file($this->mediaService->preview($media, [])); @@ -502,26 +503,31 @@ private function renderResource($mediaId, $method = null, $size = null, $renderK $response->headers->set('Cache-Control', 'no-cache, no-store, must-revalidate'); $response->headers->set('Pragma', 'no-cache'); $response->headers->set('Expires', '0'); - return $response; } else { return response(['error' => 'Error! You don\'t have permission to view this file.'], Response::HTTP_BAD_REQUEST); } - } - if ($fileType !== 'audio' && !$can_download) { + } + if ($fileType !== 'audio' && !$can_download && !$response ) { return response(['error' => 'Error! You don\'t have permission to download this file.'], Response::HTTP_BAD_REQUEST); } - return response()->file($this->mediaService->preview($media, [])); + if (!$response) { + return response()->file($this->mediaService->preview($media, [])); + } + return $response; + } + private function getAvailableResourceSizes() { $sizes = [ 'image' => [ - 'allowed_sizes' => ['thumbnail', 'small', 'medium', 'raw', 'default'], + 'allowed_sizes' => ['thumbnail', 'small', 'medium', 'large', 'raw', 'default'], 'sizes' => [ 'thumbnail' => array('width' => 256, 'height' => 144), 'small' => array('width' => 426, 'height' => 240), - 'medium' => array('width' => 854, 'height' => 480), + 'medium' => array('width' => 1920, 'height' => 1080), //HD + 'large' => array('width' => 3840, 'height' => 2160), //4k 'raw' => 'raw', 'default' => array('width' => 1280, 'height' => 720) ], @@ -529,8 +535,9 @@ private function getAvailableResourceSizes() 'thumbnail' => 25, 'small' => 25, 'medium' => 50, + 'large' => 100, 'raw' => 'raw', - 'default' => 90 + 'default' => 90, ], 'error_message' => '' ], @@ -613,6 +620,7 @@ private function getResourceSize($fileType, $size = null) */ public function download($damUrl, $size = null) { + //JAP REVIEW CANDOWNLOAD FOR IMAGES $mediaId = DamUrlUtil::decodeUrl($damUrl); $media = Media::findOrFail($mediaId); @@ -620,16 +628,21 @@ public function download($damUrl, $size = null) $fileName = $damUrl . "." . $mimes->getExtension($media->mime_type); // json $mediaFileName = $fileName; $size = ($size === null ? 'default' : $size); + //JAP return raw file + if ($size == 'default') $size = 'raw'; $mimeType = $media->mime_type; $fileType = explode('/', $mimeType)[0]; - if ($fileType == 'video' || $fileType == 'image') { + if ($size === 'raw' && $fileType === 'image') { + return response()->download($media->getPath(), $fileName); + } else if ($fileType == 'video' || $fileType == 'image') { $sizeValue = $this->getResourceSize($fileType, $size); $availableSizes = $this->getAvailableResourceSizes(); $compressed = $this->mediaService->preview($media, $availableSizes[$fileType], $size, $sizeValue, true); if ($fileType == 'image' || ($fileType == 'video' && in_array($size, ['medium', 'small', 'thumbnail']))) { + //JAP: revisar $response = $compressed->response('jpeg', $availableSizes[$fileType]['sizes'][$size] === 'raw' ? 100 : $availableSizes[$fileType]['qualities'][$size]); $response->headers->set('Content-Disposition', sprintf('attachment; filename="%s"', $mediaFileName)); $response->header('Cache-Control', 'public, max-age=86400'); // Configura el tiempo de caché en segundos (en este caso, 24 horas) @@ -640,10 +653,10 @@ public function download($damUrl, $size = null) return response()->download($compressed->getPath(), null, ['Content-Disposition' => sprintf('attachment; filename="%s"', $mediaFileName)]); } - $thumb = $this->getThumbnailBySize($size, $media); + /*$thumb = $this->getThumbnailBySize($size, $media); if ($thumb) { return response()->download($thumb, $fileName); - } + }*/ return response()->download($media->getPath(), $fileName); } @@ -706,6 +719,7 @@ public function deleteAssociatedFile(DamResource $damResource, Media $media) public function deleteAssociatedFiles(damResource $damResource, Request $request) { $idsToDelete = $request->all(); + if (!empty($idsToDelete)) { $resource = $this->resourceService->deleteAssociatedFiles($damResource, array_values($idsToDelete)); return (new ResourceResource($resource)) @@ -777,7 +791,7 @@ public function renderCDNResourceFile(CDNRequest $request) $accessCheck = true; $checkData = [ - 'ipAddress' => $ipAddress, + 'ipAddress' => $ipAddress, 'originURL' => $originURL ]; @@ -791,7 +805,7 @@ public function renderCDNResourceFile(CDNRequest $request) ]; $checkData = array_merge($checkData, $extra_data); } - + if ($cdnInfo->checkAccessRequirements($checkData)) $accessCheck = true; @@ -807,19 +821,19 @@ public function renderCDNResourceFile(CDNRequest $request) if (count($responseJson->files) == 0) return response(['error' => 'No files attached!']); - + $mediaId = DamUrlUtil::decodeUrl($responseJson->files[0]->dam_url); $size = $request->size; - if (Cache::has("{$mediaId}__{$size}")) { - return Cache::get("{$mediaId}__$size"); - } + //if (Cache::has("{$mediaId}__{$size}")) { + // return Cache::get("{$mediaId}__$size"); + //} $can_download = $resource->type == ResourceType::document ? ($resource->data->description->can_download ?? false) : true; - return $this->renderResource($mediaId, $method, $size, $request->key, true, $can_download); - + return $this->renderResource($request, $mediaId, $method, $size, $request->key, true, $can_download); } - public function renderCDNResource(CDNRequest $request){ + public function renderCDNResource(CDNRequest $request) + { $method = request()->method(); $ipAddress = $_SERVER['REMOTE_ADDR']; $originURL = $request->headers->get('referer'); @@ -850,7 +864,6 @@ public function renderCDNResource(CDNRequest $request){ return (new ResourceResource($resource)) ->response() ->setStatusCode(Response::HTTP_OK); - } @@ -865,7 +878,7 @@ public function setWorkspace(SetResourceWorkspaceRequest $request) return response(['error' => 'User inaccessible.']); $resource = DamResource::where('id', $request->damResource) - ->first(); + ->first(); if ($resource === null) return response(['error' => 'Resource doesn\'t exist.']); @@ -886,16 +899,17 @@ public function getFilesCount(DamResource $damResource) return response(['files_count' => $damResource->getNumberOfFilesAttached()], Response::HTTP_OK); } - public function checkAccess($request, $resource, $cdnInfo, $ipAddress, $originURL) { + public function checkAccess($request, $resource, $cdnInfo, $ipAddress, $originURL) + { $resourceResponse = new ResourceResource($resource); $responseJson = json_decode($resourceResponse->toJson()); if (isset($request->size) && $this->getFileType($responseJson->files[0]->dam_url) === 'application/pdf') { return true; } - + $checkData = [ - 'ipAddress' => $ipAddress, + 'ipAddress' => $ipAddress, 'originURL' => $originURL ]; @@ -910,24 +924,118 @@ public function checkAccess($request, $resource, $cdnInfo, $ipAddress, $originUR ]; $checkData = array_merge($checkData, $extra_data); } - - if ($cdnInfo->checkAccessRequirements($checkData)){ + + if ($cdnInfo->checkAccessRequirements($checkData)) { return true; } return false; } - public function retryClone($copy) { + public function retryClone($copy) + { try { $this->scormService->cloneBook($copy); } catch (\Exception $exc) { $this->resourceService->duplicateUpdateStatus($copy, ['message' => $exc->getMessage(), 'status' => 'error']); $message = $exc->getMessage(); - + return response()->json([ 'error' => $message ])->setStatusCode(Response::HTTP_BAD_GATEWAY); } } + + private function handleSizedImageRendering(string $path, Media $media, string $fileType, string $size, string $mimeType): array + { + $pathSize = $this->renderService->appendSizeToPath($path, $size); + + if (!Storage::exists($path)) { + abort(404); + } + + if (!Storage::exists($pathSize)) { + $sizeValue = $this->getResourceSize($fileType, $size); + $availableSizes = $this->getAvailableResourceSizes(); + $compressed = $this->mediaService->preview($media, $availableSizes[$fileType], $size, $sizeValue); + } + + return [$pathSize, $mimeType]; + } + + private function createStreamedResponse(string $fileVariantpath, string $type, string $mediaFileName): StreamedResponse + { + $response = new StreamedResponse(); + $file = Storage::get($fileVariantpath); + $response->setCallback(function () use ($file) { + echo $file; + }); + + $maxAge = 3600 * 24 * 7; + + $response->headers->set('Content-Type', $type); + $response->headers->set('Content-Length', strlen($file)); + $response->headers->set('Content-Disposition', sprintf('inline; filename="%s"', $mediaFileName)); + $response->headers->set('Cache-Control', 'public, max-age=' . $maxAge . ', immutable'); + $response->headers->set('Expires', gmdate('D, d M Y H:i:s', time() + $maxAge) . ' GMT'); + + $etag = md5($file); + $response->setEtag($etag); + + $lastModified = Storage::lastModified($fileVariantpath); + $response->setLastModified(Carbon::createFromTimestamp($lastModified)); + + return $response; + } + + private function createCachedResponse(string $file, StreamedResponse $streamedResponse): Response + { + return new Response( + $file, + Response::HTTP_OK, + $streamedResponse->headers->all() + ); + } + + private function handleImageAndVideoResponse($fileType, $size, $compressed, $mediaFileName, $mediaId, $availableSizes) + { + if ($fileType == 'image' || ($fileType == 'video' && in_array($size, ['medium', 'small', 'thumbnail']))) { + $response = response()->file($compressed->basePath()); + $this->setCommonHeaders($response, $mediaFileName, $compressed); + + // if ($fileType == 'image') { + // $response_cache = $this->createImageCacheResponse($compressed, $fileType, $size, $availableSizes, $mediaFileName); + // Cache::put("{$mediaId}__$size", $response_cache); + // } + + return $response; + } + + return null; + } + + private function setCommonHeaders($response, $mediaFileName, $compressed) + { + $maxAge = 3600 * 24 * 7; + $response->headers->set('Content-Disposition', sprintf('inline; filename="%s"', $mediaFileName)); + $response->headers->set('Cache-Control', 'public, max-age=' . $maxAge . ', immutable'); + $response->headers->set('Expires', gmdate('D, d M Y H:i:s', time() + $maxAge) . ' GMT'); + + $etag = md5_file($compressed->basePath()); + $response->setEtag($etag); + + $lastModified = filemtime($compressed->basePath()); + $response->setLastModified(Carbon::createFromTimestamp($lastModified)); + } + + //JAP VERIFICAR uso funcion + private function createImageCacheResponse($compressed, $fileType, $size, $availableSizes, $mediaFileName) + { + $quality = $availableSizes[$fileType]['sizes'][$size] === 'raw' ? 100 : $availableSizes[$fileType]['qualities'][$size]; + $response_cache = $compressed->response('jpeg', $quality); + + $this->setCommonHeaders($response_cache, $mediaFileName, $compressed); + + return $response_cache; + } } diff --git a/backend/app/Http/Resources/MediaResource.php b/backend/app/Http/Resources/MediaResource.php index 79ef46a8..3f6300b3 100644 --- a/backend/app/Http/Resources/MediaResource.php +++ b/backend/app/Http/Resources/MediaResource.php @@ -18,16 +18,16 @@ class MediaResource extends JsonResource */ public function toArray($request) { - $thumbnailsTypes = ThumbnailTypes::getValues(); + /*$thumbnailsTypes = ThumbnailTypes::getValues(); $thumbnails = []; if ($this->collection_name == MediaType::Preview()->key) { foreach($thumbnailsTypes as $thumbnailType) { $hasPreview = $this->hasGeneratedConversion($thumbnailType); - $thumbnails[$thumbnailType] = $hasPreview; + $thumbnails[$thumbnailType] = $hasPreview; } - } + }*/ $parent_id = $this->hasCustomProperty('parent_id') ? $this->getCustomProperty('parent_id') : ""; @@ -38,7 +38,7 @@ public function toArray($request) 'parent_id' => $parent_id, 'file_name' => $this->file_name, 'mime_type' => $this->mime_type, - 'thumbnails' => $thumbnails, + // 'thumbnails' => $thumbnails, ]; } } diff --git a/backend/app/Http/Resources/Solr/BaseSolrResource.php b/backend/app/Http/Resources/Solr/BaseSolrResource.php index db92620c..1c4ba952 100644 --- a/backend/app/Http/Resources/Solr/BaseSolrResource.php +++ b/backend/app/Http/Resources/Solr/BaseSolrResource.php @@ -7,8 +7,11 @@ use App\Http\Resources\Solr\LOMSolrResource; use App\Models\Lom; use App\Models\Lomes; +use App\Services\Media\MediaSizeImage; +use App\Utils\DamUrlUtil; use App\Utils\Utils; use Illuminate\Http\Resources\Json\JsonResource; +use Intervention\Image\ImageManager; use Solarium\Client; class BaseSolrResource extends JsonResource @@ -191,7 +194,7 @@ protected function getLOMValues(string $type = 'lom') } } - + return $values; } @@ -212,4 +215,19 @@ protected function formatSemanticTags($tags) } return $toSolr; } + + protected function imgToBase64() { + + $media = $this->getMedia(MediaType::File()->key); + $file = $media->first(); + $mediaPath = $file->getPath(); + $manager = new ImageManager(['driver' => 'imagick']); + $image = $manager->make($mediaPath); + $image->fit(256, 144, function ($constraint) { + $constraint->aspectRatio(); + $constraint->upsize(); + }); + $base64 = (string) $image->encode('data-url'); + return $base64; + } } diff --git a/backend/app/Http/Resources/Solr/MultimediaSolrResource.php b/backend/app/Http/Resources/Solr/MultimediaSolrResource.php index 169fbd76..68221a39 100644 --- a/backend/app/Http/Resources/Solr/MultimediaSolrResource.php +++ b/backend/app/Http/Resources/Solr/MultimediaSolrResource.php @@ -11,9 +11,11 @@ class MultimediaSolrResource extends BaseSolrResource { + public function __construct($resource, $lomSolrClient = null, $lomesSolrClient = null, $toSolr = false) { parent::__construct($resource, $lomSolrClient, $lomesSolrClient); + } protected function getPreviews() @@ -88,11 +90,15 @@ protected function getCoreResourceType() public function toArray($request) { $files = $this->getFiles(); - + $data = json_decode($this->getData()); + if (isset($files[0]) && env('SOLR_THUMBNAIL')) { + $data->img = $this->imgToBase64($files[0]); + } + $data = json_encode($data); return [ 'id' => $this->getID(), 'name' => $this->getName(), - 'data' => $this->getData(), + 'data' => $data, 'active' => $this->getActive(), 'type' => $this->getType(), 'types' => $this->getTypes($files), @@ -108,7 +114,7 @@ public function toArray($request) 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, 'lom' => $this->getLOMValues(), - 'lomes' => $this->getLOMValues('lomes') + 'lomes' => $this->getLOMValues('lomes'), ]; } } diff --git a/backend/app/Models/DamResource.php b/backend/app/Models/DamResource.php index b873189b..6fc587c5 100644 --- a/backend/app/Models/DamResource.php +++ b/backend/app/Models/DamResource.php @@ -40,7 +40,7 @@ class DamResource extends Model implements HasMedia, TaggableInterface "id" => "string" ]; - public function registerMediaConversions(\Spatie\MediaLibrary\MediaCollections\Models\Media $media = null): void + /* public function registerMediaConversions(\Spatie\MediaLibrary\MediaCollections\Models\Media $media = null): void { $this->addMediaConversion(ThumbnailTypes::thumb_64x64) ->width(64) @@ -51,7 +51,7 @@ public function registerMediaConversions(\Spatie\MediaLibrary\MediaCollections\M ->width(200) ->height(400) ->performOnCollections(MediaType::Preview()->key); - } + }*/ public function categories(): BelongsToMany { diff --git a/backend/app/Services/Media/MediaSizeImage.php b/backend/app/Services/Media/MediaSizeImage.php index daf5b936..8db63a49 100644 --- a/backend/app/Services/Media/MediaSizeImage.php +++ b/backend/app/Services/Media/MediaSizeImage.php @@ -1,109 +1,124 @@ array('width' => 256, 'height' => 144), - 'small' => array('width' => 426, 'height' => 240), - 'medium' => array('width' => 854, 'height' => 480), - 'raw' => 'raw', - 'default' => array('width' => 1280, 'height' => 720) - ]; - private array $qualities = [ - 'thumbnail' => 25, - 'small' => 25, - 'medium' => 50, - 'raw' => 'raw', - 'default' => 90 - ]; + private array $sizes; private string $size; private ImageManager $manager; private Image $image; private string $path; private float $height; private float $width; - public function __construct(string $size, string $path,ImageManager $manager,Image $image) - { - $this->size = $size; - $this->manager = $manager; - $this->image = $image; - $this->path = $path; - $this->height = $this->image->height(); - $this->width = $this->image->width(); - } - /** - * Save the image. - * - * @return void - */ - public function save(){ - $pathSave = $this->image->dirname."/__".$this->size.".jpg"; - $aspectRatio = $this->getAspectRatio($this->width / $this->height); - if ($this->size === 'default'){ + public function __construct(string $size, string $path, ImageManager $manager, Image $image,array $sizes) + { + $this->sizes = $sizes; + $this->size = $size; + $this->manager = $manager; + $this->image = $image; + $this->path = $path; + $this->height = $this->image->height(); + $this->width = $this->image->width(); + } + /** + * Save the image. + * + * @return void + */ + public function save(String $extension) + { + $pathSave = $this->image->dirname . "/__" . $this->size . ".$extension"; + + if ($this->size === 'default') { $pathSave = $this->path; $this->image->save($pathSave); - }else{ - $this->image->resize($aspectRatio['width'],$aspectRatio['height'])->save($pathSave); + } else { + $aspectRatio = $this->getAspectRatio(); + $this->image->resize($aspectRatio['width'], $aspectRatio['height'])->save($pathSave); + } } - } - /** - * Check the image's size - * - * @return boolean - */ - public function checkSize(){ - $result = true; - $widthNew = $this->sizes[$this->size]['width']; - $heightNew = $this->sizes[$this->size]['height'] ; - if ($widthNew >= $this->width && $heightNew >= $this->height ) $result = false; - return $result; - } - - /** - * Check if a specific image exits. - * - * @return boolean - */ - public function imageExists(){ - $result = false; - $path = $this->path; - if ($this->size !== 'default') { - $path = $this->image->dirname."/__".$this->size.".jpg"; + /** + * Check the image's size + * + * @return boolean + */ + public function checkSize() + { + $result = true; + $widthNew = $this->sizes[$this->size]['width']; + $heightNew = $this->sizes[$this->size]['height']; + if ($widthNew >= $this->width && $heightNew >= $this->height) $result = false; + return $result; } - $result = file_exists($path); - return $result; - } - - /** - * Return an image - * - * @return \Intervention\Image\Image - */ - public function getImage(){ - $result = $this->image->dirname."/__".$this->size.".jpg"; - if($this->size === "default" ) return $this->image; //$result = $this->path; - return $this->manager->make($result); - } - - private function getAspectRatio($aspectRatio){ - if ($aspectRatio >= 1.0 && $this->height<=$this->sizes[$this->size]['height']) { // Horizontal - $newWidth = $this->sizes[$this->size]['width']; - $newHeight = $newWidth / $aspectRatio; - } else { // Vertical - $newHeight = $this->sizes[$this->size]['height']; - $newWidth = $newHeight * $aspectRatio; + + /** + * Check if a specific image exits. + * + * @return boolean + */ + public function imageExists(String $extension) + { + $result = false; + $path = $this->path; + if ($this->size !== 'default') { + $path = $this->image->dirname . "/__" . $this->size . ".$extension"; + } + $result = file_exists($path); + return $result; } - $result = ["height" => $newHeight,"width" => $newWidth]; - return $result; - } - public function setSizeDefault(){ - $this->size = 'default'; - } + /** + * Return an image + * + * @return \Intervention\Image\Image + */ + public function getImage(String $extension) + { + $result = $this->image->dirname . "/__" . $this->size . ".$extension"; + if ($this->size === "default" || $this->size === "raw") return $this->image; //$result = $this->path; + return $this->manager->make($result); + } + + private function getAspectRatio() + { + $originalWidth = $this->width; + $originalHeight = $this->height; + + + $targetWidth = $this->sizes[$this->size ]['width']; + $targetHeight = $this->sizes[$this->size ]['height']; + + $isVertical = $originalHeight > $originalWidth; + + if ($originalWidth <= $targetWidth && $originalHeight <= $targetHeight) { + return ["height" => $originalHeight, "width" => $originalWidth]; + } + + if ($isVertical) { + $newHeight = $targetWidth; + $newWidth = round($newHeight*$originalWidth / $originalHeight); + } else { + $ratio = $originalWidth / $targetWidth; + $newWidth = $targetWidth; + $newHeight = round($originalHeight / $ratio); + } + + return ["height" => $newHeight, "width" => $newWidth]; + } + + public function setSizeDefault() + { + $this->size = 'default'; + } + + public function pngHasAlpha() + { + return strpos($this->image->encode('png')->getEncoded(), 'tRNS') !== false; + } } diff --git a/backend/app/Services/MediaService.php b/backend/app/Services/MediaService.php index 107f70fa..113dbee3 100755 --- a/backend/app/Services/MediaService.php +++ b/backend/app/Services/MediaService.php @@ -1,6 +1,7 @@ file_name, '', $mediaPath); $thumbnail = $file_directory . '/' . $media->filename . '__thumb_.png'; - if ($fileType === 'video') { return $isDownload ? $this->downloadVideo($media->id, $media->file_name, $mediaPath, $availableSizes, $sizeKey, $size, $thumbnail) : $this->previewVideo($media->id, $media->file_name, $mediaPath, $availableSizes, $sizeKey, $size, $thumbnail); - } else if($fileType === 'image') { - return $this->previewImage($mediaPath,$sizeKey); + } else if ($fileType === 'image') { + return $this->previewImage($mediaPath, $sizeKey, $availableSizes['sizes']); } else { return $mediaPath; } @@ -92,8 +92,13 @@ private function getVideoDimensions($path) { $command = "ffmpeg -i \"$path\" 2>&1 | grep Video: | grep -Po '\d{3,5}x\d{3,5}'"; $output = explode('x', exec($command)); - $resolution = array("path" => $path, "width" => $output[0], "height" => $output[1], "aspect_ratio" => $output[0] / $output[1], - "name" => $output[1] . "p"); + $resolution = array( + "path" => $path, + "width" => $output[0], + "height" => $output[1], + "aspect_ratio" => $output[0] / $output[1], + "name" => $output[1] . "p" + ); return $resolution; } @@ -109,9 +114,9 @@ private function updateAvailableSizes(&$availableSizes, $aspectRatio, $mediaPath if ($availableSizes['sizes'][$k]['width'] % 2 !== 0) $availableSizes['sizes'][$k]['width'] -= 1; $availableSizes['sizes'][$k]['path'] = implode('/', array_slice(explode('/', $mediaPath), 0, -1)) - . '/' . pathinfo($mediaFileName, PATHINFO_FILENAME) . '_' - . $availableSizes['sizes'][$k]['name'] . '.' - . pathinfo($mediaFileName, PATHINFO_EXTENSION); + . '/' . pathinfo($mediaFileName, PATHINFO_FILENAME) . '_' + . $availableSizes['sizes'][$k]['name'] . '.' + . pathinfo($mediaFileName, PATHINFO_EXTENSION); $availableSizes['sizes'][$k]['relative_path'] = str_replace(storage_path('app') . '/', '', $availableSizes['sizes'][$k]['path']); } } @@ -135,7 +140,6 @@ private function downloadVideo($mediaID, $mediaFileName, $mediaPath, $availableS $video = new Video(); $video->setPath($mediaPath); return $video; - } private function previewVideo($mediaID, $mediaFileName, $mediaPath, $availableSizes, $sizeKey = null, $size = null, $thumbnail = null) @@ -159,7 +163,7 @@ private function getVideo($mediaID, $mediaFileName, $mediaPath, $availableSizes, if (!$thumb_exists) { $this->saveVideoSnapshot($thumbnail, $mediaPath); } else { - return $this->previewImage($thumbnail, $size); + return $this->previewImage($thumbnail, $sizeKey, $availableSizes['sizes']); } } else if ($size == 'raw') { return $this->getPreviewOrDownload($mediaPath, $isDownload); @@ -204,21 +208,27 @@ private function getPreviewOrDownload($path, $isDownload) return VideoStreamer::streamFile($path); } - private function previewImage($mediaPath, $type = 'raw') + private function previewImage($mediaPath, $type = 'raw', $sizes) { $manager = new ImageManager(['driver' => 'imagick']); $image = $manager->make($mediaPath); - $imageProcess= new MediaSizeImage($type,$mediaPath,$manager,$image); - if(!$imageProcess->checkSize())$imageProcess->setSizeDefault(); - if (!$imageProcess->imageExists()) { - $imageProcess->save(); + $imageProcess = new MediaSizeImage($type, $mediaPath, $manager, $image, $sizes); + $extension = pathinfo($mediaPath, PATHINFO_EXTENSION); + if ($type !== 'raw') { + // if (!$imageProcess->checkSize()) $imageProcess->setSizeDefault(); + if (!$imageProcess->imageExists($extension)) { + /*if (!$imageProcess->pngHasAlpha() && $extension === 'png') { + $extension = 'jpg'; + }*/ + $imageProcess->save($extension); + } } - $result = $imageProcess->getImage(); + $result = $imageProcess->getImage($extension); return $result; } - public function saveVideoSnapshot($thumbPath, $videoSourcePath, $sec = 10) - { + public function saveVideoSnapshot($thumbPath, $videoSourcePath, $sec = 1) + { $ffmpeg = FFMpeg::create([ 'ffmpeg.binaries' => config('app.ffmpeg_path'), 'ffprobe.binaries' => config('app.ffprobe_path') @@ -236,15 +246,13 @@ public function saveVideoSnapshot($thumbPath, $videoSourcePath, $sec = 10) * @param null $files * @return array|mixed */ - public function addFromRequest(Model $model, $collection, $customProperties, $files = null,$requestKey = null) + public function addFromRequest(Model $model, $collection, $customProperties, $files = null, $requestKey = null) { $collection = $collection ?? $this->defaultFileCollection; - if (!empty($requestKey) && empty($files)) - { + if (!empty($requestKey) && empty($files)) { $model->addMediaFromRequest($requestKey)->withCustomProperties($customProperties)->toMediaCollection($collection); } - if (!empty($files) && empty($requestKey)) - { + if (!empty($files) && empty($requestKey)) { $model->addMedia($files)->withCustomProperties($customProperties)->toMediaCollection($collection); } $model->save(); @@ -254,23 +262,14 @@ public function addFromRequest(Model $model, $collection, $customProperties, $f $mediaList = $this->list($model, $collection); $media = Media::findOrFail($mediaList[0]->id); - $mimeType = $media->mime_type; + $mimeType = $media->mime_type; $mediaPath = $media->getPath(); $fileType = explode('/', $mimeType)[0]; - if($fileType == 'video') { + if ($fileType == 'video') { $file_directory = str_replace($media->file_name, '', $mediaPath); $thumbnail = $file_directory . '/' . $media->filename . '__thumb_.png'; $this->saveVideoSnapshot($thumbnail, $mediaPath); } - if ($fileType === 'image') { - $manager = new ImageManager(['driver' => 'imagick']); - $image = $manager->make($mediaPath); - $image2 = $manager->make($mediaPath); - $thumb = new MediaSizeImage('thumbnail',$mediaPath,$manager,$image); - $small = new MediaSizeImage('small',$mediaPath,$manager,$image2); - if($thumb->checkSize()) $thumb->save(); - if($small->checkSize()) $small->save(); - } return !empty($mediaList) ? end($mediaList) : []; } @@ -314,7 +313,7 @@ public function generateRenderKey() $keyEntry = DocumentRendererKey::create(['key' => $key]); $keyEntry->storeKeyExpirationDate(); $flag = true; - } catch(\Exception $e) { + } catch (\Exception $e) { // echo $e->getMessage(); } } diff --git a/backend/app/Services/RenderService.php b/backend/app/Services/RenderService.php new file mode 100644 index 00000000..34e65e1d --- /dev/null +++ b/backend/app/Services/RenderService.php @@ -0,0 +1,109 @@ +getPath(), PATHINFO_EXTENSION); + if (empty($originalExtension)) { + $avifPath = $media->getPath() . '.avif'; + } else { + $avifPath = $this->getConvertedPath($media->getPath(), 'avif'); + } + $relativeAvifPath = explode(Storage::path(''), $avifPath)[1]; + $file = Storage::get($relativeAvifPath); + $type = 'image/avif'; + + return response($file, 200)->header('Content-Type', $type); + } + + public function checkIfImageExists($mediaPath) + { + if (!Storage::exists($mediaPath)) { + return false; + } + return true; + } + + public function generateAvif($path) + { + try { + $manager = new ImageManager(['driver' => 'imagick']); + $image = $manager->make(Storage::get($path)); + $avifImage = $image->encode('avif', 70); + $avifPath = $this->getConvertedPath($path, 'avif'); + Storage::put($avifPath, (string) $avifImage); + return true; + //$img = Image::make(Storage::get($path))->encode('avif', 70); + //$avifPath = $this->getConvertedPath($path, 'avif'); + //Storage::put($avifPath, (string) $img); + //return true; + } catch (\Throwable $th) { + return false; + } + } + + public function checkAvif(Request $request) + { + $acceptHeader = $request->header('Accept', ''); + + if (strpos($acceptHeader, 'image/avif') !== false) { + return true; + } else { + return false; + } + } + + public function getConvertedPath($mediaPath, $targetExtension) + { + $originalExtension = pathinfo($mediaPath, PATHINFO_EXTENSION); + + $convertedPath = preg_replace('/\.' . preg_quote($originalExtension, '/') . '$/', '.' . $targetExtension, $mediaPath); + + return $convertedPath; + } + + public function appendSizeToPath($mediaPath, $size) + { + $originalExtension = pathinfo($mediaPath, PATHINFO_EXTENSION); + + $directoryPath = dirname($mediaPath); + + $sizedPath = $directoryPath . '/__' . $size . '.' . $originalExtension; + + return $sizedPath; + } + + public function logAvifConversion($fileName, $originalSize, $newSize) + { + $truncatedFileName = strlen($fileName) > 12 ? substr($fileName, 0, 9) . '...' : $fileName; + $sizeDifference = $originalSize - $newSize; + + // Convert to MB + $originalSizeMB = $originalSize / (1024 * 1024); + $newSizeMB = $newSize / (1024 * 1024); + $sizeDifferenceMB = $sizeDifference / (1024 * 1024); + + // Log individual file conversion + $logMessage = sprintf( + "File: %-12s Original: %10.2f MB AVIF: %10.2f MB Diff: %10.2f MB", + $truncatedFileName, + $originalSizeMB, + $newSizeMB, + $sizeDifferenceMB + ); + + // Also log to Laravel's logging system + Log::channel('avif_conversion')->info($logMessage); + } +} diff --git a/backend/app/Services/ResourceService.php b/backend/app/Services/ResourceService.php index 759973b3..b5e0cd4a 100644 --- a/backend/app/Services/ResourceService.php +++ b/backend/app/Services/ResourceService.php @@ -73,20 +73,20 @@ class ResourceService */ private XTagsService $xtagService; - /** + /** * @var XowlImageService */ private XowlImageService $xowlImageService; /** - * @var XevalSyncActivityService - */ + * @var XevalSyncActivityService + */ private XevalSyncActivityService $xevalSyncActivityService; /** - * @var XevalSyncAssessmentService - */ + * @var XevalSyncAssessmentService + */ private XevalSyncAssessmentService $xevalSyncAssessmentService; const PAGE_SIZE = 30; @@ -97,10 +97,18 @@ class ResourceService * @param SolrService $solr * @param CategoryService $categoryService */ - public function __construct(MediaService $mediaService, SolrService $solr, CategoryService $categoryService, WorkspaceService $workspaceService, - KakumaService $kakumaService, XTagsService $xtagService, XowlImageService $xowlImageService,SolrConfig $solrConfig,XevalSyncActivityService $xevalSyncActivityService, XevalSyncAssessmentService $xevalSyncAssessmentService) - - { + public function __construct( + MediaService $mediaService, + SolrService $solr, + CategoryService $categoryService, + WorkspaceService $workspaceService, + KakumaService $kakumaService, + XTagsService $xtagService, + XowlImageService $xowlImageService, + SolrConfig $solrConfig, + XevalSyncActivityService $xevalSyncActivityService, + XevalSyncAssessmentService $xevalSyncAssessmentService + ) { $this->mediaService = $mediaService; $this->categoryService = $categoryService; $this->solr = $solr; @@ -179,7 +187,7 @@ private function linkCategoriesFromJson($resource, $data): void if (property_exists($data->description, $possibleKeyName)) { $property = $data->description->$possibleKeyName; if (is_array($property)) { - if(count($property) > 0) { + if (count($property) > 0) { foreach ($property as $child) { $this->setCategories($resource, $child, $data); } @@ -199,7 +207,7 @@ private function linkCategoriesFromJson($resource, $data): void private function setDefaultLanguageIfNeeded(array $params): void { - if( isset($params['type']) && $params["type"] === ResourceType::book && !property_exists($params["data"]->description, "lang")) { + if (isset($params['type']) && $params["type"] === ResourceType::book && !property_exists($params["data"]->description, "lang")) { $params["data"]->description->lang = getenv('BOOK_DEFAULT_LANGUAGE'); } } @@ -267,7 +275,6 @@ public function queryFilter($queryFilters) { return DamResource::whereRaw($queryFilters)->get(); - } /** @@ -288,7 +295,7 @@ public function exploreCourses($user_id = null): Collection public function getRecommendedCategory($user_id): array { if (!$user_id) { - throw new Exception ("User id must be sent to get recommendations"); + throw new Exception("User id must be sent to get recommendations"); } $courses = $this->kakumaService->getRecommendedCourses($user_id); @@ -334,7 +341,7 @@ public function update(DamResource $resource, $params): DamResource ); } if (array_key_exists("data", $params) && !empty($params["data"])) { - + $this->setDefaultLanguageIfNeeded($params); if (isset($params['data']->description->entities_linked) || isset($params['data']->description->entities_non_linked)) { if (isset($params['data']->description->entities_linked)) { @@ -344,7 +351,7 @@ public function update(DamResource $resource, $params): DamResource unset($params['data']->description->entities_non_linked); } } - + $resource->update( [ 'data' => $params['data'], @@ -352,26 +359,26 @@ public function update(DamResource $resource, $params): DamResource 'name' => $params['data']->description->name ?? 'name not found' ] ); - + $this->linkCategoriesFromJson($resource, $params['data']); $this->linkTagsFromJson($resource, $params['data']); } - + if (array_key_exists("FilesToRemove", $params)) { foreach ($params["FilesToRemove"] as $mediaID) { $mediaResult = Media::where('id', $mediaID)->first(); - + if ($mediaResult !== null) { $this->deleteAssociatedFile($resource, $mediaResult); } } } - + // TODO save all languages label on taxon path if (isset($params['data']->description->semantic_tags)) { $semantic_tags = $params['data']->description->semantic_tags; $lom_params = [ - 'Taxon Path'=> [], + 'Taxon Path' => [], '_tab_key' => "9" ]; /*foreach ($semantic_tags as $semantic_tag) { @@ -393,7 +400,7 @@ public function update(DamResource $resource, $params): DamResource }*/ $this->setLomData($resource, $lom_params); } - + $this->saveAssociatedFiles($resource, $params); $resource = $resource->fresh(); $this->solr->saveOrUpdateDocument($resource); @@ -410,13 +417,13 @@ public function update(DamResource $resource, $params): DamResource $XowlQueue->addImageToQueue($mediaFiles); } $mediaFiles = $resource->getMedia('Preview'); - if( $params['type'] === "activity"){ - $parseActivity = $this->xevalSyncActivityService->parseActivityData($resource->id,$params['data'],$params['collection_id']); + if ($params['type'] === "activity") { + $parseActivity = $this->xevalSyncActivityService->parseActivityData($resource->id, $params['data'], $params['collection_id']); $res = $this->xevalSyncActivityService->syncActivityOnXeval($parseActivity); } if ($params['type'] === "assessment") { - $parseAssessment = $this->xevalSyncAssessmentService->parseAssessmentData($resource->id,$params['data'],$params['collection_id']); - $res = $this->xevalSyncAssessmentService->syncAssessmentOnXeval( $parseAssessment); + $parseAssessment = $this->xevalSyncAssessmentService->parseAssessmentData($resource->id, $params['data'], $params['collection_id']); + $res = $this->xevalSyncAssessmentService->syncAssessmentOnXeval($parseAssessment); } DB::commit(); return $resource; @@ -424,10 +431,10 @@ public function update(DamResource $resource, $params): DamResource DB::rollback(); throw $th; } - } - public function updateFromXeval(DamResource $resource,$params){ + public function updateFromXeval(DamResource $resource, $params) + { DB::beginTransaction(); try { $params['data'] = json_decode($params['data']); @@ -439,10 +446,10 @@ public function updateFromXeval(DamResource $resource,$params){ ); } if (array_key_exists("data", $params) && !empty($params["data"])) { - + $this->setDefaultLanguageIfNeeded($params); - - + + $json_data_xdam = $resource->data->description; $json_data_xeval = $params['data']->description; $merged_json = (object) array_merge((array) $json_data_xdam, (array) $json_data_xeval); @@ -459,7 +466,7 @@ public function updateFromXeval(DamResource $resource,$params){ } if (isset($params['data']->description->semantic_tags)) { $lom_params = [ - 'Taxon Path'=> [], + 'Taxon Path' => [], '_tab_key' => "9" ]; $this->setLomData($resource, $lom_params); @@ -467,7 +474,7 @@ public function updateFromXeval(DamResource $resource,$params){ $this->saveAssociatedFiles($resource, $params); $resource = $resource->fresh(); $this->solr->saveOrUpdateDocument($resource); - + DB::commit(); return $resource; } catch (\Throwable $th) { @@ -476,7 +483,7 @@ public function updateFromXeval(DamResource $resource,$params){ } } - /** + /** * @param DamResource $resource * @param $params * @return DamResource @@ -516,10 +523,8 @@ public function store( $toWorkspaceId = null, $fromBatchType = null, $launchThrow = true - ): DamResource - - { - if (is_string($params['data'] )) $params['data'] = json_decode($params['data']); + ): DamResource { + if (is_string($params['data'])) $params['data'] = json_decode($params['data']); /* $wid cannot be null */ @@ -528,7 +533,7 @@ public function store( $wsp = explode(',', $params['toWorkspaceId']); } $paramsData = is_array($params['data']) ? Utils::arrayToObject($params['data']) : $params['data']; - $exceptionStrings = ['notWorkspace' => 'Undefined workspace','notOrganization' => 'The workspace doesn\'t belong to an organization']; + $exceptionStrings = ['notWorkspace' => 'Undefined workspace', 'notOrganization' => 'The workspace doesn\'t belong to an organization']; if ($wsp) { $wsps = []; foreach ($wsp as $value) { @@ -541,13 +546,13 @@ public function store( $name = array_key_exists('name', $params) ? $params["name"] : ""; $type = $fromBatchType ?? ResourceType::fromKey($params["type"])->value; - $idResourceData = ($type == ResourceType::course) ? $params['kakuma_id'] : Str::orderedUuid() ; + $idResourceData = ($type == ResourceType::course) ? $params['kakuma_id'] : Str::orderedUuid(); $nameResource = $paramsData->description->name ?? $name; - - if($wsp === null) throw new Exception($exceptionStrings['notWorkspace']); + + if ($wsp === null) throw new Exception($exceptionStrings['notWorkspace']); if (count($wsps) === 0) $wsps[] = Workspace::find(Auth::user()->selected_workspace); foreach ($wsps as $workspace) { - if(!$workspace->organization()->first()) throw new Exception($exceptionStrings['notOrganization']); + if (!$workspace->organization()->first()) throw new Exception($exceptionStrings['notOrganization']); } $this->setDefaultLanguageIfNeeded($params); @@ -574,8 +579,8 @@ public function store( foreach ($wsps as $workspace) { $this->setResourceWorkspace($newResource, $workspace); } - $this->linkCategoriesFromJson($newResource,$paramsData ); - $this->linkTagsFromJson($newResource,$paramsData); + $this->linkCategoriesFromJson($newResource, $paramsData); + $this->linkTagsFromJson($newResource, $paramsData); $this->saveAssociatedFiles($newResource, $params); $newResource = $newResource->fresh(); $this->solr->saveOrUpdateDocument($newResource); @@ -609,11 +614,11 @@ public function store( public function duplicateResource($data): DamResource { $newResourceData = $data->toArray(); - $newResourceData['id'] = Str::orderedUuid(); - // $workspace = Workspace::find(Auth::user()->selected_workspace); - + $newResourceData['id'] = Str::orderedUuid(); + // $workspace = Workspace::find(Auth::user()->selected_workspace); + $newData = $newResourceData['data']; - $newData->description->name = $newData->description->name."_copy"; + $newData->description->name = preg_replace('/_copy/', '',$newData->description->name) . "_copy"; $newResourceData['name'] = $newData->description->name; $newResourceData['data'] = $newData; $newResource = DamResource::create($newResourceData); @@ -627,19 +632,20 @@ public function duplicateResource($data): DamResource $newResource = $this->duplicateAssociatedData($data, $newResource); $newCopy = new \App\Models\Copy([ - 'id' => (string) Str::uuid(), - 'parent_id' => null, - 'hash_new' => $newResourceData['id'], - 'hash_old' => $data->id, - 'status' => 'pending' + 'id' => (string) Str::uuid(), + 'parent_id' => null, + 'hash_new' => $newResourceData['id'], + 'hash_old' => $data->id, + 'status' => 'pending' ]); - $newCopy->save(); - + $newCopy->save(); + return $newResource; } - public function duplicateUpdateStatus($copy, $data) { + public function duplicateUpdateStatus($copy, $data) + { try { if ($data['status'] === 'OK') $data['status'] = 'completed'; if ($data['status'] === 'KO') $data['status'] = 'error'; @@ -651,7 +657,7 @@ public function duplicateUpdateStatus($copy, $data) { $copy->status = $data['status']; if ($data['status'] == 'completed' && !isset($data['message'])) { $copy->message = ''; - } + } if (isset($data['message'])) { $copy->message = $data['message']; } @@ -661,8 +667,9 @@ public function duplicateUpdateStatus($copy, $data) { throw $e; } } - - public function getCopy(String $copy) { + + public function getCopy(String $copy) + { try { $copy = Copy::where('hash_new', $copy)->first(); if (!$copy) { @@ -674,18 +681,19 @@ public function getCopy(String $copy) { } } - public function processDuplicateExtraData(DamResource $damResource, $dataToProcess,$type){ + public function processDuplicateExtraData(DamResource $damResource, $dataToProcess, $type) + { foreach ($dataToProcess as $value) { $data = array_merge($value['formData'], ['_tab_key' => $value['key']]); - if($type==="lom"){ + if ($type === "lom") { $this->setLomData($damResource, $data); - }else if($type==="lomes"){ + } else if ($type === "lomes") { $this->setLomesData($damResource, $data); - } + } } } - - public function resourcesSchema () + + public function resourcesSchema() { $path = storage_path('solr_validators'); $dir = new DirectoryIterator($path); @@ -693,7 +701,7 @@ public function resourcesSchema () foreach ($dir as $fileinfo) { if (!$fileinfo->isDot()) { $fileName = $fileinfo->getFilename(); - $json_file = file_get_contents($path .'/'. $fileName); + $json_file = file_get_contents($path . '/' . $fileName); $key = str_replace('.json', '', $fileName); $resourceName = ucfirst($this->solrConfig->getNameCoreConfig(str_replace('_validator', '', $key))); $json = json_decode($json_file); @@ -717,7 +725,7 @@ public function resourcesSchema () foreach ($dir as $fileinfo) { if (!$fileinfo->isDot()) { $fileName = $fileinfo->getFilename(); - $json_file = file_get_contents($path .'/'. $fileName); + $json_file = file_get_contents($path . '/' . $fileName); $key = str_replace('.json', '', $fileName); $schemas['collection_config'][$key] = json_decode($json_file); } @@ -728,29 +736,30 @@ public function resourcesSchema () private function searchPreviewImage($data, $name): ?UploadedFile { - $fileName = str_replace('.', '_', $name).'_preview'; + $fileName = str_replace('.', '_', $name) . '_preview'; return array_key_exists($fileName, $data) ? $data[$fileName] : null; } - public function storeBatch ($data) + public function storeBatch($data) { $collection = ModelsCollection::find($data['collection']); $organization = $collection->organization()->first(); $wsps = []; try { $wsps = json_decode($data['workspaces']); - } catch(\Exception $exc) {} + } catch (\Exception $exc) { + } if (!isset($data['workspaces']) && !$data['workspaces']) { - if($data['create_wsp'] === '1') { + if ($data['create_wsp'] === '1') { $wsp = $this->workspaceService->create($organization->id, $data['workspace'])->id; } else { $wsp = $data['workspace']; } $wsps[] = $wsp; } - + $genericResourceDescription = array_key_exists('generic', $data) ? json_decode($data['generic'], true) : []; @@ -797,7 +806,7 @@ public function lomesSchema($asArray = false) { $lomesSchema = Utils::getLomesSchema($asArray); - $lomesSchemaClient = array_keys(config('solr_facets.client.'.env('APP_CLIENT', 'DEFAULT'))); + $lomesSchemaClient = array_keys(config('solr_facets.client.' . env('APP_CLIENT', 'DEFAULT'))); $tabsToDel = []; if (count($lomesSchemaClient) > 0) { @@ -844,7 +853,7 @@ public function lomSchema($asArray = false) return Utils::getLomSchema($asArray); } - public function searchForAssociativeKey($key, $tabKey, $array ) + public function searchForAssociativeKey($key, $tabKey, $array) { //move to Utils foreach ($array as $k => $val) { @@ -870,7 +879,7 @@ public function setLomesData($damResource, $params) foreach ($tabSchema['properties'] as $label => $props) { foreach ($formData as $f_key => $f_value) { - if($f_key === $label && $f_value !== null) { + if ($f_key === $label && $f_value !== null) { $updateArray[$props['data_field']] = $f_value; } } @@ -896,7 +905,7 @@ public function setLomData($damResource, $params) foreach ($tabSchema['properties'] as $label => $props) { foreach ($formData as $f_key => $f_value) { - if($f_key === $label && $f_value !== null) { + if ($f_key === $label && $f_value !== null) { $updateArray[$props['data_field']] = $f_value; } } @@ -911,7 +920,7 @@ public function getLomesData($damResource) { $schema = $this->lomesSchema(true); $lomes = $damResource->lomes()->first(); - if(!$lomes) { + if (!$lomes) { return []; } $lomes = $lomes->toArray(); @@ -923,7 +932,7 @@ public function getLomData($damResource) { $schema = $this->lomSchema(true); $lom = $damResource->lom()->first(); - if(!$lom) { + if (!$lom) { return []; } $lom = $lom->toArray(); @@ -947,7 +956,6 @@ private function getDataFromSchema($data, $schema) 'type' => $prop_values['type'] ]; } - } foreach ($data as $db_field => $value) { @@ -962,7 +970,7 @@ private function getDataFromSchema($data, $schema) $response[$key]['formData'][$label] = $value; } } - } + } if (count($response[$key]['formData']) === 0) unset($response[$key]); } } @@ -981,18 +989,18 @@ public function setResourceWorkspace(DamResource $newResource, Workspace $wsp): public function delete($resourceId) { $resource = DamResource::withTrashed()->findOrFail($resourceId); - if($resource->type === "activity"){ + if ($resource->type === "activity") { $data = $resource->data; $data->description->status_id = 3; - $parseActivity = $this->xevalSyncActivityService->parseActivityData($resource->id,$data,$resource->collection_id); + $parseActivity = $this->xevalSyncActivityService->parseActivityData($resource->id, $data, $resource->collection_id); $res = $this->xevalSyncActivityService->syncActivityOnXeval($parseActivity); } - if(Copy::where('hash_new', $resourceId)->exists()){ + if (Copy::where('hash_new', $resourceId)->exists()) { $copy = Copy::where('hash_new', $resourceId)->first(); $copy->status = 'deleted'; $copy->save(); $copy->delete(); - foreach ($resource->getMedia('File') as $mediaFile){ + foreach ($resource->getMedia('File') as $mediaFile) { $copy_child = Copy::where('hash_new', $mediaFile->id)->first(); $copy_child->status = 'deleted'; $copy_child->save(); @@ -1006,7 +1014,6 @@ public function delete($resourceId) } catch (\Throwable $th) { throw $th; } - } /** @@ -1020,12 +1027,12 @@ public function softDelete(DamResource $resource, bool $force, bool $onlyLocal) if ($resource->type == ResourceType::course && !$onlyLocal) { $this->kakumaService->softDeleteCourse($resource->id, $force); } - if(Copy::where('hash_new', $resource->id)->exists()){ + if (Copy::where('hash_new', $resource->id)->exists()) { $copy = Copy::where('hash_new', $resource->id)->first(); $copy->status = 'deleted'; $copy->save(); $copy->delete(); - foreach ($resource->getMedia('File') as $mediaFile){ + foreach ($resource->getMedia('File') as $mediaFile) { $copy_child = Copy::where('hash_new', $mediaFile->id)->first(); $copy_child->status = 'deleted'; $copy_child->save(); @@ -1036,7 +1043,6 @@ public function softDelete(DamResource $resource, bool $force, bool $onlyLocal) $this->solr->saveOrUpdateDocument($resource); return true; - } /** @@ -1055,7 +1061,6 @@ public function restore($resourceId, bool $onlyLocal) $this->solr->saveOrUpdateDocument($resource); return true; - } /** @@ -1097,7 +1102,7 @@ public function addCategoryTo(DamResource $resource, Category $category): DamRes $resource->categories()->attach($category); } } else { - throw new Exception ("category type and resource type are not equals"); + throw new Exception("category type and resource type are not equals"); } $this->solr->saveOrUpdateDocument($resource); return $resource; @@ -1106,12 +1111,12 @@ public function addCategoryTo(DamResource $resource, Category $category): DamRes public function setOnlyOneCategoryTo(DamResource $resource, Category $category): DamResource { if ($category->type == $resource->type) { - foreach($resource->categories()->get() as $cat) { + foreach ($resource->categories()->get() as $cat) { $resource->categories()->detach($cat); } $resource->categories()->attach($category); } else { - throw new Exception ("category type and resource type are not equals"); + throw new Exception("category type and resource type are not equals"); } $this->solr->saveOrUpdateDocument($resource); return $resource; @@ -1165,7 +1170,7 @@ public function deleteCategoryFrom(DamResource $resource, Category $category): D $this->solr->saveOrUpdateDocument($resource); } } else { - throw new Exception ("category type and resource type are not equals"); + throw new Exception("category type and resource type are not equals"); } return $resource; } @@ -1204,13 +1209,12 @@ public function deleteAssociatedFiles(DamResource $resource, array $ids): DamRes foreach ($ids as $id) { $media = Media::findOrFail($id); $media->delete(); - if(Copy::where('hash_new', $id)->exists()){ + if (Copy::where('hash_new', $id)->exists()) { $copy = Copy::where('hash_new', $id)->first(); $copy->status = 'deleted'; $copy->save(); $copy->delete(); } - } $this->solr->saveOrUpdateDocument($resource); return $resource->refresh(); @@ -1225,7 +1229,7 @@ public function deleteAssociatedFiles(DamResource $resource, array $ids): DamRes public function deleteAssociatedFile(DamResource $resource, Media $media): DamResource { $media->delete(); - if(Copy::where('hash_new', $media->id)->exists()){ + if (Copy::where('hash_new', $media->id)->exists()) { $copy = Copy::where('hash_new', $media->id)->first(); $copy->status = 'deleted'; $copy->save(); @@ -1235,7 +1239,8 @@ public function deleteAssociatedFile(DamResource $resource, Media $media): DamRe return $resource->refresh(); } - public function updateAsLast($toBeCloned, $created) { + public function updateAsLast($toBeCloned, $created) + { $collection = $toBeCloned->collection()->first(); $theClon = $collection->resources()->orderBy($created ? 'created_at' : 'updated_at', 'desc')->first(); $toBeCloned->data = $theClon->data; @@ -1244,15 +1249,13 @@ public function updateAsLast($toBeCloned, $created) { return $toBeCloned; } - public function updateAsOther($toBeCloned, $theClon) { - - } + public function updateAsOther($toBeCloned, $theClon) {} public function getResource($resourceID, $collectionID) { $resource = DamResource::where('id', $resourceID) - ->where('collection_id', $collectionID) - ->first(); + ->where('collection_id', $collectionID) + ->first(); return $resource; } @@ -1260,7 +1263,7 @@ public function getResource($resourceID, $collectionID) public function getCollection($collectionID) { $collection = ModelsCollection::where('id', $collectionID) - ->first(); + ->first(); return $collection; } @@ -1275,66 +1278,79 @@ protected function duplicateAssociatedData(DamResource $originalResource, DamRes { foreach ($originalResource->getMedia('File') as $mediaFile) { $path_parts = pathinfo($mediaFile->file_name); - $newFileName = $path_parts['filename'] . '_copy.' . $path_parts['extension']; - $newMediaFilePath = $path_parts['dirname'] . '/' . $newFileName; - + $filename = $path_parts['filename']; + $extension = $path_parts['extension']; + + $newFileName = preg_replace('/_copy/', '', $filename) . '_copy.' . $extension; + $newMediaFilePath = storage_path('app/public/'.$mediaFile->uuid . '/' . $newFileName); + if (!file_exists($newMediaFilePath)) { - // Copy the file to the new location - copy($mediaFile->getPath(), $newMediaFilePath); + $path = ($mediaFile->getPath()); + $dir = dirname($newMediaFilePath); + if (!file_exists($dir)) { + mkdir($dir); + } + copy($path, $newMediaFilePath); } $mediaFile->name = $newFileName; $newMedia = $newResource->addMedia($newMediaFilePath) - ->usingName($newFileName) - ->preservingOriginal() - ->withCustomProperties(['parent_id' => $originalResource->id]) - ->toMediaCollection('File'); + ->usingName($newFileName) + ->preservingOriginal() + ->withCustomProperties(['parent_id' => $originalResource->id]) + ->toMediaCollection('File'); $newCopy = new \App\Models\Copy([ - 'id' => (string) Str::uuid(), - 'parent_id' => $originalResource->id, - 'hash_new' => $newMedia->id, - 'hash_old' => $mediaFile->id, + 'id' => (string) Str::uuid(), + 'parent_id' => $originalResource->id, + 'hash_new' => $newMedia->id, + 'hash_old' => $mediaFile->id, 'status' => "completed" ]); - - $newCopy->save(); + + $newCopy->save(); } foreach ($originalResource->getMedia('Preview') as $mediaFile) { $path_parts = pathinfo($mediaFile->file_name); - $newFileName = $path_parts['filename'] . '_copy.' . $path_parts['extension']; - $newMediaFilePath = $path_parts['dirname'] . '/' . $newFileName; - + $filename = $path_parts['filename']; + $extension = $path_parts['extension']; + + $newFileName = preg_replace('/_copy/', '', $filename) . '_copy.' . $extension; + $newMediaFilePath = storage_path('app/public/'.$mediaFile->uuid . '/' . $newFileName); + if (!file_exists($newMediaFilePath)) { - // Copy the file to the new location - copy($mediaFile->getPath(), $newMediaFilePath); + $path = $mediaFile->getPath(); + $dir = dirname($newMediaFilePath); + if (!file_exists($dir)) { + mkdir($dir); + } + copy($path, $newMediaFilePath); } $mediaFile->name = $newFileName; $newResource->addMedia($newMediaFilePath) - ->usingName($newFileName) - ->withCustomProperties(['parent_id' => $originalResource->id]) - ->preservingOriginal() - ->toMediaCollection('Preview'); - + ->usingName($newFileName) + ->withCustomProperties(['parent_id' => $originalResource->id]) + ->preservingOriginal() + ->toMediaCollection('Preview'); + $newCopy = new \App\Models\Copy([ - 'id' => (string) Str::uuid(), - 'parent_id' => $originalResource->id, - 'hash_new' => $newMedia->uuid, - 'hash_old' => $mediaFile->uuid, - 'status' => 'completed' + 'id' => (string) Str::uuid(), + 'parent_id' => $originalResource->id, + 'hash_new' => $newMedia->uuid, + 'hash_old' => $mediaFile->uuid, + 'status' => 'completed' ]); - - $newCopy->save(); - + + $newCopy->save(); } foreach ($originalResource->categories as $category) { $newResource->categories()->attach($category); } - + foreach ($originalResource->tags as $tag) { $newResource->tags()->attach($tag); } diff --git a/backend/config/logging.php b/backend/config/logging.php index 6aa77fe2..a1e188ee 100644 --- a/backend/config/logging.php +++ b/backend/config/logging.php @@ -99,6 +99,11 @@ 'emergency' => [ 'path' => storage_path('logs/laravel.log'), ], + 'avif_conversion' => [ + 'driver' => 'single', + 'path' => storage_path('logs/avif_conversion.log'), + 'level' => 'info', + ], ], ];