diff --git a/appinfo/info.xml b/appinfo/info.xml
index 19e2997f6..334249bd0 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -1,28 +1,36 @@
- photos
- Photos
- Your memories under your control
- Your memories under your control
- 1.3.0
- agpl
- John Molakvoæ
- Photos
- multimedia
+ xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
+photos
+Photos
+Your memories under your control
+Your memories under your control
+1.3.1
+agpl
+John Molakvoæ
+Photos
+multimedia
+
+
+
+
+
+https://github.com/nextcloud/photos
+https://github.com/nextcloud/photos/issues
+https://github.com/nextcloud/photos.git
+
+
+
+
+
+
+ Photos
+ photos.page.index
+ 1
+
+
+
+ OCA\Photos\Command\ExtractMetadata
+
- https://github.com/nextcloud/photos
- https://github.com/nextcloud/photos/issues
- https://github.com/nextcloud/photos.git
-
-
-
-
-
-
- Photos
- photos.page.index
- 1
-
-
diff --git a/appinfo/routes.php b/appinfo/routes.php
index 62f3ce4a1..ecbbc0c8a 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -73,5 +73,30 @@
'path' => '',
],
],
+ [
+ 'name' => 'albums#getNumberByMonth',
+ 'url' => '/api/v1/numberbymonth/{path}',
+ 'verb' => 'GET',
+ 'requirements' => [
+ 'path' => '.*',
+ ],
+ 'defaults' => [
+ 'path' => '',
+ ],
+ ],
+ [
+ 'name' => 'albums#getPhotosOfMonth',
+ 'url' => '/api/v1/photosofmonth/{yearandmonth}',
+ 'verb' => 'GET',
+ 'requirements' => [
+ 'yearandmonth' => '.*',
+ 'path' => '.*',
+ ],
+ 'defaults' => [
+ 'yearandmonth' => '',
+ 'path' => '',
+ ],
+ ],
+
]
];
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index 7abc80cae..5fd037696 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -29,6 +29,7 @@
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
+use OCP\Files\Events\NodeAddedToCache;
class Application extends App implements IBootstrap {
public const APP_ID = 'photos';
@@ -52,11 +53,18 @@ class Application extends App implements IBootstrap {
public function __construct() {
parent::__construct(self::APP_ID);
+ /* @var IEventDispatcher $eventDispatcher */
+ $dispatcher = $this->getContainer()->get("OCP\EventDispatcher\IEventDispatcher" );
+ $dispatcher->addListener(NodeAddedToCache::class, function(NodeAddedToCache $event) {
+ $filePath = $event->getPath();
+ });
+
}
public function register(IRegistrationContext $context): void {
}
public function boot(IBootContext $context): void {
+ \OCP\Util::connectHook('OC_Filesystem', 'preSetup', 'OCA\Photos\Storage', 'setupStorage');
}
}
diff --git a/lib/Command/ExtractMetadata.php b/lib/Command/ExtractMetadata.php
new file mode 100644
index 000000000..33cecd617
--- /dev/null
+++ b/lib/Command/ExtractMetadata.php
@@ -0,0 +1,171 @@
+userManager = $userManager;
+ $this->rootFolder = $rootFolder;
+ $this->config = $config;
+ $this->encryptionManager = $encryptionManager;
+ $this->photoMetadataMapper = $photoMetadataMapper;
+ $this->metadataService = $metadataService;
+ }
+
+ protected function configure() {
+ $this
+ ->setName('photos:extractmetadata')
+ ->setDescription('extract metadata from image files: date taken, location...')
+ ->addArgument(
+ 'user_id',
+ InputArgument::OPTIONAL,
+ 'extract photo metadata for the given user'
+ )->addOption(
+ 'path',
+ 'p',
+ InputOption::VALUE_OPTIONAL,
+ 'limit extraction to this photo folder(album), eg. --path="/alice/files/Photos/2020-3/", the user_id is determined by the path and the user_id parameter is ignored'
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ if ($this->encryptionManager->isEnabled()) {
+ $output->writeln('Encryption is enabled. Aborted.');
+ return 1;
+ }
+
+ $this->output = $output;
+
+ $inputPath = $input->getOption('path');
+ if ($inputPath) {
+ $inputPath = '/' . trim($inputPath, '/');
+ list (, $userId,) = explode('/', $inputPath, 3);
+ $user = $this->userManager->get($userId);
+ if ($user !== null) {
+ $this->extractPathPhotosMetadata($user, $inputPath);
+ }
+ } else {
+ $userId = $input->getArgument('user_id');
+ if ($userId === null) {
+ $this->userManager->callForSeenUsers(function (IUser $user) {
+ $this->extractUserPhotosMetadata($user);
+ });
+ } else {
+ $user = $this->userManager->get($userId);
+ if ($user !== null) {
+ $this->extractUserPhotosMetadata($user);
+ }
+ }
+ }
+
+ return 0;
+ }
+
+ private function extractPathPhotosMetadata(IUser $user, string $path) {
+ \OC_Util::tearDownFS();
+ \OC_Util::setupFS($user->getUID());
+ $userFolder = $this->rootFolder->getUserFolder($user->getUID());
+ try {
+ $relativePath = $userFolder->getRelativePath($path);
+ } catch (NotFoundException $e) {
+ $this->output->writeln('Path not found');
+ return;
+ }
+ $pathFolder = $userFolder->get($relativePath);
+ $this->parseFolder($pathFolder);
+ }
+
+ private function extractUserPhotosMetadata(IUser $user) {
+ \OC_Util::tearDownFS();
+ \OC_Util::setupFS($user->getUID());
+
+ $userFolder = $this->rootFolder->getUserFolder($user->getUID());
+ $this->parseFolder($userFolder);
+
+ }
+
+ private function parseFolder(Folder $folder) {
+ // Respect the '.nomedia' file. If present don't traverse the folder
+ if ($folder->nodeExists('.nomedia')) {
+ $this->output->writeln('Skipping folder ' . $folder->getPath());
+ return;
+ }
+
+ $this->output->writeln('Scanning folder ' . $folder->getPath());
+
+ $nodes = $folder->getDirectoryListing();
+
+ foreach ($nodes as $node) {
+ if ($node instanceof Folder) {
+ $this->parseFolder($node);
+ } else if ($node instanceof File) {
+ $this->parseFile($node);
+ }
+ }
+ }
+
+ private function parseFile(File $file) {
+ if ($this->output->getVerbosity() > OutputInterface::VERBOSITY_VERBOSE) {
+ $this->output->writeln('Extracting metadata from ' . $file->getPath());
+ }
+ $mimeType = \OC::$server->getMimeTypeDetector()->detectPath($file->getPath());
+ if (in_array($mimeType, ['image/jpeg'])) {
+ $fileStorage = $file->getStorage();
+ $tempStream = $fileStorage->fopen($file->getInternalPath(),'r');
+ $photoMetadata = $this->metadataService->extractPhotoMetadata($tempStream,$file->getId());
+ fclose($tempStream);
+ $this->photoMetadataMapper->insert($photoMetadata);
+ }
+ }
+
+}
+
diff --git a/lib/Controller/AlbumsController.php b/lib/Controller/AlbumsController.php
index d047474b2..0c6edcf98 100644
--- a/lib/Controller/AlbumsController.php
+++ b/lib/Controller/AlbumsController.php
@@ -27,6 +27,7 @@
use OCA\Files_Sharing\SharedStorage;
use OCA\Photos\AppInfo\Application;
+use OCA\Photos\Db\PhotoMetadataMapper;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
@@ -44,11 +45,18 @@ class AlbumsController extends Controller {
private $userId;
/** @var IRootFolder */
private $rootFolder;
+ /** @var PhotoMetadataMapper */
+ private $photoMetadataMapper;
- public function __construct($appName, IRequest $request, string $userId, IRootFolder $rootFolder) {
+ public function __construct($appName,
+ IRequest $request,
+ string $userId,
+ IRootFolder $rootFolder,
+ PhotoMetadataMapper $photoMetadataMapper) {
parent::__construct($appName, $request);
$this->userId = $userId;
$this->rootFolder = $rootFolder;
+ $this->photoMetadataMapper = $photoMetadataMapper;
}
/**
@@ -65,18 +73,86 @@ public function sharedAlbums(string $path = ''): JSONResponse {
return $this->generate($path, true);
}
- private function generate(string $path, bool $shared): JSONResponse {
- $userFolder = $this->rootFolder->getUserFolder($this->userId);
+ /**
+ * @NoAdminRequired
+ */
+ public function getNumberByMonth(string $path = ''): JSONResponse {
+ return $this->formatPhotosByMonth($path);
+ }
- $folder = $userFolder;
- if ($path !== '') {
- try {
- $folder = $userFolder->get($path);
- } catch (NotFoundException $e) {
- return new JSONResponse([], Http::STATUS_NOT_FOUND);
+ /**
+ * @NoAdminRequired
+ * @var yearandmonth : 2016-08
+ */
+ public function getPhotosOfMonth(string $yearandmonth = ''): JSONResponse {
+ if ($yearandmonth == '') {
+ $targetYearMonth = '';
+ }else{
+ $targetYearMonth = $this->convertDate($yearandmonth);
+ }
+
+ $nodes = $this->getAllUserPhotos('');
+ $photosMetadata = $this->getPhotosMetadata($nodes);
+ $photosOfMonth = [];
+ foreach ($photosMetadata as $metadata) {
+ $dateTime = $metadata->getDateTimeOriginal();
+
+ if ($dateTime == '') {
+ if ($targetYearMonth == '') {
+ $photosOfMonth[]= $metadata->getFileId();
+ }
+ continue;
+ }
+
+ $yearMonth = date('Y-M',strtotime($dateTime));
+ if ($yearMonth == $targetYearMonth) {
+ $photosOfMonth[] = $metadata->getFileId();
}
}
+ return new JSONResponse($photosOfMonth, Http::STATUS_OK);
+ }
+
+ private function formatPhotosByMonth(string $path): JSONResponse {
+ $nodes = $this->getAllUserPhotos($path);
+ $photosMetadata = $this->getPhotosMetadata($nodes);
+ //caculate photos in each month
+ $photosByMonth = ["unknown" => 0];
+ foreach ($photosMetadata as $metadata) {
+ $dateTime = $metadata->getDateTimeOriginal();
+ if ($dateTime == "") {
+ $photosByMonth['unknown'] = $photosByMonth['unknown'] + 1;
+ continue;
+ }
+ $yearMonth = date('Y-M',strtotime($dateTime));
+ if (array_key_exists($yearMonth,$photosByMonth)) {
+ $photosByMonth[$yearMonth] = $photosByMonth[$yearMonth] + 1;
+ } else {
+ $photosByMonth[$yearMonth] = 1;
+ }
+ }
+ return new JSONResponse($photosByMonth,Http::STATUS_OK);
+ }
+
+ private function getAllUserPhotos(string $path) {
+ $folder = $this->getFolder($path);
+ if (is_null($folder)) {
+ return new JSONResponse([], Http::STATUS_NOT_FOUND);
+ }
+ $nodes = $this->scanCurrentFolderRecusive($folder,false);
+ return $nodes;
+ }
+
+ private function getPhotosMetadata(iterable $nodes) {
+ $fileIds = [];
+ foreach ($nodes as $node) {
+ $fileIds[] = $node->getId();
+ }
+ return $this->photoMetadataMapper->findAll($fileIds);
+ }
+
+ private function generate(string $path, bool $shared): JSONResponse {
+ $folder = $this->getFolder($path);
$data = $this->scanCurrentFolder($folder, $shared);
$result = $this->formatData($data);
@@ -108,6 +184,25 @@ private function formatData(iterable $nodes): array {
return $result;
}
+
+ private function scanCurrentFolderRecusive(Folder $folder): iterable
+ {
+ $nodes = $folder->getDirectoryListing();
+
+ foreach ($nodes as $node) {
+ if ($node instanceof Folder) {
+ foreach ($this->scanCurrentFolderRecusive($node) as $subnode) {
+ if ($subnode instanceof File) {
+ yield $subnode;
+ }
+ }
+ } elseif ($node instanceof File) {
+ if ($this->validFile($node, false)) {
+ yield $node;
+ }
+ }
+ }
+ }
private function scanCurrentFolder(Folder $folder, bool $shared): iterable {
$nodes = $folder->getDirectoryListing();
@@ -125,6 +220,22 @@ private function scanCurrentFolder(Folder $folder, bool $shared): iterable {
}
}
}
+
+ private function getFolder(string $path) {
+ $userFolder = $this->rootFolder->getUserFolder($this->userId);
+
+ $folder = $userFolder;
+ if ($path !== '') {
+ try {
+ $folder = $userFolder->get($path);
+ } catch (NotFoundException $e) {
+ return null;
+ }
+ }
+ return $folder;
+ }
+
+
private function validFile(File $file, bool $shared): bool {
if (in_array($file->getMimeType(), Application::MIMES) && $this->isShared($file) === $shared) {
@@ -173,4 +284,13 @@ private function scanFolder(Folder $folder, int $depth, bool $shared): bool {
return false;
}
+
+ /**
+ * convert 2016-08 to 2016-Aug
+ */
+ private function convertDate(string $yearMonth) {
+ $temp = str_replace('-','',$yearMonth);
+ $month = date('M', strtotime($temp . '01'));
+ return explode('-',$yearMonth)[0] . '-' . $month;
+ }
}
diff --git a/lib/Db/PhotoMetadata.php b/lib/Db/PhotoMetadata.php
new file mode 100644
index 000000000..ecf851b42
--- /dev/null
+++ b/lib/Db/PhotoMetadata.php
@@ -0,0 +1,36 @@
+addType('id','integer');
+ $this->addType('fileId','integer');
+ }
+
+ public function jsonSerialize() {
+ return [
+ 'id' => $this->id,
+ 'dateTimeOriginal' => $this->dateTimeOriginal,
+ 'gpsLatitude' => $this->gpsLatitude,
+ 'gpsLongitude' => $this->gpsLongitude,
+ 'gpsLatitudeRefl' => $this->gpsLatitudeRef,
+ 'gpsLongitudeRef' => $this->gpsLongitudeRef,
+ 'path' => $this->path,
+ ];
+ }
+}
+
+?>
diff --git a/lib/Db/PhotoMetadataMapper.php b/lib/Db/PhotoMetadataMapper.php
new file mode 100644
index 000000000..28a891d23
--- /dev/null
+++ b/lib/Db/PhotoMetadataMapper.php
@@ -0,0 +1,50 @@
+table_name,PhotoMetadata::class);
+ }
+
+ public function find(int $fileId){
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('*')
+ ->from($this->table_name)
+ ->where(
+ $qb->expr()->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))
+ );
+ try{
+ return $this->findEntity($qb);
+ }catch(DoesNotExistException $e){
+ return null;
+ }
+
+ }
+ /**
+ * @throws \OCP\AppFramework\Db\DoesNotExistException if not found
+ *
+ * @param array $fileIds
+ */
+ public function findAll(array $fileIds) {
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('*')
+ ->from($this->table_name);
+
+ $filesMetadata = $this->findEntities($qb);
+ return array_filter($filesMetadata, function($fileMetadata) use ($fileIds) {
+ return in_array($fileMetadata->getFileId(),$fileIds);
+ });
+ }
+}
diff --git a/lib/Migration/Version000000Date20201002183800.php b/lib/Migration/Version000000Date20201002183800.php
new file mode 100644
index 000000000..7aab0e321
--- /dev/null
+++ b/lib/Migration/Version000000Date20201002183800.php
@@ -0,0 +1,61 @@
+hasTable($tableName)) {
+ $table = $schema->createTable($tableName);
+ $table->addColumn('id', 'integer', [
+ 'notnull' => true,
+ 'autoincrement' => true,
+ ]);
+
+ $table->addColumn('file_id', 'integer', [
+ 'notnull' => false,
+ 'length' => 64,
+ ]);
+ $table->addColumn('date_time_original', 'string', [
+ 'notnull' => false,
+ 'length' => 200
+ ]);
+ $table->addColumn('gps_latitude', 'string', [
+ 'notnull' => false,
+ ]);
+ $table->addColumn('gps_longitude', 'string', [
+ 'notnull' => false
+ ]);
+ $table->addColumn('gps_latitude_ref', 'string', [
+ 'notnull' => false
+ ]);
+ $table->addColumn('gps_longitude_ref', 'string', [
+ 'notnull' => false
+ ]);
+
+ $table->addColumn('path', 'string', [
+ 'notnull' => false
+ ]);
+
+
+ $table->setPrimaryKey(['id']);
+ }
+ return $schema;
+ }
+}
diff --git a/lib/Service/MetadataService.php b/lib/Service/MetadataService.php
new file mode 100644
index 000000000..a004c755e
--- /dev/null
+++ b/lib/Service/MetadataService.php
@@ -0,0 +1,34 @@
+ $metadata['DateTimeOriginal'] ?? "",
+ 'GPSLatitude' => implode('-',$metadata['GPSLatitude']) ?? "",
+ 'GPSLongitude' => implode('-',$metadata['GPSLongitude']) ?? "",
+ 'GPSLatitudeRef' => $metadata['GPSLatitudeRef'] ?? "",
+ 'GPSLongitudeRef' => $metadata['GPSLongitudeRef'] ?? ""
+ ];
+ // save metadatat to database
+ $photoMetadata = new PhotoMetadata();
+ $photoMetadata->setFileId($fileId);
+ $photoMetadata->setDateTimeOriginal($metadata['DateTimeOriginal']);
+ $photoMetadata->setGpsLatitude($metadata['GPSLatitude']);
+ $photoMetadata->setGpsLongitude($metadata['GPSLongitude']);
+ $photoMetadata->setGpsLatitudeRef($metadata['GPSLatitudeRef']);
+ $photoMetadata->setGpsLongitudeRef($metadata['GPSLongitudeRef']);
+ return $photoMetadata;
+ }
+}
+
+
+?>
diff --git a/lib/Storage.php b/lib/Storage.php
new file mode 100644
index 000000000..a9c29d0b6
--- /dev/null
+++ b/lib/Storage.php
@@ -0,0 +1,106 @@
+
+ * @author Björn Schießle
+ * @author Christoph Wurst
+ * @author Julius Härtl
+ * @author Morris Jobke
+ * @author Robin Appelman
+ * @author Roeland Jago Douma
+ * @author Vincent Petry
+ *
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License, version 3,
+ * along with this program. If not, see
+ *
+ */
+
+namespace OCA\Photos;
+
+use OC\Files\Filesystem;
+use OC\Files\Storage\Wrapper\Wrapper;
+use OCP\Files\IRootFolder;
+use OCP\ILogger;
+
+use OCA\Photos\Service\MetadataService;
+use OCA\Photos\Db\PhotoMetadataMapper;
+
+class Storage extends Wrapper {
+
+ /** @var ILogger */
+ private $logger;
+
+ /** @var IRootFolder */
+ private $rootFolder;
+
+/** @var MetadataService */
+ private $metadataService;
+
+/** @var PhotoMetadataMapper */
+ private $photoMetadataMapper;
+
+ /**
+ * Storage constructor.
+ *
+ * @param array $parameters
+ * @param ILogger|null $logger
+ * @param IRootFolder|null $rootFolder
+ * @param MetadataService|null $metadataService
+
+ */
+ public function __construct(
+ $parameters,
+ ILogger $logger = null,
+ IRootFolder $rootFolder = null,
+ MetadataService $metadataService = null,
+ PhotoMetadataMapper $photoMetadataMapper = null
+ ) {
+ $this->logger = $logger;
+ $this->rootFolder = $rootFolder;
+ $this->metadataService = $metadataService;
+ $this->photoMetadataMapper = $photoMetadataMapper;
+ parent::__construct($parameters);
+ }
+
+ /**
+ * @return bool
+ */
+ public function writeStream(string $path, $stream, int $size = null): int {
+ $mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path);
+ if (in_array($mimeType, ['image/jpeg'])) {
+ $photoMetadata = $this->metadataService->extractPhotoMetadata($stream);
+ $filePath = $path;
+ $filePath = preg_replace('/\\.[^.\\s]+$/', '', trim($filePath,'.part'));
+ $photoMetadata->setPath($filePath);
+ $this->photoMetadataMapper->insert($photoMetadata);
+ }
+ return parent::writeStream($path,$stream,$size);
+ }
+
+ /**
+ * Setup the storate wrapper callback
+ */
+ public static function setupStorage() {
+ \OC\Files\Filesystem::addStorageWrapper('oc_photos', function ($mountPoint, $storage) {
+ return new \OCA\Photos\Storage(
+ ['storage' => $storage],
+ \OC::$server->getLogger(),
+ \OC::$server->getLazyRootFolder(),
+ \OC::$server->get("OCA\Photos\Service\MetadataService"),
+ \OC::$server->get("OCA\Photos\Db\PhotoMetadataMapper")
+ );
+ }, 1);
+ }
+}