Skip to content

Commit

Permalink
Merge pull request #198 from matiasdelellis/shared-storage-experiments
Browse files Browse the repository at this point in the history
This branch ended up being the introduction of a FileService, to simplify a lot of code.
The support of shared files is quite proven, but will remain experimental, hidden to admin/users.. Note that also automatically gain support of encrypted files. (See issue #201 ) grimacing
  • Loading branch information
matiasdelellis authored Dec 18, 2019
2 parents e48f540 + a42f2e4 commit 99e2ad0
Show file tree
Hide file tree
Showing 10 changed files with 361 additions and 162 deletions.
53 changes: 15 additions & 38 deletions lib/BackgroundJob/Tasks/AddMissingImagesTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,13 @@

use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IHomeStorage;

use OCA\FaceRecognition\BackgroundJob\FaceRecognitionBackgroundTask;
use OCA\FaceRecognition\BackgroundJob\FaceRecognitionContext;
use OCA\FaceRecognition\Db\Image;
use OCA\FaceRecognition\Db\ImageMapper;
use OCA\FaceRecognition\Helper\Requirements;
use OCA\FaceRecognition\Migration\AddDefaultFaceModel;
use OCA\FaceRecognition\Service\FileService;

/**
* Task that, for each user, crawls for all images in filesystem and insert them in database.
Expand All @@ -51,14 +50,21 @@ class AddMissingImagesTask extends FaceRecognitionBackgroundTask {
/** @var ImageMapper Image mapper */
private $imageMapper;

/** @var FileService */
private $fileService;

/**
* @param IConfig $config Config
* @param ImageMapper $imageMapper Image mapper
* @param FileService $fileService File Service
*/
public function __construct(IConfig $config, ImageMapper $imageMapper) {
public function __construct(IConfig $config,
ImageMapper $imageMapper,
FileService $fileService) {
parent::__construct();
$this->config = $config;
$this->config = $config;
$this->imageMapper = $imageMapper;
$this->fileService = $fileService;
}

/**
Expand Down Expand Up @@ -120,11 +126,10 @@ public function execute(FaceRecognitionContext $context) {
*/
private function addMissingImagesForUser(string $userId, int $model): int {
$this->logInfo(sprintf('Finding missing images for user %s', $userId));
\OC_Util::tearDownFS();
\OC_Util::setupFS($userId);
$this->fileService->setupFS($userId);

$userFolder = $this->context->rootFolder->getUserFolder($userId);
return $this->parseUserFolder($model, $userFolder);
return $this->parseUserFolder($userId, $model, $userFolder);
}

/**
Expand All @@ -134,14 +139,14 @@ private function addMissingImagesForUser(string $userId, int $model): int {
* @param Folder $folder Folder to recursively search images in
* @return int Number of missing images found
*/
private function parseUserFolder(int $model, Folder $folder): int {
private function parseUserFolder(string $userId, int $model, Folder $folder): int {
$insertedImages = 0;
$nodes = $this->getPicturesFromFolder($folder);
$nodes = $this->fileService->getPicturesFromFolder($folder);
foreach ($nodes as $file) {
$this->logDebug('Found ' . $file->getPath());

$image = new Image();
$image->setUser($file->getOwner()->getUid());
$image->setUser($userId);
$image->setFile($file->getId());
$image->setModel($model);
// todo: this check/insert logic for each image is so inefficient it hurts my mind
Expand All @@ -155,32 +160,4 @@ private function parseUserFolder(int $model, Folder $folder): int {
return $insertedImages;
}

/**
* Return all images from a given folder.
*
* TODO: It is inefficient since it copies the array recursively.
*
* @param Folder $folder Folder to get images from
* @return array List of all images and folders to continue recursive crawling
*/
private function getPicturesFromFolder(Folder $folder, $results = array()) {
// todo: should we also care about this too: instanceOfStorage(ISharedStorage::class);
if ($folder->getStorage()->instanceOfStorage(IHomeStorage::class) === false) {
return $results;
}

$nodes = $folder->getDirectoryListing();

foreach ($nodes as $node) {
if ($node instanceof Folder and !$node->nodeExists('.nomedia')) {
$results = $this->getPicturesFromFolder($node, $results);
} else if ($node instanceof File) {
if (Requirements::isImageTypeSupported($node->getMimeType())) {
$results[] = $node;
}
}
}

return $results;
}
}
48 changes: 28 additions & 20 deletions lib/BackgroundJob/Tasks/ImageProcessingTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\IConfig;
use OCP\ITempManager;
use OCP\IUser;

use OCA\FaceRecognition\BackgroundJob\FaceRecognitionBackgroundTask;
Expand All @@ -39,6 +38,8 @@
use OCA\FaceRecognition\Helper\Requirements;
use OCA\FaceRecognition\Migration\AddDefaultFaceModel;

use OCA\FaceRecognition\Service\FileService;

/**
* Plain old PHP object holding all information
* that are needed to process all faces from one image
Expand Down Expand Up @@ -115,20 +116,25 @@ class ImageProcessingTask extends FaceRecognitionBackgroundTask {
/** @var ImageMapper Image mapper*/
protected $imageMapper;

/** @var ITempManager */
private $tempManager;
/** @var FileService */
private $fileService;

/** @var int|null Maximum image area (cached, so it is not recalculated for each image) */
private $maxImageAreaCached;

/**
* @param IConfig $config
* @param ImageMapper $imageMapper Image mapper
* @param FileService $fileService
*/
public function __construct(IConfig $config, ImageMapper $imageMapper, ITempManager $tempManager) {
public function __construct(IConfig $config,
ImageMapper $imageMapper,
FileService $fileService)
{
parent::__construct();
$this->config = $config;
$this->imageMapper = $imageMapper;
$this->tempManager = $tempManager;
$this->config = $config;
$this->imageMapper = $imageMapper;
$this->fileService = $fileService;
$this->maxImageAreaCached = null;
}

Expand All @@ -148,7 +154,6 @@ public function execute(FaceRecognitionContext $context) {
$model = intval($this->config->getAppValue('facerecognition', 'model', AddDefaultFaceModel::DEFAULT_FACE_MODEL_ID));
$requirements = new Requirements($context->modelService, $model);

$dataDir = rtrim($context->config->getSystemValue('datadirectory', \OC::$SERVERROOT.'/data'), '/');
$images = $context->propertyBag['images'];

$cfd = new \CnnFaceDetection($requirements->getFaceDetectionModel());
Expand All @@ -160,11 +165,11 @@ public function execute(FaceRecognitionContext $context) {
foreach($images as $image) {
yield;

$imageProcessingContext = null;
$startMillis = round(microtime(true) * 1000);

try {
$imageProcessingContext = $this->findFaces($cfd, $dataDir, $image);
$imageProcessingContext = $this->findFaces($cfd, $image);

if (($imageProcessingContext !== null) && ($imageProcessingContext->getSkipDetection() === false)) {
$this->populateDescriptors($fld, $fr, $imageProcessingContext);
}
Expand All @@ -184,7 +189,7 @@ public function execute(FaceRecognitionContext $context) {
$this->logDebug($e);
$this->imageMapper->imageProcessed($image, array(), 0, $e);
} finally {
$this->tempManager->clean();
$this->fileService->clean();
}
}

Expand All @@ -197,22 +202,22 @@ public function execute(FaceRecognitionContext $context) {
* If there is any error, throws exception
*
* @param \CnnFaceDetection $cfd Face detection model
* @param string $dataDir Directory where data is stored
* @param Image $image Image to find faces on
* @return ImageProcessingContext|null Generated context that hold all information needed later for this image
*/
private function findFaces(\CnnFaceDetection $cfd, string $dataDir, Image $image) {
private function findFaces(\CnnFaceDetection $cfd, Image $image) {
// todo: check if this hits I/O (database, disk...), consider having lazy caching to return user folder from user
$userFolder = $this->context->rootFolder->getUserFolder($image->user);
$userRoot = $userFolder->getParent();
$file = $userRoot->getById($image->file);
$file = $this->fileService->getFileById($image->getFile(), $image->getUser());

if (empty($file)) {
// If we cannot find a file probably it was deleted out of our control and we must clean our tables.
$this->config->setUserValue($image->user, 'facerecognition', StaleImagesRemovalTask::STALE_IMAGES_REMOVAL_NEEDED_KEY, 'true');
$this->logInfo('File with ID ' . $image->file . ' doesn\'t exist anymore, skipping it');
return null;
}

// todo: this concat is wrong with shared files.
$imagePath = $dataDir . $file[0]->getPath();
$imagePath = $this->fileService->getLocalFile($file);

$this->logInfo('Processing image ' . $imagePath);
$imageProcessingContext = $this->prepareImage($imagePath);
if ($imageProcessingContext->getSkipDetection() === true) {
Expand Down Expand Up @@ -248,6 +253,7 @@ private function prepareImage(string $imagePath) {
$image = new OCP_Image(null, $this->context->logger->getLogger(), $this->context->config);
$image->loadFromFile($imagePath);
$image->fixOrientation();

if (!$image->valid()) {
throw new \RuntimeException("Image is not valid, probably cannot be loaded");
}
Expand All @@ -261,16 +267,17 @@ private function prepareImage(string $imagePath) {
$maxImageArea = $this->getMaxImageArea();
$ratio = $this->resizeImage($image, $maxImageArea);

$tempfile = $this->tempManager->getTemporaryFile(pathinfo($imagePath, PATHINFO_EXTENSION));
$tempfile = $this->fileService->getTemporaryFile(pathinfo($imagePath, PATHINFO_EXTENSION));
$image->save($tempfile);

return new ImageProcessingContext($imagePath, $tempfile, $ratio, false);
}

/**
* Resizes the image to reach max image area, but preserving ratio.
* Stolen and adopted from OC_Image->resize() (difference is that this returns ratio of resize.)
*
* @param OC_Image $image Image to resize
* @param Image $image Image to resize
* @param int $maxImageArea The maximum size of image we can handle (in pixels^2).
*
* @return float Ratio of resize. 1 if there was no resize
Expand Down Expand Up @@ -383,4 +390,5 @@ private function calculateMaxImageArea(): int {
$maxImageArea = intval((0.75 * $allowedMemory) / 1024); // in pixels^2
return $maxImageArea;
}

}
79 changes: 25 additions & 54 deletions lib/BackgroundJob/Tasks/StaleImagesRemovalTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IHomeStorage;
use OCP\Files\Node;

use OCA\FaceRecognition\BackgroundJob\FaceRecognitionBackgroundTask;
use OCA\FaceRecognition\BackgroundJob\FaceRecognitionContext;
Expand All @@ -37,6 +37,7 @@
use OCA\FaceRecognition\Db\FaceMapper;
use OCA\FaceRecognition\Db\PersonMapper;
use OCA\FaceRecognition\Migration\AddDefaultFaceModel;
use OCA\FaceRecognition\Service\FileService;

/**
* Task that, for each user, crawls for all images in database,
Expand All @@ -59,18 +60,27 @@ class StaleImagesRemovalTask extends FaceRecognitionBackgroundTask {
/** @var PersonMapper Person mapper */
private $personMapper;

/** @var FileService */
private $fileService;

/**
* @param IConfig $config Config
* @param ImageMapper $imageMapper Image mapper
* @param FaceMapper $faceMapper Face mapper
* @param PersonMapper $personMapper Person mapper
* @param FileService $fileService File Service
*/
public function __construct(IConfig $config, ImageMapper $imageMapper, FaceMapper $faceMapper, PersonMapper $personMapper) {
public function __construct(IConfig $config,
ImageMapper $imageMapper,
FaceMapper $faceMapper,
PersonMapper $personMapper,
FileService $fileService) {
parent::__construct();
$this->config = $config;
$this->imageMapper = $imageMapper;
$this->faceMapper = $faceMapper;
$this->imageMapper = $imageMapper;
$this->faceMapper = $faceMapper;
$this->personMapper = $personMapper;
$this->fileService = $fileService;
}

/**
Expand Down Expand Up @@ -134,8 +144,8 @@ public function execute(FaceRecognitionContext $context) {
* which represent number of stale images removed
*/
private function staleImagesRemovalForUser(string $userId, int $model) {
\OC_Util::tearDownFS();
\OC_Util::setupFS($userId);

$this->fileService->setupFS($userId);

$this->logDebug(sprintf('Getting all images for user %s', $userId));
$allImages = $this->imageMapper->findImages($userId, $model);
Expand Down Expand Up @@ -165,17 +175,19 @@ private function staleImagesRemovalForUser(string $userId, int $model) {
yield;

// Now iterate and check remaining images
$userFolder = $this->context->rootFolder->getUserFolder($userId);
$processed = 0;
$imagesRemoved = 0;
foreach ($allImages as $image) {
// Delete image doesn't exist anymore in filesystem or it is under .nomedia
$mount = $this->getHomeMount($userFolder, $image);
// Try to get the file to ensure that exist.
try {
$file = $this->fileService->getFileById($image->getFile(), $userId);
} catch (\OCP\Files\NotFoundException $e) {
$file = null;
}

if ($mount === null) {
$this->deleteImage($image, $userId);
$imagesRemoved++;
} else if ($this->isUnderNoMedia($mount)) {
// Delete image doesn't exist anymore in filesystem or it is under .nomedia
if (($file === null) || (!$this->fileService->isAllowedNode($file)) ||
($this->fileService->isUnderNoDetection($file))) {
$this->deleteImage($image, $userId);
$imagesRemoved++;
}
Expand All @@ -197,47 +209,6 @@ private function staleImagesRemovalForUser(string $userId, int $model) {
return $imagesRemoved;
}

/**
* For a given image, tries to find home mount. Returns null if it is not found (equivalent of image does not exist).
*
* @param Folder $userFolder User folder to search in
* @param Image $image Image to find home mount for
*
* @return File|null File if image file node is found, null otherwise.
*/
private function getHomeMount(Folder $userFolder, Image $image) {
$allMounts = $userFolder->getById($image->file);
$homeMounts = array_filter($allMounts, function ($m) {
return $m->getStorage()->instanceOfStorage(IHomeStorage::class);
});

if (count($homeMounts) === 0) {
return null;
} else {
return $homeMounts[0];
}
}

/**
* Checks if this file is located somewhere under .nomedia file and should be therefore ignored.
* TODO: same method is in Watcher.php, find a place for both methods
*
* @param File $file File to search for
* @return bool True if file is located under .nomedia, false otherwise
*/
private function isUnderNoMedia(File $file): bool {
// If we detect .nomedia file anywhere on the path to root folder (id===null), bail out
$parentNode = $file->getParent();
while (($parentNode instanceof Folder) && ($parentNode->getId() !== null)) {
if ($parentNode->nodeExists('.nomedia')) {
return true;
}
$parentNode = $parentNode->getParent();
}

return false;
}

private function deleteImage(Image $image, string $userId) {
$this->logInfo(sprintf('Removing stale image %d for user %s', $image->id, $userId));
// note that invalidatePersons depends on existence of faces for a given image,
Expand Down
Loading

0 comments on commit 99e2ad0

Please sign in to comment.