middleware(['auth', 'max_post_size_exceeded']); View::share('is_admin', true); } public function analyse($photoId, $queue_token) { $this->authorizeAccessToAdminPanel(); /** @var Photo $photo */ $photo = Photo::where('id', intval($photoId))->first(); if (is_null($photo)) { App::abort(404); return null; } $result = ['is_successful' => false, 'message' => '']; try { if (UserConfig::isImageProcessingQueueEnabled()) { // Find the last record that is analysing this photo $photoQueueItem = QueueItem::where('photo_id', $photo->id) ->orderBy('queued_at', 'desc') ->limit(1) ->first(); $timeToWait = 60; $timeWaited = 0; $continueToMonitor = true; while ($continueToMonitor && $timeWaited < $timeToWait) { $continueToMonitor = is_null($photoQueueItem->completed_at); if ($continueToMonitor) { sleep(1); $timeWaited++; $photoQueueItem = QueueItem::where('id', $photoQueueItem->id)->first(); $continueToMonitor = is_null($photoQueueItem->completed_at); } } $didComplete = !is_null($photoQueueItem->completed_at); if (!$didComplete) { $result['message'] = 'Timed out waiting for queue processing.'; } else if (!$photoQueueItem->is_successful) { $result['is_successful'] = false; $result['message'] = $photoQueueItem->error_message; // Remove the photo from the album if it was newly-uploaded and couldn't be processed if (intval($photo->metadata_version) === 0) { $photo->delete(); } } else { $result['is_successful'] = true; } } else { /* IF CHANGING THIS LOGIC, ALSO CHECK ProcessQueueCommand::processPhotoAnalyseMessage */ $photoService = new PhotoService($photo); $photoService->analyse($queue_token); // Log an activity record for the user's feed (remove an existing one as the date may have changed) $this->removeExistingActivityRecords($photo, 'photo.taken'); if (!is_null($photo->taken_at)) { // Log an activity record for the user's feed $this->createActivityRecord($photo, 'photo.taken', $photo->taken_at); } $result['is_successful'] = true; } } catch (\Exception $ex) { $result['is_successful'] = false; $result['message'] = $ex->getMessage(); // Remove the photo if it cannot be analysed (only if there isn't currently a version of metadata) $photo->delete(); } return response()->json($result); } /** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ public function index() { // } /** * Show the form for creating a new resource. * * @return \Illuminate\Http\Response */ public function create() { // } /** * Remove the specified resource from storage. * * @param int $id * @return \Illuminate\Http\Response */ public function destroy(Request $request, $id) { $this->authorizeAccessToAdminPanel(); $photo = $this->loadPhoto($id, 'delete'); $photoService = new PhotoService($photo); $photoService->delete(); $request->session()->flash('success', trans('admin.delete_photo_successful_message', ['name' => $photo->name])); } public function flip($photoId, $horizontal, $vertical) { $this->authorizeAccessToAdminPanel(); settype($horizontal, 'boolean'); settype($vertical, 'boolean'); $photo = $this->loadPhoto($photoId, 'manipulate'); $photoService = new PhotoService($photo); $photoService->flip($horizontal, $vertical); // Log an activity record for the user's feed $this->createActivityRecord($photo, 'photo.edited'); } public function move(Request $request, $photoId) { $this->authorizeAccessToAdminPanel(); $photo = $this->loadPhoto($photoId, 'manipulate'); $newAlbum = Album::where('id', intval($request->get('new_album_id')))->first(); if (is_null($newAlbum)) { App::abort(404); } $messageData = ['name' => $photo->name, 'album' => $newAlbum->name]; if ($newAlbum->id == $photo->album_id) { $request->session()->flash('warning', trans('admin.move_failed_same_album', $messageData)); } else { $photoService = new PhotoService($photo); $photoService->changeAlbum($newAlbum); $request->session()->flash('success', trans('admin.move_successful_message', $messageData)); } } public function reAnalyse($id, $queue_token) { $this->authorizeAccessToAdminPanel(); /** @var Photo $photo */ $photo = $this->loadPhoto($id); $result = ['is_successful' => false, 'message' => '']; try { $photoService = new PhotoService($photo); $photoService->downloadOriginalToFolder(FileHelper::getQueuePath($queue_token)); $photoService->analyse($queue_token); // Log an activity record for the user's feed (remove an existing one as the date may have changed) $this->removeExistingActivityRecords($photo, 'photo.taken'); if (!is_null($photo->taken_at)) { // Log an activity record for the user's feed $this->createActivityRecord($photo, 'photo.taken', $photo->taken_at); } $result['is_successful'] = true; } catch (\Exception $ex) { $result['is_successful'] = false; $result['message'] = $ex->getMessage(); // Unlike the analyse method, we don't remove the photo if it cannot be analysed } return response()->json($result); } public function regenerateThumbnails($photoId) { $this->authorizeAccessToAdminPanel(); $photo = $this->loadPhoto($photoId, 'change-metadata'); $result = ['is_successful' => false, 'message' => '']; try { $photoService = new PhotoService($photo); $photoService->regenerateThumbnails(); $result['is_successful'] = true; } catch (\Exception $ex) { $result['is_successful'] = false; $result['message'] = $ex->getMessage(); } return response()->json($result); } public function rotate($photoId, $angle) { $this->authorizeAccessToAdminPanel(); $photo = $this->loadPhoto($photoId, 'manipulate'); if ($angle != 90 && $angle != 180 && $angle != 270) { App::aport(400); return null; } $photoService = new PhotoService($photo); $photoService->rotate($angle); // Log an activity record for the user's feed $this->createActivityRecord($photo, 'photo.edited'); } /** * Store a newly created resource in storage. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function store(Request $request) { $this->authorizeAccessToAdminPanel(); $photoFiles = $request->files->get('photo'); // Load the linked album $album = $this->loadAlbum($request->get('album_id'), 'upload-photos'); $isSuccessful = false; // Create the folder to hold the analysis results if not already present $queueUid = $request->get('queue_token'); if (strlen($queueUid) == 0) { throw new \Exception('No queue_token value was provided!'); } $queueStorage = AnalysisQueueHelper::getStorageQueueSource(); foreach ($photoFiles as $photoFile) { $photoFile = UploadedFile::createFromBase($photoFile); if ($photoFile->getError() != UPLOAD_ERR_OK) { Log::error('Image upload failed.', ['error' => $photoFile->getError(), 'reason' => $photoFile->getErrorMessage()]); } else { if ($request->has('photo_id')) { // Photo ID provided (using the Replace Photo function) - use that record $photo = Photo::where('id', intval($request->get('photo_id')))->first(); $photo->raw_exif_data = null; $queuedFileName = $queueStorage->uploadToAnalysisQueue($photoFile, $queueUid, $photo->storage_file_name); $uploadedTempFile = new File($photoFile); $this->removeExistingActivityRecords($photo, 'photo.uploaded'); $this->removeExistingActivityRecords($photo, 'photo.taken'); $photo->file_name = $photoFile->getClientOriginalName(); $photo->mime_type = $uploadedTempFile->getMimeType(); $photo->file_size = $uploadedTempFile->getSize(); $photo->storage_file_name = basename($queuedFileName); } else { $queuedFileName = $queueStorage->uploadToAnalysisQueue($photoFile, $queueUid); $uploadedTempFile = new File($photoFile); $photo = new Photo(); $photo->album_id = $album->id; $photo->user_id = Auth::user()->id; $photo->name = pathinfo($photoFile->getClientOriginalName(), PATHINFO_FILENAME); $photo->file_name = $photoFile->getClientOriginalName(); $photo->mime_type = $uploadedTempFile->getMimeType(); $photo->file_size = $uploadedTempFile->getSize(); $photo->storage_file_name = basename($queuedFileName); } $photo->is_analysed = false; $photo->save(); // Log an activity record for the user's feed $this->createActivityRecord($photo, 'photo.uploaded'); // If queueing is enabled, store the photo in the queue now if (UserConfig::isImageProcessingQueueEnabled()) { $queueItem = new QueueItem([ 'batch_reference' => $queueUid, 'action_type' => 'photo.analyse', 'album_id' => $photo->album_id, 'photo_id' => $photo->id, 'user_id' => $this->getUser()->id, 'queued_at' => new \DateTime() ]); $queueItem->save(); $rabbitmq = new RabbitMQService(); $rabbitmq->queueItem($queueItem); } $isSuccessful = true; } } if ($request->isXmlHttpRequest()) { return response()->json(['is_successful' => $isSuccessful]); } else { return redirect(route('albums.analyse', [ 'id' => $album->id, 'queue_token' => $queueUid ])); } } public function storeBulk(Request $request) { $this->authorizeAccessToAdminPanel(); // Load the linked album $album = $this->loadAlbum($request->get('album_id')); if (is_null($request->files->get('archive'))) { $request->session()->flash('error', trans('admin.upload_bulk_no_file')); return redirect(route('albums.show', ['id' => $album->id])); } $archiveFile = UploadedFile::createFromBase($request->files->get('archive')); if ($archiveFile->getError() != UPLOAD_ERR_OK) { Log::error('Bulk image upload failed.', ['error' => $archiveFile->getError(), 'reason' => $archiveFile->getErrorMessage()]); $request->session()->flash('error', $archiveFile->getErrorMessage()); return redirect(route('albums.show', ['id' => $album->id])); } // Create the folder to hold the analysis results if not already present $queueUid = $request->get('queue_token'); if (strlen($queueUid) == 0) { throw new \Exception('No queue_token value was provided!'); } $temporaryFolder = sprintf('%s/%s', sys_get_temp_dir(), MiscHelper::randomString()); @mkdir($temporaryFolder); $queueStorage = AnalysisQueueHelper::getStorageQueueSource(); $mimeType = strtolower($archiveFile->getMimeType()); switch ($mimeType) { case 'application/zip': $zip = new \ZipArchive(); $zip->open($archiveFile->getPathname()); $zip->extractTo($temporaryFolder); $zip->close(); @unlink($archiveFile->getPathname()); break; default: $request->session()->flash('error', sprintf('The file type "%s" is not supported for bulk uploads.', $mimeType)); return redirect(route('albums.show', ['id' => $album->id])); } $di = new \RecursiveDirectoryIterator($temporaryFolder, \RecursiveDirectoryIterator::SKIP_DOTS); $recursive = new \RecursiveIteratorIterator($di); /** @var \SplFileInfo $fileInfo */ foreach ($recursive as $fileInfo) { if ($fileInfo->isDir()) { if ($fileInfo->getFilename() == '__MACOSX' || substr($fileInfo->getFilename(), 0, 1) == '.') { @rmdir($fileInfo->getPathname()); } continue; } if (substr($fileInfo->getFilename(), 0, 1) == '.') { // Temporary/hidden file - skip @unlink($fileInfo->getPathname()); continue; } $result = getimagesize($fileInfo->getPathname()); if ($result === false) { // Not an image file - skip @unlink($fileInfo->getPathname()); continue; } $photoFile = new File($fileInfo->getPathname()); $queuedFileName = $queueStorage->uploadToAnalysisQueue($photoFile, $queueUid); $photo = new Photo(); $photo->album_id = $album->id; $photo->user_id = Auth::user()->id; $photo->name = pathinfo($photoFile->getFilename(), PATHINFO_FILENAME); $photo->file_name = $photoFile->getFilename(); $photo->mime_type = $photoFile->getMimeType(); $photo->file_size = $photoFile->getSize(); $photo->is_analysed = false; $photo->storage_file_name = basename($queuedFileName); $photo->save(); // Log an activity record for the user's feed $this->createActivityRecord($photo, 'photo.uploaded'); // If queueing is enabled, store the photo in the queue now if (UserConfig::isImageProcessingQueueEnabled()) { $queueItem = new QueueItem([ 'batch_reference' => $queueUid, 'action_type' => 'photo.analyse', 'album_id' => $photo->album_id, 'photo_id' => $photo->id, 'user_id' => $this->getUser()->id, 'queued_at' => new \DateTime() ]); $queueItem->save(); $rabbitmq = new RabbitMQService(); $rabbitmq->queueItem($queueItem); } } return redirect(route('albums.analyse', [ 'id' => $album->id, 'queue_token' => $queueUid ])); } /** * Display the specified resource. * * @param int $id * @return \Illuminate\Http\Response */ public function show($id) { // } /** * Show the form for editing the specified resource. * * @param int $id * @return \Illuminate\Http\Response */ public function edit($id) { // } /** * Update the specified resource in storage. * * @param \Illuminate\Http\Request $request * @param int $id * @return \Illuminate\Http\Response */ public function update(Request $request, $id) { // } public function updateBulk(UpdatePhotosBulkRequest $request, $albumId) { $this->authorizeAccessToAdminPanel(); $album = $this->loadAlbum($albumId); if ($request->has('bulk-apply')) { $numberChanged = $this->applyBulkActions($request, $album); } else { $numberChanged = $this->updatePhotoDetails($request, $album); } $request->session()->flash( 'success', trans_choice( UserConfig::isImageProcessingQueueEnabled() ? 'admin.bulk_photos_changed_queued' : 'admin.bulk_photos_changed', $numberChanged, ['number' => $numberChanged] ) ); return redirect(route('albums.show', array('id' => $albumId, 'page' => $request->get('page', 1)))); } private function applyBulkActions(Request $request, Album $album) { $selectAllInAlbum = boolval($request->get('select-all-album')); $photosToProcess = []; if ($selectAllInAlbum) { foreach ($album->photos as $photo) { $photosToProcess[] = $photo; } } else { $photoIds = $request->get('select-photo'); if (is_null($photoIds) || !is_array($photoIds) || count($photoIds) == 0) { $request->session()->flash('warning', trans('admin.no_photo_selected_message')); return 0; } foreach ($photoIds as $photoId) { /** @var Photo $photo */ $photo = $album->photos()->where('id', intval($photoId))->first(); if (is_null($photo)) { continue; } $photosToProcess[] = $photo; } } $action = $request->get('bulk-action'); $numberChanged = 0; if (UserConfig::isImageProcessingQueueEnabled()) { $queueUid = MiscHelper::randomString(); foreach ($photosToProcess as $photo) { $queueItem = new QueueItem([ 'batch_reference' => $queueUid, 'action_type' => sprintf('photo.bulk_action.%s', strtolower($action)), 'album_id' => $photo->album_id, 'photo_id' => $photo->id, 'user_id' => $this->getUser()->id, 'queued_at' => new \DateTime() ]); if (strtolower($action) == 'change_album') { $queueItem->new_album_id = intval($request->get('new-album-id')); $newAlbumId = intval($request->get('new-album-id')); if ($newAlbumId == $photo->album_id) { // Photo already belongs to this album, don't move continue; } } $queueItem->save(); $rabbitmq = new RabbitMQService(); $rabbitmq->queueItem($queueItem); $numberChanged++; } } else { foreach ($photosToProcess as $photo) { $changed = false; $photoService = new PhotoService($photo); $doNotSave = false; /* IF CHANGING THIS LOGIC OR ADDING EXTRA case OPTIONS, ALSO CHECK ProcessQueueCommand::processQueueItem AND ProcessQueueCommand::processPhotoBulkActionMessage */ switch (strtolower($action)) { case 'change_album': if (Auth::user()->can('change-metadata', $photo)) { $newAlbumId = intval($request->get('new-album-id')); if ($newAlbumId == $photo->album_id) { // Photo already belongs to this album, don't move continue 2; } $newAlbum = $this->loadAlbum($newAlbumId, 'upload-photos'); $photoService->changeAlbum($newAlbum); $changed = true; } break; case 'delete': if (Auth::user()->can('delete', $photo)) { $photoService->delete(); $doNotSave = true; $changed = true; } break; case 'flip_both': if (Auth::user()->can('manipulate', $photo)) { $photoService->flip(true, true); $changed = true; } break; case 'flip_horizontal': if (Auth::user()->can('manipulate', $photo)) { $photoService->flip(true, false); $changed = true; } break; case 'flip_vertical': if (Auth::user()->can('manipulate', $photo)) { $photoService->flip(false, true); $changed = true; } break; case 'refresh_thumbnails': if (Auth::user()->can('change-metadata', $photo)) { $photoService->regenerateThumbnails(); $changed = true; } break; case 'rotate_left': if (Auth::user()->can('manipulate', $photo)) { $photoService->rotate(90); $changed = true; } break; case 'rotate_right': if (Auth::user()->can('manipulate', $photo)) { $photoService->rotate(270); $changed = true; } break; } if (!$doNotSave) { $photo->save(); } if (!in_array(strtolower($action), ['delete', 'refresh_thumbnails', 'change_album'])) { // Log an activity record for the user's feed $this->createActivityRecord($photo, 'photo.edited'); } if ($changed) { $numberChanged++; } } } return $numberChanged; } private function createActivityRecord(Photo $photo, $type, $activityDateTime = null) { if (is_null($activityDateTime)) { $activityDateTime = new \DateTime(); } $userActivity = new UserActivity(); $userActivity->user_id = $this->getUser()->id; $userActivity->activity_at = $activityDateTime; $userActivity->type = $type; $userActivity->photo_id = $photo->id; $userActivity->save(); } /** * @param $id * @return Album */ private function loadAlbum($id, $permission = 'edit') { $album = Album::where('id', intval($id))->first(); if (is_null($album)) { App::abort(404); return null; } $this->authorize($permission, $album); return $album; } /** * @param $id * @param string|null $permission * @return Photo */ private function loadPhoto($id, $permission = null) { $photo = Photo::where('id', intval($id))->first(); if (is_null($photo)) { App::abort(404); return null; } if (!is_null($permission)) { $this->authorize($permission, $photo); } return $photo; } private function removeExistingActivityRecords(Photo $photo, $type) { $existingFeedRecords = UserActivity::where([ 'user_id' => $this->getUser()->id, 'photo_id' => $photo->id, 'type' => $type ])->get(); foreach ($existingFeedRecords as $existingFeedRecord) { $existingFeedRecord->delete(); } } private function updatePhotoDetails(Request $request, Album $album) { $numberChanged = 0; $photos = $request->get('photo'); foreach ($photos as $photoId => $value) { /** @var Photo $photo */ $photo = $album->photos()->where('id', intval($photoId))->first(); if (is_null($photo) || !Auth::user()->can('change-metadata', $photo)) { continue; } $photo->fill($value); // Update the photo labels $labelString = trim($value['labels']); $photo->labels()->detach(); if (strlen($labelString) > 0) { app(LabelController::class)->applyLabelsToPhoto($photo, $labelString); } // Save all changes $photo->save(); $numberChanged++; } return $numberChanged; } }