diff --git a/appinfo/routes.php b/appinfo/routes.php index e480734f..32b99163 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -53,8 +53,7 @@ return [ ['name' => 'Faces#preview', 'url' => '/api/faces/preview/{id}', 'verb' => 'GET'], ['name' => 'Image#info', 'url' => '/api/image/info/{id}', 'verb' => 'GET'], - ['name' => 'Image#edit', 'url' => '/api/image/edit/{id}', 'verb' => 'PATCH'], - ['name' => 'Image#setExif', 'url' => '/api/image/set-exif/{id}', 'verb' => 'PUT'], + ['name' => 'Image#setExif', 'url' => '/api/image/set-exif/{id}', 'verb' => 'PATCH'], ['name' => 'Archive#archive', 'url' => '/api/archive/{id}', 'verb' => 'PATCH'], diff --git a/lib/Controller/ApiBase.php b/lib/Controller/ApiBase.php index 1916cca4..1dcd4336 100644 --- a/lib/Controller/ApiBase.php +++ b/lib/Controller/ApiBase.php @@ -31,6 +31,7 @@ use OCP\App\IAppManager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\JSONResponse; +use OCP\Files\File; use OCP\Files\Folder; use OCP\Files\IRootFolder; use OCP\IConfig; @@ -133,6 +134,23 @@ class ApiBase extends Controller return $folder; } + protected function getUserFile(int $id): File + { + $user = $this->userSession->getUser(); + if (null === $user) { + return null; + } + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + + // Check for permissions and get numeric Id + $file = $userFolder->getById($id); + if (0 === \count($file)) { + return null; + } + + return $file[0]; + } + protected function isRecursive() { return null === $this->request->getParam('folder'); diff --git a/lib/Controller/ImageController.php b/lib/Controller/ImageController.php index b793318d..ee5c36e8 100644 --- a/lib/Controller/ImageController.php +++ b/lib/Controller/ImageController.php @@ -38,18 +38,10 @@ class ImageController extends ApiBase */ public function info(string $id): JSONResponse { - $user = $this->userSession->getUser(); - if (null === $user) { - return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED); - } - $userFolder = $this->rootFolder->getUserFolder($user->getUID()); - - // Check for permissions and get numeric Id - $file = $userFolder->getById((int) $id); - if (0 === \count($file)) { + $file = $this->getUserFile((int) $id); + if (!$file) { return new JSONResponse([], Http::STATUS_NOT_FOUND); } - $file = $file[0]; // Get the image info $basic = false !== $this->request->getParam('basic', false); @@ -57,85 +49,21 @@ class ImageController extends ApiBase // Get latest exif data if requested if ($this->request->getParam('current', false)) { - $info["current"] = Exif::getExifFromFile($file); + $info['current'] = Exif::getExifFromFile($file); } return new JSONResponse($info, Http::STATUS_OK); } /** - * @NoAdminRequired - * - * Change exif data for one file - * - * @param string fileid - */ - public function edit(string $id): JSONResponse - { - $user = $this->userSession->getUser(); - if (null === $user) { - return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED); - } - $userFolder = $this->rootFolder->getUserFolder($user->getUID()); - - // Check for permissions and get numeric Id - $file = $userFolder->getById((int) $id); - if (0 === \count($file)) { - return new JSONResponse([], Http::STATUS_NOT_FOUND); - } - $file = $file[0]; - - // Check if user has permissions - if (!$file->isUpdateable()) { - return new JSONResponse([], Http::STATUS_FORBIDDEN); - } - - // Get new date from body - $body = $this->request->getParams(); - if (!isset($body['date'])) { - return new JSONResponse(['message' => 'Missing date'], Http::STATUS_BAD_REQUEST); - } - - // Make sure the date is valid - try { - Exif::parseExifDate($body['date']); - } catch (\Exception $e) { - return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST); - } - - // Update date - try { - $res = Exif::updateExifDate($file, $body['date']); - if (false === $res) { - return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR); - } - } catch (\Exception $e) { - return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR); - } - - // Reprocess the file - $this->timelineWrite->processFile($file, true); - - return $this->info($id); - } - - /** - * Set the exif data for a file + * Set the exif data for a file. */ public function setExif(string $id): JSONResponse { - $user = $this->userSession->getUser(); - if (null === $user) { - return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED); - } - $userFolder = $this->rootFolder->getUserFolder($user->getUID()); - - // Check for permissions and get numeric Id - $file = $userFolder->getById((int) $id); - if (0 === \count($file)) { + $file = $this->getUserFile((int) $id); + if (!$file) { return new JSONResponse([], Http::STATUS_NOT_FOUND); } - $file = $file[0]; // Check if user has permissions if (!$file->isUpdateable()) { @@ -145,15 +73,45 @@ class ImageController extends ApiBase // Get original file from body $exif = $this->request->getParam('raw'); $path = $file->getStorage()->getLocalFile($file->getInternalPath()); + try { Exif::setExif($path, $exif); } catch (\Exception $e) { return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR); } + // Update remote file if not local + if (!$file->getStorage()->isLocal()) { + $file->putContent(fopen($path, 'r')); // closes the handler + } + // Reprocess the file $this->timelineWrite->processFile($file, true); return new JSONResponse([], Http::STATUS_OK); } + + /** + * Get a full resolution PNG for editing from a file. + */ + public function getPNG(string $id) + { + $file = $this->getUserFile((int) $id); + if (!$file) { + return new JSONResponse([], Http::STATUS_NOT_FOUND); + } + + // Get the image info + $info = $this->timelineQuery->getInfoById($file->getId(), true); + + // Get the image + $path = $file->getStorage()->getLocalFile($file->getInternalPath()); + $image = Exif::getPNG($path, $info['exif']); + + // Return the image + $response = new Http\DataDisplayResponse($image, Http::STATUS_OK, ['Content-Type' => 'image/png']); + $response->cacheFor(0); + + return $response; + } } diff --git a/lib/Db/TimelineQueryDays.php b/lib/Db/TimelineQueryDays.php index 2899c2d9..0bbbe9a1 100644 --- a/lib/Db/TimelineQueryDays.php +++ b/lib/Db/TimelineQueryDays.php @@ -203,8 +203,7 @@ trait TimelineQueryDays $row['w'] = (int) $row['w']; $row['h'] = (int) $row['h']; if (!$row['isvideo']) { - unset($row['isvideo']); - unset($row['video_duration']); + unset($row['isvideo'], $row['video_duration']); } if ($row['categoryid']) { $row['isfavorite'] = 1; diff --git a/lib/Exif.php b/lib/Exif.php index 80313f3c..1d98df75 100644 --- a/lib/Exif.php +++ b/lib/Exif.php @@ -230,29 +230,35 @@ class Exif } /** - * Update exif date using exiftool. + * Set exif data using raw json. * - * @param string $newDate formatted in standard Exif format (YYYY:MM:DD HH:MM:SS) + * @param string $path to local file + * @param array $data exif data + * + * @throws \Exception on failure */ - public static function updateExifDate(File &$file, string $newDate) + public static function setExif(string &$path, array &$data) { - // Don't want to mess these up, definitely - if ($file->isEncrypted()) { - throw new \Exception('Cannot update exif date on encrypted files'); - } + $data['SourceFile'] = $path; + $raw = json_encode([$data]); + $cmd = array_merge(self::getExiftool(), ['-json=-', $path]); + $proc = proc_open($cmd, [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ], $pipes); - // Get path to local (copy) of the file - $path = $file->getStorage()->getLocalFile($file->getInternalPath()); - if (!\is_string($path)) { - throw new \Exception('Failed to get local file path'); - } + fwrite($pipes[0], $raw); + fclose($pipes[0]); - // Update exif data - self::updateExifDateForLocalFile($path, $newDate); + $stdout = self::readOrTimeout($pipes[1], 30000); + fclose($pipes[1]); + fclose($pipes[2]); + proc_terminate($proc); + if (false !== strpos($stdout, 'error')) { + error_log("Exiftool error: {$stdout}"); - // Update remote file if not local - if (!$file->getStorage()->isLocal()) { - $file->putContent(fopen($path, 'r')); // closes the handler + throw new \Exception('Could not set exif data: '.$stdout); } } @@ -415,62 +421,4 @@ class Exif return $json[0]; } - - /** - * Update exif date using exiftool for a local file. - * - * @param string $newDate formatted in standard Exif format (YYYY:MM:DD HH:MM:SS) - * - * @throws \Exception on failure - */ - private static function updateExifDateForLocalFile(string $path, string $newDate) - { - $cmd = array_merge(self::getExiftool(), ['-api', 'QuickTimeUTC=1', '-overwrite_original', '-DateTimeOriginal='.$newDate, $path]); - $proc = proc_open($cmd, [ - 1 => ['pipe', 'w'], - 2 => ['pipe', 'w'], - ], $pipes); - $stdout = self::readOrTimeout($pipes[1], 300000); - fclose($pipes[1]); - fclose($pipes[2]); - proc_terminate($proc); - if (false !== strpos($stdout, 'error')) { - error_log("Exiftool error: {$stdout}"); - - throw new \Exception('Could not update exif date: '.$stdout); - } - } - - /** - * Set exif data using raw json. - * - * @param string $path to local file - * @param array $data exif data - * - * @throws \Exception on failure - */ - public static function setExif(string &$path, array &$data) - { - $data['SourceFile'] = $path; - $raw = json_encode([$data]); - $cmd = array_merge(self::getExiftool(), ['-json=-', $path]); - $proc = proc_open($cmd, [ - 0 => ['pipe', 'r'], - 1 => ['pipe', 'w'], - 2 => ['pipe', 'w'], - ], $pipes); - - fwrite($pipes[0], $raw); - fclose($pipes[0]); - - $stdout = self::readOrTimeout($pipes[1], 30000); - fclose($pipes[1]); - fclose($pipes[2]); - proc_terminate($proc); - if (false !== strpos($stdout, 'error')) { - error_log("Exiftool error: {$stdout}"); - - throw new \Exception('Could not set exif data: '.$stdout); - } - } } diff --git a/src/components/ImageEditor.vue b/src/components/ImageEditor.vue index 6c4c000e..1b76e9b8 100644 --- a/src/components/ImageEditor.vue +++ b/src/components/ImageEditor.vue @@ -215,7 +215,7 @@ export default class ImageEditor extends Mixins(GlobalMixin) { delete exif.ExifImageSize; // Update exif data - await axios.put( + await axios.patch( generateUrl("/apps/memories/api/image/set-exif/{id}", { id: fileid, }), diff --git a/src/components/modal/EditDate.vue b/src/components/modal/EditDate.vue index 8cf5ec3c..fbf99c0b 100644 --- a/src/components/modal/EditDate.vue +++ b/src/components/modal/EditDate.vue @@ -144,7 +144,7 @@ import * as utils from "../../services/Utils"; import * as dav from "../../services/DavRequests"; const INFO_API_URL = "/apps/memories/api/image/info/{id}"; -const EDIT_API_URL = "/apps/memories/api/image/edit/{id}"; +const EDIT_API_URL = "/apps/memories/api/image/set-exif/{id}"; @Component({ components: { @@ -269,12 +269,11 @@ export default class EditDate extends Mixins(GlobalMixin) { try { this.processing = true; const fileid = this.photos[0].fileid; - const res = await axios.patch( - generateUrl(EDIT_API_URL, { id: fileid }), - { - date: this.getExifFormat(this.getDate()), - } - ); + await axios.patch(generateUrl(EDIT_API_URL, { id: fileid }), { + raw: { + DateTimeOriginal: this.getExifFormat(this.getDate()), + }, + }); emit("files:file:updated", { fileid }); this.emitRefresh(true); this.close();