refactor: rework controller (1)

Signed-off-by: Varun Patil <varunpatil@ucla.edu>
pull/563/head
Varun Patil 2023-03-23 13:32:23 -07:00
parent bd6aaeee3a
commit 0e385d2283
17 changed files with 781 additions and 783 deletions

View File

@ -24,8 +24,7 @@ declare(strict_types=1);
namespace OCA\Memories\ClustersBackend; namespace OCA\Memories\ClustersBackend;
use OCA\Memories\Db\TimelineQuery; use OCA\Memories\Db\TimelineQuery;
use OCA\Memories\Errors; use OCA\Memories\Exceptions;
use OCA\Memories\HttpResponseException;
use OCP\App\IAppManager; use OCP\App\IAppManager;
use OCP\IUserSession; use OCP\IUserSession;
@ -95,7 +94,7 @@ class AlbumsBackend extends Backend
// Get album // Get album
$album = $this->timelineQuery->getAlbumIfAllowed($this->userId, $name); $album = $this->timelineQuery->getAlbumIfAllowed($this->userId, $name);
if (null === $album) { if (null === $album) {
throw new HttpResponseException(Errors::NotFound("album {$name}")); throw Exceptions::NotFound("album {$name}");
} }
// Get files // Get files

View File

@ -23,8 +23,9 @@ declare(strict_types=1);
namespace OCA\Memories\Controller; namespace OCA\Memories\Controller;
use OCA\Memories\Errors; use OCA\Memories\Exceptions;
use OCA\Memories\Exif; use OCA\Memories\Exif;
use OCA\Memories\Util;
use OCP\AppFramework\Http; use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\JSONResponse;
use OCP\Files\Folder; use OCP\Files\Folder;
@ -40,142 +41,130 @@ class ArchiveController extends GenericApiController
*/ */
public function archive(string $id): Http\Response public function archive(string $id): Http\Response
{ {
$user = $this->userSession->getUser(); return Util::guardEx(function () use ($id) {
if (null === $user) { $uid = Util::getUID();
return Errors::NotLoggedIn(); $userFolder = Util::getUserFolder();
}
$uid = $user->getUID();
$userFolder = $this->rootFolder->getUserFolder($uid);
// Check for permissions and get numeric Id // Check for permissions and get numeric Id
$file = $userFolder->getById((int) $id); $file = $userFolder->getById((int) $id);
if (0 === \count($file)) { if (0 === \count($file)) {
return Errors::NotFound("file id {$id}"); throw Exceptions::NotFound("file id {$id}");
}
$file = $file[0];
// Check if user has permissions
if (!$file->isUpdateable()) {
return Errors::ForbiddenFileUpdate($file->getName());
}
// Create archive folder in the root of the user's configured timeline
$configPath = Exif::removeExtraSlash(Exif::getPhotosPath($this->config, $uid));
$configPaths = explode(';', $configPath);
$timelineFolders = [];
$timelinePaths = [];
// Get all timeline paths
foreach ($configPaths as $path) {
try {
$f = $userFolder->get($path);
$timelineFolders[] = $f;
$timelinePaths[] = $f->getPath();
} catch (\OCP\Files\NotFoundException $e) {
return new JSONResponse(['message' => 'Timeline folder not found'], Http::STATUS_NOT_FOUND);
} }
} $file = $file[0];
// Bubble up from file until we reach the correct folder // Check if user has permissions
$fileStorageId = $file->getStorage()->getId(); if (!$file->isUpdateable()) {
$parent = $file->getParent(); throw Exceptions::ForbiddenFileUpdate($file->getName());
$isArchived = false;
while (true) {
if (null === $parent) {
throw new \Exception('Cannot get correct parent of file');
} }
// Hit a timeline folder // Create archive folder in the root of the user's configured timeline
if (\in_array($parent->getPath(), $timelinePaths, true)) { $configPath = Exif::removeExtraSlash(Exif::getPhotosPath($this->config, $uid));
break; $configPaths = explode(';', $configPath);
$timelineFolders = [];
$timelinePaths = [];
// Get all timeline paths
foreach ($configPaths as $path) {
try {
$f = $userFolder->get($path);
$timelineFolders[] = $f;
$timelinePaths[] = $f->getPath();
} catch (\OCP\Files\NotFoundException $e) {
throw Exceptions::NotFound("timeline folder {$path}");
}
} }
// Hit the user's root folder // Bubble up from file until we reach the correct folder
if ($parent->getPath() === $userFolder->getPath()) { $fileStorageId = $file->getStorage()->getId();
break; $parent = $file->getParent();
} $isArchived = false;
while (true) {
if (null === $parent) {
throw new \Exception('Cannot get correct parent of file');
}
// Hit a storage root // Hit a timeline folder
try { if (\in_array($parent->getPath(), $timelinePaths, true)) {
if ($parent->getParent()->getStorage()->getId() !== $fileStorageId) {
break; break;
} }
} catch (\OCP\Files\NotFoundException $e) {
break;
}
// Hit an archive folder root // Hit the user's root folder
if ($parent->getName() === \OCA\Memories\Util::$ARCHIVE_FOLDER) { if ($parent->getPath() === $userFolder->getPath()) {
$isArchived = true; break;
break;
}
$parent = $parent->getParent();
}
// Get path of current file relative to the parent folder
$relativeFilePath = $parent->getRelativePath($file->getPath());
// Check if we want to archive or unarchive
$body = $this->request->getParams();
$unarchive = isset($body['archive']) && false === $body['archive'];
if ($isArchived && !$unarchive) {
return new JSONResponse(['message' => 'File already archived'], Http::STATUS_BAD_REQUEST);
}
if (!$isArchived && $unarchive) {
return new JSONResponse(['message' => 'File not archived'], Http::STATUS_BAD_REQUEST);
}
// Final path of the file including the file name
$destinationPath = '';
// Get if the file is already in the archive (relativePath starts with archive)
if ($isArchived) {
// file already in archive, remove it
$destinationPath = $relativeFilePath;
$parent = $parent->getParent();
} else {
// file not in archive, put it in there
$af = \OCA\Memories\Util::$ARCHIVE_FOLDER;
$destinationPath = Exif::removeExtraSlash($af.$relativeFilePath);
}
// Remove the filename
$destinationFolders = array_filter(explode('/', $destinationPath));
array_pop($destinationFolders);
// Create folder tree
$folder = $parent;
foreach ($destinationFolders as $folderName) {
try {
$existingFolder = $folder->get($folderName.'/');
if (!$existingFolder instanceof Folder) {
throw new \OCP\Files\NotFoundException('Not a folder');
} }
$folder = $existingFolder;
} catch (\OCP\Files\NotFoundException $e) { // Hit a storage root
try { try {
$folder = $folder->newFolder($folderName); if ($parent->getParent()->getStorage()->getId() !== $fileStorageId) {
} catch (\OCP\Files\NotPermittedException $e) { break;
return new JSONResponse(['message' => 'Failed to create folder'], Http::STATUS_FORBIDDEN); }
} catch (\OCP\Files\NotFoundException $e) {
break;
}
// Hit an archive folder root
if ($parent->getName() === \OCA\Memories\Util::$ARCHIVE_FOLDER) {
$isArchived = true;
break;
}
$parent = $parent->getParent();
}
// Get path of current file relative to the parent folder
$relativeFilePath = $parent->getRelativePath($file->getPath());
// Check if we want to archive or unarchive
$body = $this->request->getParams();
$unarchive = isset($body['archive']) && false === $body['archive'];
if ($isArchived && !$unarchive) {
throw Exceptions::BadRequest('File already archived');
}
if (!$isArchived && $unarchive) {
throw Exceptions::BadRequest('File not archived');
}
// Final path of the file including the file name
$destinationPath = '';
// Get if the file is already in the archive (relativePath starts with archive)
if ($isArchived) {
// file already in archive, remove it
$destinationPath = $relativeFilePath;
$parent = $parent->getParent();
} else {
// file not in archive, put it in there
$af = \OCA\Memories\Util::$ARCHIVE_FOLDER;
$destinationPath = Exif::removeExtraSlash($af.$relativeFilePath);
}
// Remove the filename
$destinationFolders = array_filter(explode('/', $destinationPath));
array_pop($destinationFolders);
// Create folder tree
$folder = $parent;
foreach ($destinationFolders as $folderName) {
try {
$existingFolder = $folder->get($folderName.'/');
if (!$existingFolder instanceof Folder) {
throw Exceptions::NotFound('Not a folder: '.$existingFolder->getPath());
}
$folder = $existingFolder;
} catch (\OCP\Files\NotFoundException $e) {
try {
$folder = $folder->newFolder($folderName);
} catch (\OCP\Files\NotPermittedException $e) {
throw Exceptions::ForbiddenFileUpdate($folder->getPath().' [create]');
}
} }
} }
}
// Move file to archive folder // Move file to archive folder
try {
$file->move($folder->getPath().'/'.$file->getName()); $file->move($folder->getPath().'/'.$file->getName());
} catch (\OCP\Files\NotPermittedException $e) {
return new JSONResponse(['message' => 'Failed to move file'], Http::STATUS_FORBIDDEN);
} catch (\OCP\Files\NotFoundException $e) {
return new JSONResponse(['message' => 'File not found'], Http::STATUS_INTERNAL_SERVER_ERROR);
} catch (\OCP\Files\InvalidPathException $e) {
return new JSONResponse(['message' => 'Invalid path'], Http::STATUS_INTERNAL_SERVER_ERROR);
} catch (\OCP\Lock\LockedException $e) {
return new JSONResponse(['message' => 'File is locked'], Http::STATUS_INTERNAL_SERVER_ERROR);
}
return new JSONResponse([], Http::STATUS_OK); return new JSONResponse([], Http::STATUS_OK);
});
} }
} }

View File

@ -24,8 +24,8 @@ declare(strict_types=1);
namespace OCA\Memories\Controller; namespace OCA\Memories\Controller;
use OCA\Memories\ClustersBackend\Backend; use OCA\Memories\ClustersBackend\Backend;
use OCA\Memories\Errors; use OCA\Memories\Exceptions;
use OCA\Memories\HttpResponseException; use OCA\Memories\Util;
use OCP\AppFramework\Http; use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataDisplayResponse; use OCP\AppFramework\Http\DataDisplayResponse;
use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\JSONResponse;
@ -42,7 +42,7 @@ class ClustersController extends GenericApiController
*/ */
public function list(string $backend): Http\Response public function list(string $backend): Http\Response
{ {
return $this->guardEx(function () use ($backend) { return Util::guardEx(function () use ($backend) {
$this->init($backend); $this->init($backend);
$list = $this->backend->getClusters(); $list = $this->backend->getClusters();
@ -60,7 +60,7 @@ class ClustersController extends GenericApiController
*/ */
public function preview(string $backend, string $name): Http\Response public function preview(string $backend, string $name): Http\Response
{ {
return $this->guardEx(function () use ($backend, $name) { return Util::guardEx(function () use ($backend, $name) {
$this->init($backend); $this->init($backend);
// Get list of some photos in this cluster // Get list of some photos in this cluster
@ -88,7 +88,7 @@ class ClustersController extends GenericApiController
*/ */
public function download(string $backend, string $name): Http\Response public function download(string $backend, string $name): Http\Response
{ {
return $this->guardEx(function () use ($backend, $name) { return Util::guardEx(function () use ($backend, $name) {
$this->init($backend); $this->init($backend);
// Get list of all files in this cluster // Get list of all files in this cluster
@ -111,7 +111,7 @@ class ClustersController extends GenericApiController
{ {
$user = $this->userSession->getUser(); $user = $this->userSession->getUser();
if (null === $user) { if (null === $user) {
throw new HttpResponseException(Errors::NotLoggedIn()); throw Exceptions::NotLoggedIn();
} }
if (\array_key_exists($backend, Backend::$backends)) { if (\array_key_exists($backend, Backend::$backends)) {
@ -121,13 +121,13 @@ class ClustersController extends GenericApiController
} }
if (!$this->backend->isEnabled()) { if (!$this->backend->isEnabled()) {
throw new HttpResponseException(Errors::NotEnabled($this->backend->appName())); throw Exceptions::NotEnabled($this->backend->appName());
} }
if (property_exists($this->backend, 'root')) { if (property_exists($this->backend, 'root')) {
$this->backend->root = $this->getRequestRoot(); $this->backend->root = $this->getRequestRoot();
if ($this->backend->root->isEmpty()) { if ($this->backend->root->isEmpty()) {
throw new HttpResponseException(Errors::NoRequestRoot()); throw Exceptions::NoRequestRoot();
} }
} }
} }
@ -141,7 +141,7 @@ class ClustersController extends GenericApiController
$previewManager = \OC::$server->get(\OCP\IPreview::class); $previewManager = \OC::$server->get(\OCP\IPreview::class);
// Try to get a preview // Try to get a preview
$userFolder = $this->rootFolder->getUserFolder($this->getUID()); $userFolder = Util::getUserFolder();
foreach ($photos as $img) { foreach ($photos as $img) {
// Get the file // Get the file
$files = $userFolder->getById($this->backend->getFileId($img)); $files = $userFolder->getById($this->backend->getFileId($img));
@ -172,6 +172,6 @@ class ClustersController extends GenericApiController
} }
} }
return Errors::NotFound('preview from photos list'); throw Exceptions::NotFound('preview from photos list');
} }
} }

View File

@ -24,7 +24,8 @@ declare(strict_types=1);
namespace OCA\Memories\Controller; namespace OCA\Memories\Controller;
use OCA\Memories\Db\TimelineRoot; use OCA\Memories\Db\TimelineRoot;
use OCA\Memories\Errors; use OCA\Memories\Exceptions;
use OCA\Memories\Util;
use OCP\AppFramework\Http; use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\JSONResponse;
@ -39,24 +40,10 @@ class DaysController extends GenericApiController
*/ */
public function days(): Http\Response public function days(): Http\Response
{ {
// Get the folder to show return Util::guardEx(function () {
try { $uid = $this->getShareToken() ? '' : Util::getUID();
$uid = $this->getUID();
} catch (\Exception $e) {
return Errors::NotLoggedIn();
}
// Get the folder to show
$root = null;
try {
$root = $this->getRequestRoot(); $root = $this->getRequestRoot();
} catch (\Exception $e) {
return Errors::Generic($e);
}
// Run actual query
try {
$list = $this->timelineQuery->getDays( $list = $this->timelineQuery->getDays(
$root, $root,
$uid, $uid,
@ -84,9 +71,7 @@ class DaysController extends GenericApiController
} }
return new JSONResponse($list, Http::STATUS_OK); return new JSONResponse($list, Http::STATUS_OK);
} catch (\Exception $e) { });
return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
}
} }
/** /**
@ -96,41 +81,32 @@ class DaysController extends GenericApiController
*/ */
public function day(string $id): Http\Response public function day(string $id): Http\Response
{ {
// Get user return Util::guardEx(function () use ($id) {
$uid = $this->getUID(); $uid = $this->getShareToken() ? '' : Util::getUID();
// Check for wildcard // Check for wildcard
$dayIds = []; $dayIds = [];
if ('*' === $id) { if ('*' === $id) {
$dayIds = null; $dayIds = null;
} else { } else {
// Split at commas and convert all parts to int // Split at commas and convert all parts to int
$dayIds = array_map(function ($part) { $dayIds = array_map(fn ($p) => (int) $p, explode(',', $id));
return (int) $part; }
}, explode(',', $id));
}
// Check if $dayIds is empty // Check if $dayIds is empty
if (null !== $dayIds && 0 === \count($dayIds)) { if (null !== $dayIds && 0 === \count($dayIds)) {
return new JSONResponse([], Http::STATUS_OK); return new JSONResponse([], Http::STATUS_OK);
} }
// Get the folder to show // Get the folder to show
$root = null;
try {
$root = $this->getRequestRoot(); $root = $this->getRequestRoot();
} catch (\Exception $e) {
return Errors::Generic($e);
}
// Convert to actual dayIds if month view // Convert to actual dayIds if month view
if ($this->isMonthView()) { if ($this->isMonthView()) {
$dayIds = $this->timelineQuery->monthIdToDayIds((int) $dayIds[0]); $dayIds = $this->timelineQuery->monthIdToDayIds((int) $dayIds[0]);
} }
// Run actual query // Run actual query
try {
$list = $this->timelineQuery->getDay( $list = $this->timelineQuery->getDay(
$root, $root,
$uid, $uid,
@ -153,9 +129,7 @@ class DaysController extends GenericApiController
} }
return new JSONResponse($list, Http::STATUS_OK); return new JSONResponse($list, Http::STATUS_OK);
} catch (\Exception $e) { });
return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
}
} }
/** /**
@ -165,12 +139,14 @@ class DaysController extends GenericApiController
*/ */
public function dayPost(): Http\Response public function dayPost(): Http\Response
{ {
$id = $this->request->getParam('body_ids'); return Util::guardEx(function () {
if (null === $id) { $id = $this->request->getParam('body_ids');
return Errors::MissingParameter('body_ids'); if (null === $id) {
} throw Exceptions::MissingParameter('body_ids');
}
return $this->day($id); return $this->day($id);
});
} }
/** /**

View File

@ -24,7 +24,8 @@ declare(strict_types=1);
namespace OCA\Memories\Controller; namespace OCA\Memories\Controller;
use bantu\IniGetWrapper\IniGetWrapper; use bantu\IniGetWrapper\IniGetWrapper;
use OCA\Memories\Errors; use OCA\Memories\Exceptions;
use OCA\Memories\Util;
use OCP\AppFramework\Http; use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\JSONResponse;
use OCP\ISession; use OCP\ISession;
@ -44,16 +45,18 @@ class DownloadController extends GenericApiController
*/ */
public function request(): Http\Response public function request(): Http\Response
{ {
// Get ids from body return Util::guardEx(function () {
$files = $this->request->getParam('files'); // Get ids from body
if (null === $files || !\is_array($files)) { $files = $this->request->getParam('files');
return Errors::MissingParameter('files'); if (null === $files || !\is_array($files)) {
} throw Exceptions::MissingParameter('files');
}
// Return id // Return id
$handle = self::createHandle('memories', $files); $handle = self::createHandle('memories', $files);
return new JSONResponse(['handle' => $handle]); return new JSONResponse(['handle' => $handle]);
});
} }
/** /**
@ -83,36 +86,36 @@ class DownloadController extends GenericApiController
*/ */
public function file(string $handle): Http\Response public function file(string $handle): Http\Response
{ {
// Get ids from request return Util::guardEx(function () use ($handle) {
$session = \OC::$server->get(ISession::class); // Get ids from request
$key = "memories_download_{$handle}"; $session = \OC::$server->get(ISession::class);
$info = $session->get($key); $key = "memories_download_{$handle}";
$session->remove($key); $info = $session->get($key);
$session->remove($key);
if (null === $info) { if (null === $info) {
return Errors::NotFound('handle'); return Exceptions::NotFound('handle');
} }
$name = $info[0].'-'.date('YmdHis'); $name = $info[0].'-'.date('YmdHis');
$fileIds = $info[1]; $fileIds = $info[1];
/** @var int[] $fileIds */ /** @var int[] $fileIds */
$fileIds = array_filter(array_map('intval', $fileIds), function (int $id): bool { $fileIds = array_filter(array_map('intval', $fileIds), fn ($id) => $id > 0);
return $id > 0;
// Check if we have any valid ids
if (0 === \count($fileIds)) {
return Exceptions::NotFound('file IDs');
}
// Download single file
if (1 === \count($fileIds)) {
return $this->one($fileIds[0]);
}
// Download multiple files
$this->multiple($name, $fileIds); // exits
}); });
// Check if we have any valid ids
if (0 === \count($fileIds)) {
return Errors::NotFound('file IDs');
}
// Download single file
if (1 === \count($fileIds)) {
return $this->one($fileIds[0]);
}
// Download multiple files
$this->multiple($name, $fileIds); // exits
} }
/** /**
@ -124,47 +127,45 @@ class DownloadController extends GenericApiController
*/ */
public function one(int $fileid): Http\Response public function one(int $fileid): Http\Response
{ {
$file = $this->getUserFile($fileid); return Util::guardEx(function () use ($fileid) {
if (null === $file) { $file = $this->getUserFile($fileid);
return Errors::NotFoundFile($fileid); if (null === $file) {
} return Exceptions::NotFoundFile($fileid);
}
// Get the owner's root folder // Get the owner's root folder
$owner = $file->getOwner()->getUID(); $owner = $file->getOwner()->getUID();
$userFolder = $this->rootFolder->getUserFolder($owner); $userFolder = Util::getUserFolder($owner);
// Get the file in the context of the owner // Get the file in the context of the owner
$ownerFile = $userFolder->getById($fileid); $ownerFile = $userFolder->getById($fileid);
if (0 === \count($ownerFile)) { if (0 === \count($ownerFile)) {
// This should never happen, since the file was already found earlier // This should never happen, since the file was already found earlier
// Except if it was deleted in the meantime ... // Except if it was deleted in the meantime ...
return new JSONResponse([ throw new \Exception('File not found in owner\'s root folder');
'message' => 'File not found in owner\'s root folder', }
], Http::STATUS_INTERNAL_SERVER_ERROR);
}
// Get DAV path of file relative to owner's root folder // Get DAV path of file relative to owner's root folder
$path = $userFolder->getRelativePath($ownerFile[0]->getPath()); $path = $userFolder->getRelativePath($ownerFile[0]->getPath());
if (null === $path) { if (null === $path) {
return new JSONResponse([ throw new \Exception('File path not found in owner\'s root folder');
'message' => 'File path not found in owner\'s root folder', }
], Http::STATUS_INTERNAL_SERVER_ERROR);
}
// Setup filesystem for owner // Setup filesystem for owner
\OC_Util::tearDownFS(); \OC_Util::tearDownFS();
\OC_Util::setupFS($owner); \OC_Util::setupFS($owner);
// HEAD and RANGE support // HEAD and RANGE support
$server_params = ['head' => 'HEAD' === $this->request->getMethod()]; $server_params = ['head' => 'HEAD' === $this->request->getMethod()];
if (isset($_SERVER['HTTP_RANGE'])) { if (isset($_SERVER['HTTP_RANGE'])) {
$server_params['range'] = $this->request->getHeader('Range'); $server_params['range'] = $this->request->getHeader('Range');
} }
// Write file to output and exit // Write file to output and exit
\OC_Files::get(\dirname($path), basename($path), $server_params); \OC_Files::get(\dirname($path), basename($path), $server_params);
exit; exit;
});
} }
/** /**
@ -173,7 +174,7 @@ class DownloadController extends GenericApiController
* @param string $name Name of zip file * @param string $name Name of zip file
* @param int[] $fileIds * @param int[] $fileIds
*/ */
private function multiple(string $name, array &$fileIds) private function multiple(string $name, array $fileIds)
{ {
// Disable time limit // Disable time limit
$executionTime = (int) \OC::$server->get(IniGetWrapper::class)->getNumeric('max_execution_time'); $executionTime = (int) \OC::$server->get(IniGetWrapper::class)->getNumeric('max_execution_time');

View File

@ -26,9 +26,7 @@ trait FoldersTrait
$folders = $view->getDirectoryContent($folder->getPath(), FileInfo::MIMETYPE_FOLDER, $folder); $folders = $view->getDirectoryContent($folder->getPath(), FileInfo::MIMETYPE_FOLDER, $folder);
// Sort by name // Sort by name
usort($folders, function ($a, $b) { usort($folders, fn ($a, $b) => strnatcmp($a->getName(), $b->getName()));
return strnatcmp($a->getName(), $b->getName());
});
// Process to response type // Process to response type
return [ return [

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace OCA\Memories\Controller; namespace OCA\Memories\Controller;
use OCA\Memories\Util;
use OCP\App\IAppManager; use OCP\App\IAppManager;
use OCP\IConfig; use OCP\IConfig;
@ -31,41 +32,12 @@ trait GenericApiControllerUtils
protected IAppManager $appManager; protected IAppManager $appManager;
protected IConfig $config; protected IConfig $config;
/** Get logged in user's UID or throw exception */
protected function getUID(): string
{
$user = $this->userSession->getUser();
if ($this->getShareToken()) {
$user = null;
} elseif (null === $user) {
throw new \Exception('User not logged in');
}
return $user ? $user->getUID() : '';
}
/**
* Runa function and catch exceptions to return HTTP response.
*
* @param mixed $function
*/
protected function guardEx($function): \OCP\AppFramework\Http\Response
{
try {
return $function();
} catch (\OCA\Memories\HttpResponseException $e) {
return $e->response;
} catch (\Exception $e) {
return \OCA\Memories\Errors::Generic($e);
}
}
/** /**
* Check if albums are enabled for this user. * Check if albums are enabled for this user.
*/ */
protected function albumsIsEnabled(): bool protected function albumsIsEnabled(): bool
{ {
return \OCA\Memories\Util::albumsIsEnabled($this->appManager); return Util::albumsIsEnabled($this->appManager);
} }
/** /**
@ -73,7 +45,7 @@ trait GenericApiControllerUtils
*/ */
protected function tagsIsEnabled(): bool protected function tagsIsEnabled(): bool
{ {
return \OCA\Memories\Util::tagsIsEnabled($this->appManager); return Util::tagsIsEnabled($this->appManager);
} }
/** /**
@ -81,13 +53,13 @@ trait GenericApiControllerUtils
*/ */
protected function recognizeIsEnabled(): bool protected function recognizeIsEnabled(): bool
{ {
return \OCA\Memories\Util::recognizeIsEnabled($this->appManager); return Util::recognizeIsEnabled($this->appManager);
} }
// Check if facerecognition is installed and enabled for this user. // Check if facerecognition is installed and enabled for this user.
protected function facerecognitionIsInstalled(): bool protected function facerecognitionIsInstalled(): bool
{ {
return \OCA\Memories\Util::facerecognitionIsInstalled($this->appManager); return Util::facerecognitionIsInstalled($this->appManager);
} }
/** /**
@ -95,7 +67,7 @@ trait GenericApiControllerUtils
*/ */
protected function facerecognitionIsEnabled(): bool protected function facerecognitionIsEnabled(): bool
{ {
return \OCA\Memories\Util::facerecognitionIsEnabled($this->config, $this->getUID()); return Util::facerecognitionIsEnabled($this->config, Util::getUID());
} }
/** /**
@ -103,6 +75,6 @@ trait GenericApiControllerUtils
*/ */
protected function placesIsEnabled(): bool protected function placesIsEnabled(): bool
{ {
return \OCA\Memories\Util::placesGISType() > 0; return Util::placesGISType() > 0;
} }
} }

View File

@ -24,8 +24,9 @@ declare(strict_types=1);
namespace OCA\Memories\Controller; namespace OCA\Memories\Controller;
use OCA\Memories\AppInfo\Application; use OCA\Memories\AppInfo\Application;
use OCA\Memories\Errors; use OCA\Memories\Exceptions;
use OCA\Memories\Exif; use OCA\Memories\Exif;
use OCA\Memories\Util;
use OCP\AppFramework\Http; use OCP\AppFramework\Http;
use OCP\AppFramework\Http\FileDisplayResponse; use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\JSONResponse;
@ -49,16 +50,16 @@ class ImageController extends GenericApiController
bool $a = false, bool $a = false,
string $mode = 'fill' string $mode = 'fill'
) { ) {
if (-1 === $id || 0 === $x || 0 === $y) { return Util::guardEx(function () use ($id, $x, $y, $a, $mode) {
return Errors::MissingParameter('id, x, y'); if (-1 === $id || 0 === $x || 0 === $y) {
} throw Exceptions::MissingParameter('id, x, y');
}
$file = $this->getUserFile($id); $file = $this->getUserFile($id);
if (!$file) { if (!$file) {
return Errors::NotFoundFile($id); throw Exceptions::NotFoundFile($id);
} }
try {
$preview = \OC::$server->get(\OCP\IPreview::class)->getPreview($file, $x, $y, !$a, $mode); $preview = \OC::$server->get(\OCP\IPreview::class)->getPreview($file, $x, $y, !$a, $mode);
$response = new FileDisplayResponse($preview, Http::STATUS_OK, [ $response = new FileDisplayResponse($preview, Http::STATUS_OK, [
'Content-Type' => $preview->getMimeType(), 'Content-Type' => $preview->getMimeType(),
@ -66,11 +67,7 @@ class ImageController extends GenericApiController
$response->cacheFor(3600 * 24, false, true); $response->cacheFor(3600 * 24, false, true);
return $response; return $response;
} catch (\OCP\Files\NotFoundException $e) { });
return Errors::NotFound('preview');
} catch (\InvalidArgumentException $e) {
return Errors::Generic($e);
}
} }
/** /**
@ -84,87 +81,85 @@ class ImageController extends GenericApiController
*/ */
public function multipreview() public function multipreview()
{ {
// read body to array return Util::guardEx(function () {
try { // read body to array
$body = file_get_contents('php://input'); $body = file_get_contents('php://input');
$files = json_decode($body, true); $files = json_decode($body, true);
} catch (\Exception $e) {
return new JSONResponse([], Http::STATUS_BAD_REQUEST);
}
/** @var \OCP\IPreview $previewManager */ /** @var \OCP\IPreview $previewManager */
$previewManager = \OC::$server->get(\OCP\IPreview::class); $previewManager = \OC::$server->get(\OCP\IPreview::class);
// For checking max previews // For checking max previews
$previewRoot = new \OC\Preview\Storage\Root( $previewRoot = new \OC\Preview\Storage\Root(
\OC::$server->get(IRootFolder::class), \OC::$server->get(IRootFolder::class),
\OC::$server->get(\OC\SystemConfig::class), \OC::$server->get(\OC\SystemConfig::class),
); );
// stream the response // stream the response
header('Content-Type: application/octet-stream'); header('Content-Type: application/octet-stream');
header('Expires: '.gmdate('D, d M Y H:i:s \G\M\T', time() + 7 * 3600 * 24)); header('Expires: '.gmdate('D, d M Y H:i:s \G\M\T', time() + 7 * 3600 * 24));
header('Cache-Control: max-age='. 7 * 3600 * 24 .', private'); header('Cache-Control: max-age='. 7 * 3600 * 24 .', private');
foreach ($files as $bodyFile) { foreach ($files as $bodyFile) {
if (!isset($bodyFile['reqid']) || !isset($bodyFile['fileid']) || !isset($bodyFile['x']) || !isset($bodyFile['y']) || !isset($bodyFile['a'])) { if (!isset($bodyFile['reqid']) || !isset($bodyFile['fileid']) || !isset($bodyFile['x']) || !isset($bodyFile['y']) || !isset($bodyFile['a'])) {
continue; continue;
} }
$reqid = $bodyFile['reqid']; $reqid = $bodyFile['reqid'];
$fileid = (int) $bodyFile['fileid']; $fileid = (int) $bodyFile['fileid'];
$x = (int) $bodyFile['x']; $x = (int) $bodyFile['x'];
$y = (int) $bodyFile['y']; $y = (int) $bodyFile['y'];
$a = '1' === $bodyFile['a']; $a = '1' === $bodyFile['a'];
if ($fileid <= 0 || $x <= 0 || $y <= 0) { if ($fileid <= 0 || $x <= 0 || $y <= 0) {
continue; continue;
} }
$file = $this->getUserFile($fileid); $file = $this->getUserFile($fileid);
if (!$file) { if (!$file) {
continue; continue;
} }
try { try {
// Make sure max preview exists // Make sure max preview exists
$fileId = (string) $file->getId(); $fileId = (string) $file->getId();
$folder = $previewRoot->getFolder($fileId); $folder = $previewRoot->getFolder($fileId);
$hasMax = false; $hasMax = false;
foreach ($folder->getDirectoryListing() as $preview) { foreach ($folder->getDirectoryListing() as $preview) {
$name = $preview->getName(); $name = $preview->getName();
if (str_contains($name, '-max')) { if (str_contains($name, '-max')) {
$hasMax = true; $hasMax = true;
break; break;
}
} }
} if (!$hasMax) {
if (!$hasMax) { continue;
}
// Add this preview to the response
$preview = $previewManager->getPreview($file, $x, $y, !$a, \OCP\IPreview::MODE_FILL);
$content = $preview->getContent();
if (empty($content)) {
continue;
}
ob_start();
echo json_encode([
'reqid' => $reqid,
'Content-Length' => \strlen($content),
'Content-Type' => $preview->getMimeType(),
]);
echo "\n";
echo $content;
ob_end_flush();
} catch (\OCP\Files\NotFoundException $e) {
continue;
} catch (\Exception $e) {
continue; continue;
} }
// Add this preview to the response
$preview = $previewManager->getPreview($file, $x, $y, !$a, \OCP\IPreview::MODE_FILL);
$content = $preview->getContent();
if (empty($content)) {
continue;
}
ob_start();
echo json_encode([
'reqid' => $reqid,
'Content-Length' => \strlen($content),
'Content-Type' => $preview->getMimeType(),
]);
echo "\n";
echo $content;
ob_end_flush();
} catch (\OCP\Files\NotFoundException $e) {
continue;
} catch (\Exception $e) {
continue;
} }
}
exit; exit;
});
} }
/** /**
@ -182,36 +177,38 @@ class ImageController extends GenericApiController
bool $current = false, bool $current = false,
bool $tags = false bool $tags = false
): Http\Response { ): Http\Response {
$file = $this->getUserFile((int) $id); return Util::guardEx(function () use ($id, $basic, $current, $tags) {
if (!$file) { $file = $this->getUserFile((int) $id);
return Errors::NotFoundFile($id); if (!$file) {
} throw Exceptions::NotFoundFile($id);
// Get the image info
$info = $this->timelineQuery->getInfoById($file->getId(), $basic);
// Allow these ony for logged in users
if (null !== $this->userSession->getUser()) {
// Get list of tags for this file
if ($tags) {
$info['tags'] = $this->getTags($file->getId());
} }
// Get latest exif data if requested // Get the image info
if ($current) { $info = $this->timelineQuery->getInfoById($file->getId(), $basic);
$info['current'] = Exif::getExifFromFile($file);
// Allow these ony for logged in users
if (null !== $this->userSession->getUser()) {
// Get list of tags for this file
if ($tags) {
$info['tags'] = $this->getTags($file->getId());
}
// Get latest exif data if requested
if ($current) {
$info['current'] = Exif::getExifFromFile($file);
}
} }
}
// Inject permissions and convert to string // Inject permissions and convert to string
$info['permissions'] = \OCA\Memories\Util::permissionsToStr($file->getPermissions()); $info['permissions'] = \OCA\Memories\Util::permissionsToStr($file->getPermissions());
// Inject other file parameters that are cheap to get now // Inject other file parameters that are cheap to get now
$info['mimetype'] = $file->getMimeType(); $info['mimetype'] = $file->getMimeType();
$info['size'] = $file->getSize(); $info['size'] = $file->getSize();
$info['basename'] = $file->getName(); $info['basename'] = $file->getName();
return new JSONResponse($info, Http::STATUS_OK); return new JSONResponse($info, Http::STATUS_OK);
});
} }
/** /**
@ -224,36 +221,30 @@ class ImageController extends GenericApiController
*/ */
public function setExif(string $id, array $raw): Http\Response public function setExif(string $id, array $raw): Http\Response
{ {
$file = $this->getUserFile((int) $id); return Util::guardEx(function () use ($id, $raw) {
if (!$file) { $file = $this->getUserFile((int) $id);
return Errors::NotFoundFile($id); if (!$file) {
} throw Exceptions::NotFoundFile($id);
}
// Check if user has permissions // Check if user has permissions
if (!$file->isUpdateable()) { if (!$file->isUpdateable() || Util::isEncryptionEnabled()) {
return Errors::ForbiddenFileUpdate($file->getName()); throw Exceptions::ForbiddenFileUpdate($file->getName());
} }
// Check for end-to-end encryption // Check if allowed to edit file
if (\OCA\Memories\Util::isEncryptionEnabled()) { $mime = $file->getMimeType();
return new JSONResponse(['message' => 'Cannot change encrypted file'], Http::STATUS_PRECONDITION_FAILED); if (!\in_array($mime, Exif::allowedEditMimetypes(), true)) {
} $name = $file->getName();
// Check if allowed to edit file throw Exceptions::Forbidden("Cannot edit file {$name} (blacklisted type {$mime})");
$mime = $file->getMimeType(); }
if (!\in_array($mime, Exif::allowedEditMimetypes(), true)) {
$name = $file->getName();
return new JSONResponse(['message' => "Cannot edit file {$name} (blacklisted type {$mime})"], Http::STATUS_PRECONDITION_FAILED); // Set the exif data
}
try {
Exif::setFileExif($file, $raw); Exif::setFileExif($file, $raw);
} catch (\Exception $e) {
return Errors::Generic($e);
}
return new JSONResponse([], Http::STATUS_OK); return new JSONResponse([], Http::STATUS_OK);
});
} }
/** /**
@ -269,35 +260,37 @@ class ImageController extends GenericApiController
*/ */
public function decodable(string $id): Http\Response public function decodable(string $id): Http\Response
{ {
$file = $this->getUserFile((int) $id); return Util::guardEx(function () use ($id) {
if (!$file) { $file = $this->getUserFile((int) $id);
return Errors::NotFoundFile($id); if (!$file) {
} throw Exceptions::NotFoundFile($id);
}
// Check if valid image // Check if valid image
$mimetype = $file->getMimeType(); $mimetype = $file->getMimeType();
if (!\in_array($mimetype, Application::IMAGE_MIMES, true)) { if (!\in_array($mimetype, Application::IMAGE_MIMES, true)) {
return Errors::ForbiddenFileUpdate($file->getName()); throw Exceptions::Forbidden('Not an image');
} }
/** @var string Blob of image */ /** @var string Blob of image */
$blob = $file->getContent(); $blob = $file->getContent();
// Convert image to JPEG if required // Convert image to JPEG if required
if (!\in_array($mimetype, ['image/png', 'image/webp', 'image/jpeg', 'image/gif'], true)) { if (!\in_array($mimetype, ['image/png', 'image/webp', 'image/jpeg', 'image/gif'], true)) {
$image = new \Imagick(); $image = new \Imagick();
$image->readImageBlob($blob); $image->readImageBlob($blob);
$image->setImageFormat('jpeg'); $image->setImageFormat('jpeg');
$image->setImageCompressionQuality(95); $image->setImageCompressionQuality(95);
$blob = $image->getImageBlob(); $blob = $image->getImageBlob();
$mimetype = $image->getImageMimeType(); $mimetype = $image->getImageMimeType();
} }
// Return the image // Return the image
$response = new Http\DataDisplayResponse($blob, Http::STATUS_OK, ['Content-Type' => $mimetype]); $response = new Http\DataDisplayResponse($blob, Http::STATUS_OK, ['Content-Type' => $mimetype]);
$response->cacheFor(3600 * 24, false, false); $response->cacheFor(3600 * 24, false, false);
return $response; return $response;
});
} }
/** /**
@ -306,7 +299,7 @@ class ImageController extends GenericApiController
private function getTags(int $fileId): array private function getTags(int $fileId): array
{ {
// Make sure tags are enabled // Make sure tags are enabled
if (!\OCA\Memories\Util::tagsIsEnabled($this->appManager)) { if (!Util::tagsIsEnabled($this->appManager)) {
return []; return [];
} }
@ -320,10 +313,9 @@ class ImageController extends GenericApiController
/** @var \OCP\SystemTag\ISystemTag[] */ /** @var \OCP\SystemTag\ISystemTag[] */
$tags = $tagManager->getTagsByIds($tagIds); $tags = $tagManager->getTagsByIds($tagIds);
return array_map(function ($tag) { $visible = array_filter($tags, fn ($t) => $t->isUserVisible());
return $tag->getName();
}, array_filter($tags, function ($tag) { // Get the tag names
return $tag->isUserVisible(); return array_map(fn ($t) => $t->getName(), $visible);
}));
} }
} }

View File

@ -23,7 +23,8 @@ declare(strict_types=1);
namespace OCA\Memories\Controller; namespace OCA\Memories\Controller;
use OCA\Memories\Errors; use OCA\Memories\Exceptions;
use OCA\Memories\Util;
use OCP\AppFramework\Http; use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\JSONResponse;
@ -34,35 +35,27 @@ class MapController extends GenericApiController
*/ */
public function clusters(): Http\Response public function clusters(): Http\Response
{ {
// Get the folder to show return Util::guardEx(function () {
$root = null; // Get the folder to show
try {
$root = $this->getRequestRoot(); $root = $this->getRequestRoot();
} catch (\Exception $e) {
return Errors::NoRequestRoot();
}
// Make sure we have bounds and zoom level // Make sure we have bounds and zoom level
// Zoom level is used to determine the grid length // Zoom level is used to determine the grid length
$bounds = $this->request->getParam('bounds'); $bounds = $this->request->getParam('bounds');
$zoomLevel = $this->request->getParam('zoom'); $zoomLevel = $this->request->getParam('zoom');
if (!$bounds || !$zoomLevel || !is_numeric($zoomLevel)) { if (!$bounds || !$zoomLevel || !is_numeric($zoomLevel)) {
return Errors::MissingParameter('bounds or zoom'); throw Exceptions::MissingParameter('bounds or zoom');
} }
// A tweakable parameter to determine the number of boxes in the map // A tweakable parameter to determine the number of boxes in the map
// Note: these parameters need to be changed in MapSplitMatter.vue as well // Note: these parameters need to be changed in MapSplitMatter.vue as well
$clusterDensity = 1; $clusterDensity = 1;
$gridLen = 180.0 / (2 ** $zoomLevel * $clusterDensity); $gridLen = 180.0 / (2 ** $zoomLevel * $clusterDensity);
try {
$clusters = $this->timelineQuery->getMapClusters($gridLen, $bounds, $root); $clusters = $this->timelineQuery->getMapClusters($gridLen, $bounds, $root);
// Get previews for each cluster // Get previews for each cluster
$clusterIds = array_map(function ($cluster) { $clusterIds = array_map(fn ($cluster) => (int) $cluster['id'], $clusters);
return (int) $cluster['id'];
}, $clusters);
$previews = $this->timelineQuery->getMapClusterPreviews($clusterIds, $root); $previews = $this->timelineQuery->getMapClusterPreviews($clusterIds, $root);
// Merge the responses // Merge the responses
@ -75,8 +68,6 @@ class MapController extends GenericApiController
} }
return new JSONResponse($clusters); return new JSONResponse($clusters);
} catch (\Exception $e) { });
return Errors::Generic($e);
}
} }
} }

View File

@ -24,7 +24,8 @@ declare(strict_types=1);
namespace OCA\Memories\Controller; namespace OCA\Memories\Controller;
use OCA\Memories\AppInfo\Application; use OCA\Memories\AppInfo\Application;
use OCA\Memories\Errors; use OCA\Memories\Exceptions;
use OCA\Memories\Util;
use OCP\AppFramework\Http; use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\StreamResponse; use OCP\AppFramework\Http\StreamResponse;
@ -43,20 +44,18 @@ class OtherController extends GenericApiController
*/ */
public function setUserConfig(string $key, string $value): Http\Response public function setUserConfig(string $key, string $value): Http\Response
{ {
$user = $this->userSession->getUser(); return Util::guardEx(function () use ($key, $value) {
if (null === $user) { $uid = Util::getUID();
return Errors::NotLoggedIn();
}
// Make sure not running in read-only mode // Make sure not running in read-only mode
if ($this->config->getSystemValue('memories.readonly', false)) { if ($this->config->getSystemValue('memories.readonly', false)) {
return new JSONResponse(['message' => 'Cannot change settings in readonly mode'], Http::STATUS_FORBIDDEN); throw Exceptions::Forbidden('Cannot change settings in readonly mode');
} }
$userId = $user->getUID(); $this->config->setUserValue($uid, Application::APPNAME, $key, $value);
$this->config->setUserValue($userId, Application::APPNAME, $key, $value);
return new JSONResponse([], Http::STATUS_OK); return new JSONResponse([], Http::STATUS_OK);
});
} }
/** /**

View File

@ -23,7 +23,8 @@ declare(strict_types=1);
namespace OCA\Memories\Controller; namespace OCA\Memories\Controller;
use OCA\Memories\Errors; use OCA\Memories\Exceptions;
use OCA\Memories\Util;
use OCP\AppFramework\Http; use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\JSONResponse;
@ -39,22 +40,21 @@ class ShareController extends GenericApiController
*/ */
public function links($id, $path): Http\Response public function links($id, $path): Http\Response
{ {
$file = $this->getNodeByIdOrPath($id, $path); return Util::guardEx(function () use ($id, $path) {
if (!$file) { $file = $this->getNodeByIdOrPath($id, $path);
return Errors::Forbidden('file');
}
/** @var \OCP\Share\IManager $shareManager */ /** @var \OCP\Share\IManager $shareManager */
$shareManager = \OC::$server->get(\OCP\Share\IManager::class); $shareManager = \OC::$server->get(\OCP\Share\IManager::class);
$shares = $shareManager->getSharesBy($this->getUID(), \OCP\Share\IShare::TYPE_LINK, $file, true, 50, 0); $shares = $shareManager->getSharesBy(Util::getUID(), \OCP\Share\IShare::TYPE_LINK, $file, true, 50, 0);
if (empty($shares)) { if (empty($shares)) {
return Errors::NotFound('external links'); throw Exceptions::NotFound('external links');
} }
$links = array_map([$this, 'makeShareResponse'], $shares); $links = array_map(fn ($s) => $this->makeShareResponse($s), $shares);
return new JSONResponse($links, Http::STATUS_OK); return new JSONResponse($links, Http::STATUS_OK);
});
} }
/** /**
@ -67,24 +67,23 @@ class ShareController extends GenericApiController
*/ */
public function createNode($id, $path): Http\Response public function createNode($id, $path): Http\Response
{ {
$file = $this->getNodeByIdOrPath($id, $path); return Util::guardEx(function () use ($id, $path) {
if (!$file) { $file = $this->getNodeByIdOrPath($id, $path);
return Errors::Forbidden('You are not allowed to share this file');
}
/** @var \OCP\Share\IManager $shareManager */ /** @var \OCP\Share\IManager $shareManager */
$shareManager = \OC::$server->get(\OCP\Share\IManager::class); $shareManager = \OC::$server->get(\OCP\Share\IManager::class);
/** @var \OCP\Share\IShare $share */ /** @var \OCP\Share\IShare $share */
$share = $shareManager->newShare(); $share = $shareManager->newShare();
$share->setNode($file); $share->setNode($file);
$share->setShareType(\OCP\Share\IShare::TYPE_LINK); $share->setShareType(\OCP\Share\IShare::TYPE_LINK);
$share->setSharedBy($this->userSession->getUser()->getUID()); $share->setSharedBy($this->userSession->getUser()->getUID());
$share->setPermissions(\OCP\Constants::PERMISSION_READ); $share->setPermissions(\OCP\Constants::PERMISSION_READ);
$share = $shareManager->createShare($share); $share = $shareManager->createShare($share);
return new JSONResponse($this->makeShareResponse($share), Http::STATUS_OK); return new JSONResponse($this->makeShareResponse($share), Http::STATUS_OK);
});
} }
/** /**
@ -94,46 +93,41 @@ class ShareController extends GenericApiController
*/ */
public function deleteShare(string $id): Http\Response public function deleteShare(string $id): Http\Response
{ {
$uid = $this->getUID(); return Util::guardEx(function () use ($id) {
if (!$uid) { $uid = Util::getUID();
return Errors::NotLoggedIn();
}
/** @var \OCP\Share\IManager $shareManager */ /** @var \OCP\Share\IManager $shareManager */
$shareManager = \OC::$server->get(\OCP\Share\IManager::class); $shareManager = \OC::$server->get(\OCP\Share\IManager::class);
$share = $shareManager->getShareById($id); $share = $shareManager->getShareById($id);
if ($share->getSharedBy() !== $uid) { if ($share->getSharedBy() !== $uid) {
return Errors::Forbidden('You are not the owner of this share'); throw Exceptions::Forbidden('You are not the owner of this share');
} }
$shareManager->deleteShare($share); $shareManager->deleteShare($share);
return new JSONResponse([], Http::STATUS_OK); return new JSONResponse([], Http::STATUS_OK);
});
} }
private function getNodeByIdOrPath($id, $path) private function getNodeByIdOrPath($id, $path)
{ {
$uid = $this->getUID(); $uid = Util::getUID();
if (!$uid) {
return null;
}
$file = null; try {
if ($id) { $file = null;
$file = $this->getUserFile($id); if ($id) {
} elseif ($path) { $file = $this->getUserFile($id);
try { } elseif ($path) {
$userFolder = $this->rootFolder->getUserFolder($uid); $file = Util::getUserFolder($uid)->get($path);
$file = $userFolder->get($path);
} catch (\OCP\Files\NotFoundException $e) {
return null;
} }
} catch (\OCP\Files\NotFoundException $e) {
throw Exceptions::NotFoundFile($path ?? $id);
} }
if (!$file || !$file->isShareable()) { if (!$file || !$file->isShareable()) {
return null; throw Exceptions::Forbidden('File not sharable');
} }
return $file; return $file;

View File

@ -23,7 +23,8 @@ declare(strict_types=1);
namespace OCA\Memories\Controller; namespace OCA\Memories\Controller;
use OCA\Memories\Errors; use OCA\Memories\Exceptions;
use OCA\Memories\Util;
use OCP\AppFramework\Http; use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\JSONResponse;
@ -36,29 +37,31 @@ class TagsController extends GenericApiController
*/ */
public function set(int $id, array $add, array $remove): Http\Response public function set(int $id, array $add, array $remove): Http\Response
{ {
// Check tags enabled for this user return Util::guardEx(function () use ($id, $add, $remove) {
if (!$this->tagsIsEnabled()) { // Check tags enabled for this user
return Errors::NotEnabled('Tags'); if (!$this->tagsIsEnabled()) {
} throw Exceptions::NotEnabled('Tags');
}
// Check the user is allowed to edit the file // Check the user is allowed to edit the file
$file = $this->getUserFile($id); $file = $this->getUserFile($id);
if (null === $file) { if (null === $file) {
return Errors::NotFoundFile($id); throw Exceptions::NotFoundFile($id);
} }
// Check the user is allowed to edit the file // Check the user is allowed to edit the file
if (!$file->isUpdateable() || !($file->getPermissions() & \OCP\Constants::PERMISSION_UPDATE)) { if (!$file->isUpdateable() || !($file->getPermissions() & \OCP\Constants::PERMISSION_UPDATE)) {
return Errors::ForbiddenFileUpdate($file->getName()); throw Exceptions::ForbiddenFileUpdate($file->getName());
} }
// Get mapper from tags to objects // Get mapper from tags to objects
$om = \OC::$server->get(\OCP\SystemTag\ISystemTagObjectMapper::class); $om = \OC::$server->get(\OCP\SystemTag\ISystemTagObjectMapper::class);
// Add and remove tags // Add and remove tags
$om->assignTags((string) $id, 'files', $add); $om->assignTags((string) $id, 'files', $add);
$om->unassignTags((string) $id, 'files', $remove); $om->unassignTags((string) $id, 'files', $remove);
return new JSONResponse([], Http::STATUS_OK); return new JSONResponse([], Http::STATUS_OK);
});
} }
} }

View File

@ -23,8 +23,9 @@ declare(strict_types=1);
namespace OCA\Memories\Controller; namespace OCA\Memories\Controller;
use OCA\Memories\Errors; use OCA\Memories\Exceptions;
use OCA\Memories\Exif; use OCA\Memories\Exif;
use OCA\Memories\Util;
use OCP\AppFramework\Http; use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataDisplayResponse; use OCP\AppFramework\Http\DataDisplayResponse;
use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\JSONResponse;
@ -43,58 +44,60 @@ class VideoController extends GenericApiController
*/ */
public function transcode(string $client, int $fileid, string $profile): Http\Response public function transcode(string $client, int $fileid, string $profile): Http\Response
{ {
// Make sure not running in read-only mode return Util::guardEx(function () use ($client, $fileid, $profile) {
if (false !== $this->config->getSystemValue('memories.vod.disable', 'UNSET')) { // Make sure not running in read-only mode
return Errors::Forbidden('Transcoding disabled'); if (false !== $this->config->getSystemValue('memories.vod.disable', 'UNSET')) {
} throw Exceptions::Forbidden('Transcoding disabled');
// Check client identifier is 8 characters or more
if (\strlen($client) < 8) {
return Errors::MissingParameter('client (invalid)');
}
// Get file
$file = $this->getUserFile($fileid);
if (!$file || !$file->isReadable()) {
return Errors::NotFoundFile($fileid);
}
// Local files only for now
if (!$file->getStorage()->isLocal()) {
return Errors::Forbidden('External storage not supported');
}
// Get file path
$path = $file->getStorage()->getLocalFile($file->getInternalPath());
if (!$path || !file_exists($path)) {
return Errors::NotFound('local file path');
}
// Check if file starts with temp dir
$tmpDir = sys_get_temp_dir();
if (0 === strpos($path, $tmpDir)) {
return Errors::Forbidden('files in temp directory not supported');
}
// Request and check data was received
try {
$status = $this->getUpstream($client, $path, $profile);
if (409 === $status || -1 === $status) {
// Just a conflict (transcoding process changed)
return new JSONResponse(['message' => 'Conflict'], Http::STATUS_CONFLICT);
} }
if (200 !== $status) {
throw new \Exception("Transcoder returned {$status}"); // Check client identifier is 8 characters or more
if (\strlen($client) < 8) {
throw Exceptions::MissingParameter('client (invalid)');
} }
} catch (\Exception $e) {
$msg = 'Transcode failed: '.$e->getMessage();
$this->logger->error($msg, ['app' => 'memories']);
return Errors::Generic($e); // Get file
} $file = $this->getUserFile($fileid);
if (!$file || !$file->isReadable()) {
throw Exceptions::NotFoundFile($fileid);
}
// The response was already streamed, so we have nothing to do here // Local files only for now
exit; if (!$file->getStorage()->isLocal()) {
throw Exceptions::Forbidden('External storage not supported');
}
// Get file path
$path = $file->getStorage()->getLocalFile($file->getInternalPath());
if (!$path || !file_exists($path)) {
throw Exceptions::NotFound('local file path');
}
// Check if file starts with temp dir
$tmpDir = sys_get_temp_dir();
if (0 === strpos($path, $tmpDir)) {
throw Exceptions::Forbidden('files in temp directory not supported');
}
// Request and check data was received
try {
$status = $this->getUpstream($client, $path, $profile);
if (409 === $status || -1 === $status) {
// Just a conflict (transcoding process changed)
return new JSONResponse(['message' => 'Conflict'], Http::STATUS_CONFLICT);
}
if (200 !== $status) {
throw new \Exception("Transcoder returned {$status}");
}
} catch (\Exception $e) {
$msg = 'Transcode failed: '.$e->getMessage();
$this->logger->error($msg, ['app' => 'memories']);
throw $e;
}
// The response was already streamed, so we have nothing to do here
exit;
});
} }
/** /**
@ -112,111 +115,113 @@ class VideoController extends GenericApiController
string $format = '', string $format = '',
string $transcode = '' string $transcode = ''
) { ) {
$file = $this->getUserFile($fileid); return Util::guardEx(function () use ($fileid, $liveid, $format, $transcode) {
if (null === $file) { $file = $this->getUserFile($fileid);
return Errors::NotFoundFile($fileid); if (null === $file) {
} throw Exceptions::NotFoundFile($fileid);
// Check file liveid
if (!$liveid) {
return Errors::MissingParameter('liveid');
}
// Response data
$name = '';
$mime = '';
$blob = null;
$liveVideoPath = null;
// Video is inside the file
$path = null;
if (str_starts_with($liveid, 'self__')) {
$path = $file->getStorage()->getLocalFile($file->getInternalPath());
$mime = 'video/mp4';
$name = $file->getName().'.mp4';
}
// Different manufacurers have different formats
if ('self__trailer' === $liveid) {
try { // Get trailer
$blob = Exif::getBinaryExifProp($path, '-trailer');
} catch (\Exception $e) {
return Errors::NotFound('file trailer');
}
} elseif ('self__embeddedvideo' === $liveid) {
try { // Get embedded video file
$blob = Exif::getBinaryExifProp($path, '-EmbeddedVideoFile');
} catch (\Exception $e) {
return Errors::NotFound('embedded video');
}
} elseif (str_starts_with($liveid, 'self__traileroffset=')) {
// Remove prefix
$offset = (int) substr($liveid, \strlen('self__traileroffset='));
if ($offset <= 0) {
return new JSONResponse(['message' => 'Invalid offset'], Http::STATUS_BAD_REQUEST);
} }
// Read file from offset to end // Check file liveid
$blob = file_get_contents($path, false, null, $offset); if (!$liveid) {
} else { throw Exceptions::MissingParameter('liveid');
// Get stored video file (Apple MOV)
$lp = $this->timelineQuery->getLivePhoto($fileid);
if (!$lp || $lp['liveid'] !== $liveid) {
return Errors::NotFound('live video entry');
} }
// Get and return file // Response data
$liveFileId = (int) $lp['fileid']; $name = '';
$files = $this->rootFolder->getById($liveFileId); $mime = '';
if (0 === \count($files)) { $blob = null;
return Errors::NotFound('live video file'); $liveVideoPath = null;
}
$liveFile = $files[0];
if ($liveFile instanceof File) { // Video is inside the file
// Requested only JSON info $path = null;
if ('json' === $format) { if (str_starts_with($liveid, 'self__')) {
return new JSONResponse($lp); $path = $file->getStorage()->getLocalFile($file->getInternalPath());
$mime = 'video/mp4';
$name = $file->getName().'.mp4';
}
// Different manufacurers have different formats
if ('self__trailer' === $liveid) {
try { // Get trailer
$blob = Exif::getBinaryExifProp($path, '-trailer');
} catch (\Exception $e) {
throw Exceptions::NotFound('file trailer');
}
} elseif ('self__embeddedvideo' === $liveid) {
try { // Get embedded video file
$blob = Exif::getBinaryExifProp($path, '-EmbeddedVideoFile');
} catch (\Exception $e) {
throw Exceptions::NotFound('embedded video');
}
} elseif (str_starts_with($liveid, 'self__traileroffset=')) {
// Remove prefix
$offset = (int) substr($liveid, \strlen('self__traileroffset='));
if ($offset <= 0) {
throw Exceptions::BadRequest('Invalid offset');
} }
$name = $liveFile->getName(); // Read file from offset to end
$blob = $liveFile->getContent(); $blob = file_get_contents($path, false, null, $offset);
$mime = $liveFile->getMimeType(); } else {
$liveVideoPath = $liveFile->getStorage()->getLocalFile($liveFile->getInternalPath()); // Get stored video file (Apple MOV)
} $lp = $this->timelineQuery->getLivePhoto($fileid);
} if (!$lp || $lp['liveid'] !== $liveid) {
throw Exceptions::NotFound('live video entry');
// Data not found
if (!$blob) {
return Errors::NotFound('live video data');
}
// Transcode video if allowed
if ($transcode && !$this->config->getSystemValue('memories.vod.disable', true)) {
try {
// If video path not given, write to temp file
if (!$liveVideoPath) {
$liveVideoPath = self::postFile($transcode, $blob)['path'];
} }
// If this is H.264 it won't get transcoded anyway // Get and return file
if ($liveVideoPath && 200 === $this->getUpstream($transcode, $liveVideoPath, 'max.mov')) { $liveFileId = (int) $lp['fileid'];
exit; $files = $this->rootFolder->getById($liveFileId);
if (0 === \count($files)) {
throw Exceptions::NotFound('live video file');
}
$liveFile = $files[0];
if ($liveFile instanceof File) {
// Requested only JSON info
if ('json' === $format) {
return new JSONResponse($lp);
}
$name = $liveFile->getName();
$blob = $liveFile->getContent();
$mime = $liveFile->getMimeType();
$liveVideoPath = $liveFile->getStorage()->getLocalFile($liveFile->getInternalPath());
} }
} catch (\Exception $e) {
// Transcoding failed, just return the original video
} }
}
// Make and send response // Data not found
$response = new DataDisplayResponse($blob, Http::STATUS_OK, []); if (!$blob) {
$response->setHeaders([ throw Exceptions::NotFound('live video data');
'Content-Type' => $mime, }
'Content-Disposition' => "attachment; filename=\"{$name}\"",
]);
$response->cacheFor(3600 * 24, false, false);
return $response; // Transcode video if allowed
if ($transcode && !$this->config->getSystemValue('memories.vod.disable', true)) {
try {
// If video path not given, write to temp file
if (!$liveVideoPath) {
$liveVideoPath = self::postFile($transcode, $blob)['path'];
}
// If this is H.264 it won't get transcoded anyway
if ($liveVideoPath && 200 === $this->getUpstream($transcode, $liveVideoPath, 'max.mov')) {
exit;
}
} catch (\Exception $e) {
// Transcoding failed, just return the original video
}
}
// Make and send response
$response = new DataDisplayResponse($blob, Http::STATUS_OK, []);
$response->setHeaders([
'Content-Type' => $mime,
'Content-Disposition' => "attachment; filename=\"{$name}\"",
]);
$response->cacheFor(3600 * 24, false, false);
return $response;
});
} }
/** /**

View File

@ -1,74 +0,0 @@
<?php
declare(strict_types=1);
namespace OCA\Memories;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
class Errors
{
public static function Generic(\Exception $e, int $status = Http::STATUS_INTERNAL_SERVER_ERROR): Http\Response
{
return new DataResponse([
'message' => $e->getMessage(),
], $status);
}
public static function NotLoggedIn(): Http\Response
{
return new DataResponse([
'message' => 'User not logged in',
], Http::STATUS_PRECONDITION_FAILED);
}
public static function NotEnabled(string $app): Http\Response
{
return new DataResponse([
'message' => "{$app} app not enabled or not the required version.",
], Http::STATUS_PRECONDITION_FAILED);
}
public static function NoRequestRoot(): Http\Response
{
return new DataResponse([
'message' => 'Request root could not be determined',
], Http::STATUS_NOT_FOUND);
}
public static function NotFound(string $ctx): Http\Response
{
return new DataResponse([
'message' => "Not found ({$ctx})",
], Http::STATUS_NOT_FOUND);
}
public static function NotFoundFile($identifier): Http\Response
{
return new DataResponse([
'message' => "File not found ({$identifier})",
], Http::STATUS_NOT_FOUND);
}
public static function Forbidden(string $ctx): Http\Response
{
return new DataResponse([
'message' => "Forbidden ({$ctx})",
], Http::STATUS_FORBIDDEN);
}
public static function ForbiddenFileUpdate(string $name): Http\Response
{
return new DataResponse([
'message' => "Forbidden ({$name} cannot be updated)",
], Http::STATUS_FORBIDDEN);
}
public static function MissingParameter(string $name): Http\Response
{
return new DataResponse([
'message' => "Missing parameter ({$name})",
], Http::STATUS_BAD_REQUEST);
}
}

81
lib/Exceptions.php 100644
View File

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace OCA\Memories;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
class Exceptions
{
public static function Generic(\Exception $e, int $status = Http::STATUS_INTERNAL_SERVER_ERROR): HttpResponseException
{
return new HttpResponseException(new DataResponse([
'message' => $e->getMessage(),
], $status));
}
public static function NotLoggedIn(): HttpResponseException
{
return new HttpResponseException(new DataResponse([
'message' => 'User not logged in',
], Http::STATUS_PRECONDITION_FAILED));
}
public static function NotEnabled(string $app): HttpResponseException
{
return new HttpResponseException(new DataResponse([
'message' => "{$app} app not enabled or not the required version.",
], Http::STATUS_PRECONDITION_FAILED));
}
public static function NoRequestRoot(): HttpResponseException
{
return new HttpResponseException(new DataResponse([
'message' => 'Request root could not be determined',
], Http::STATUS_NOT_FOUND));
}
public static function NotFound(string $ctx): HttpResponseException
{
return new HttpResponseException(new DataResponse([
'message' => "Not found ({$ctx})",
], Http::STATUS_NOT_FOUND));
}
public static function NotFoundFile($identifier): HttpResponseException
{
return new HttpResponseException(new DataResponse([
'message' => "File not found ({$identifier})",
], Http::STATUS_NOT_FOUND));
}
public static function Forbidden(string $ctx): HttpResponseException
{
return new HttpResponseException(new DataResponse([
'message' => "Forbidden ({$ctx})",
], Http::STATUS_FORBIDDEN));
}
public static function ForbiddenFileUpdate(string $name): HttpResponseException
{
return new HttpResponseException(new DataResponse([
'message' => "Forbidden ({$name} cannot be updated)",
], Http::STATUS_FORBIDDEN));
}
public static function MissingParameter(string $name): HttpResponseException
{
return new HttpResponseException(new DataResponse([
'message' => "Missing parameter ({$name})",
], Http::STATUS_BAD_REQUEST));
}
public static function BadRequest(string $ctx): HttpResponseException
{
return new HttpResponseException(new DataResponse([
'message' => "Bad Request ({$ctx})",
], Http::STATUS_BAD_REQUEST));
}
}

View File

@ -16,6 +16,8 @@ use OCP\IConfig;
class Util class Util
{ {
use UtilController;
public static $TAG_DAYID_START = -(1 << 30); // the world surely didn't exist public static $TAG_DAYID_START = -(1 << 30); // the world surely didn't exist
public static $TAG_DAYID_FOLDERS = -(1 << 30) + 1; public static $TAG_DAYID_FOLDERS = -(1 << 30) + 1;

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace OCA\Memories;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
trait UtilController
{
/**
* Run a function and catch exceptions to return HTTP response.
*
* @param Function $function
*/
public static function guardEx($function): Http\Response
{
try {
return $function();
} catch (\OCA\Memories\HttpResponseException $e) {
return $e->response;
} catch (\Exception $e) {
return new DataResponse([
'message' => $e->getMessage(),
], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
/**
* Get the current user.
*
* @throws \OCA\Memories\HttpResponseException if the user is not logged in
*/
public static function getUser(): \OCP\IUser
{
$user = \OC::$server->get(\OCP\IUserSession::class)->getUser();
if (null === $user) {
throw Exceptions::NotLoggedIn();
}
return $user;
}
/**
* Get the current user ID.
*
* @throws \OCA\Memories\HttpResponseException if the user is not logged in
*/
public static function getUID(): string
{
return self::getUser()->getUID();
}
/**
* Get a user's home folder.
*
* @param null|string $uid User ID, or null for current user
*
* @throws \OCA\Memories\HttpResponseException if the user is not logged in
*/
public static function getUserFolder(?string $uid = null): \OCP\Files\Folder
{
if (null === $uid) {
$uid = self::getUID();
}
return \OC::$server->get(\OCP\Files\IRootFolder::class)->getUserFolder($uid);
}
}