Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: timeline server supporting features #466

Closed
wants to merge 4 commits into from
Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
command working, prepred to add wrapper
Signed-off-by: xiangbin.li <[email protected]>
dassio committed Oct 6, 2020
commit ff4930d5b26111176648f31148faf2e5e78aefd7
52 changes: 28 additions & 24 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
<?xml version="1.0"?>
<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<id>photos</id>
<name>Photos</name>
<summary>Your memories under your control</summary>
<description>Your memories under your control</description>
<version>1.3.0</version>
<licence>agpl</licence>
<author mail="[email protected]">John Molakvoæ</author>
<namespace>Photos</namespace>
<category>multimedia</category>
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<id>photos</id>
<name>Photos</name>
<summary>Your memories under your control</summary>
<description>Your memories under your control</description>
<version>1.3.0</version>
<licence>agpl</licence>
<author mail="[email protected]">John Molakvoæ</author>
<namespace>Photos</namespace>
<category>multimedia</category>

<website>https://github.com/nextcloud/photos</website>
<bugs>https://github.com/nextcloud/photos/issues</bugs>
<repository>https://github.com/nextcloud/photos.git</repository>
<default_enable />
<dependencies>
<nextcloud min-version="20" max-version="21" />
</dependencies>
<navigations>
<navigation>
<name>Photos</name>
<route>photos.page.index</route>
<order>1</order>
</navigation>
</navigations>
<commands>
<command>OCA\Photos\Command\ExtractMetadata</command>
</commands>

<website>https://github.com/nextcloud/photos</website>
<bugs>https://github.com/nextcloud/photos/issues</bugs>
<repository>https://github.com/nextcloud/photos.git</repository>
<default_enable />
<dependencies>
<nextcloud min-version="21" max-version="21" />
</dependencies>
<navigations>
<navigation>
<name>Photos</name>
<route>photos.page.index</route>
<order>1</order>
</navigation>
</navigations>
</info>
25 changes: 25 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
@@ -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/{path}',
'verb' => 'GET',
'requirements' => [
'yearandmonth' => '.*',
'path' => '.*',
],
'defaults' => [
'yearandmonth' => '',
'path' => '',
],
],

]
];
2 changes: 2 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
@@ -52,11 +52,13 @@ class Application extends App implements IBootstrap {

public function __construct() {
parent::__construct(self::APP_ID);

}

public function register(IRegistrationContext $context): void {
}

public function boot(IBootContext $context): void {
\OCP\Util::connectHook('OC_Filesystem', 'preSetup', 'OCA\Photos\Storage', 'setupStorage');
}
}
168 changes: 168 additions & 0 deletions lib/Command/ExtractMetadata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);


namespace OCA\Photos\Command;

use OCP\Encryption\IManager;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\IConfig;
use OCP\IUser;
use OCP\IUserManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

use OCA\Photos\Db\PhotoMetadata;
use OCA\Photos\Db\PhotoMetadataMapper;
use OCA\Photos\Service\MetadataService;

class ExtractMetadata extends Command {

/** @var IUserManager */
protected $userManager;

/** @var IRootFolder */
protected $rootFolder;

/** @var IConfig */
protected $config;

/** @var OutputInterface */
protected $output;

/** @var IManager */
protected $encryptionManager;

/** @var PhotoMetadataMapper */
private $photoMetadataMapper;

/** @var MetadataService */
private $metadataService;

public function __construct(IRootFolder $rootFolder,
IUserManager $userManager,
IConfig $config,
IManager $encryptionManager,
PhotoMetadataMapper $photoMetadataMapper,
MetadataService $metadataService) {
parent::__construct();

$this->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'])) {
$photoMetadata = $this->metadataService->extractPhotoMetadata($file);
$this->photoMetadataMapper->insert($photoMetadata);
}
}

}

92 changes: 82 additions & 10 deletions lib/Controller/AlbumsController.php
Original file line number Diff line number Diff line change
@@ -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;

public function __construct($appName, IRequest $request, string $userId, IRootFolder $rootFolder) {
/** @var PhotoMetadataMapper */
private $photoMetadataMapper;

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,47 @@ 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);
private function formatPhotosByMonth(string $path): JSONResponse {
$folder = $this->getFolder($path);
if (is_null($folder)) {
return new JSONResponse([], Http::STATUS_NOT_FOUND);
}
$nodes = $this->scanCurrentFolderRecusive($folder,false);

// get all photos metadata
$fileIds = [];
foreach ($nodes as $node) {
$fileIds[] = $node->getId();
}
$photosMetadata = $this->photoMetadataMapper->findAll($fileIds);

//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 generate(string $path, bool $shared): JSONResponse {
$folder = $this->getFolder($path);
$data = $this->scanCurrentFolder($folder, $shared);
$result = $this->formatData($data);

@@ -108,6 +145,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 +181,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) {
36 changes: 36 additions & 0 deletions lib/Db/PhotoMetadata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php
namespace OCA\Photos\Db;

use JsonSerializable;

use OCP\AppFramework\Db\Entity;

class PhotoMetadata extends Entity implements JsonSerializable {

protected $fileId;
protected $dateTimeOriginal;
protected $gpsLatitude;
protected $gpsLongitude;
protected $gpsLatitudeRef;
protected $gpsLongitudeRef;
protected $path;

public function __construct() {
$this->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,
];
}
}

?>
50 changes: 50 additions & 0 deletions lib/Db/PhotoMetadataMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php
// db/FileCacheMapper.php

namespace OCA\Photos\Db;

use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\AppFramework\Db\QBMapper;
use OCP\AppFramework\Db\DoesNotExistException;

class PhotoMetadataMapper extends QBMapper {

private $table_name = 'photosmetadata';

public function __construct(IDBConnection $db) {
parent::__construct($db, $this->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);
});
}
}
63 changes: 63 additions & 0 deletions lib/Migration/Version000000Date20201002183800.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

namespace OCA\Photos\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\SimpleMigrationStep;
use OCP\Migration\IOutput;

class Version000000Date20201002183800 extends SimpleMigrationStep
{

/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options)
{
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$tableName = "photosmetadata";
if (!$schema->hasTable($tableName)) {
$table = $schema->createTable($tableName);
$table->addColumn('id', 'integer', [
'notnull' => true,
'autoincrement' => true,
'unsigned' => true,
]);

$table->addColumn('file_id', 'integer', [
'notnull' => true,
'unsigned' => true,
'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;
}
}
37 changes: 37 additions & 0 deletions lib/Service/MetadataService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php
namespace OCA\Photos\Service;

use OCA\Photos\Db\PhotoMetadata;
use OCP\Files\File;

class MetadataService {

public function __construct() {
}

public function extractPhotoMetadata(File $file): PhotoMetadata {
$fileStorage = $file->getStorage();
$tempStream = $fileStorage->fopen($file->getInternalPath(),'r');
$metadata = exif_read_data($tempStream);
fclose($tempStream);
$metadata = [
'DateTimeOriginal' => $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($file->getId());
$photoMetadata->setDateTimeOriginal($metadata['DateTimeOriginal']);
$photoMetadata->setGpsLatitude($metadata['GPSLatitude']);
$photoMetadata->setGpsLongitude($metadata['GPSLongitude']);
$photoMetadata->setGpsLatitudeRef($metadata['GPSLatitudeRef']);
$photoMetadata->setGpsLongitudeRef($metadata['GPSLongitudeRef']);
return $photoMetadata;
}
}


?>
109 changes: 109 additions & 0 deletions lib/Storage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bjoern Schiessle <bjoern@schiessle.org>
* @author Björn Schießle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Julius Härtl <jus@bitgrid.net>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Vincent Petry <pvince81@owncloud.com>
*
* @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 <http://www.gnu.org/licenses/>
*
*/

namespace OCA\Photos;

use OC\Files\Filesystem;
use OC\Files\Storage\Wrapper\Wrapper;
use OCA\Files_Trashbin\Events\MoveToTrashEvent;
use OCA\Files_Trashbin\Trash\ITrashManager;
use OCP\Encryption\Exceptions\GenericEncryptionException;
use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountPoint;
use OCP\Files\Node;
use OCP\ILogger;
use OCP\IUserManager;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class Storage extends Wrapper {
/** @var IMountPoint */
private $mountPoint;

/** @var IUserManager */
private $userManager;

/** @var ILogger */
private $logger;

/** @var EventDispatcherInterface */
private $eventDispatcher;

/** @var IRootFolder */
private $rootFolder;

/** @var ITrashManager */
private $trashManager;

/**
* Storage constructor.
*
* @param array $parameters
* @param ITrashManager $trashManager
* @param IUserManager|null $userManager
* @param ILogger|null $logger
* @param EventDispatcherInterface|null $eventDispatcher
* @param IRootFolder|null $rootFolder
*/
public function __construct(
$parameters,
ITrashManager $trashManager = null,
IUserManager $userManager = null,
ILogger $logger = null,
EventDispatcherInterface $eventDispatcher = null,
IRootFolder $rootFolder = null
) {
$this->mountPoint = $parameters['mountPoint'];
$this->trashManager = $trashManager;
$this->userManager = $userManager;
$this->logger = $logger;
$this->eventDispatcher = $eventDispatcher;
$this->rootFolder = $rootFolder;
parent::__construct($parameters);
}

/**
* 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, 'mountPoint' => $mountPoint],
\OC::$server->query(ITrashManager::class),
\OC::$server->getUserManager(),
\OC::$server->getLogger(),
\OC::$server->getEventDispatcher(),
\OC::$server->getLazyRootFolder()
);
}, 1);
}

public function getMountPoint() {
return $this->mountPoint;
}
}