Merge branch 'master' into PHRAS-3049_more_setup_in_docker-compose
@@ -209,7 +209,7 @@ CMD ["php-fpm", "-F"]
|
||||
|
||||
FROM phraseanet-fpm as phraseanet-worker
|
||||
ENTRYPOINT ["docker/phraseanet/worker/entrypoint.sh"]
|
||||
CMD ["bin/console", "task-manager:scheduler:run"]
|
||||
CMD ["bin/console", "worker:execute"]
|
||||
|
||||
#########################################################################
|
||||
# phraseanet-nginx
|
||||
|
@@ -58,6 +58,9 @@ use Alchemy\Phrasea\Command\User\UserPasswordCommand;
|
||||
use Alchemy\Phrasea\Command\User\UserListCommand;
|
||||
use Alchemy\Phrasea\Command\UpgradeDBDatas;
|
||||
use Alchemy\Phrasea\Command\ApplyRightsCommand;
|
||||
use Alchemy\Phrasea\WorkerManager\Command\WorkerExecuteCommand;
|
||||
use Alchemy\Phrasea\WorkerManager\Command\WorkerRunServiceCommand;
|
||||
use Alchemy\Phrasea\WorkerManager\Command\WorkerShowConfigCommand;
|
||||
|
||||
require_once __DIR__ . '/../lib/autoload.php';
|
||||
|
||||
@@ -162,9 +165,9 @@ $cli->command(new QueryParseCommand());
|
||||
$cli->command(new QuerySampleCommand());
|
||||
$cli->command(new FindConceptsCommand());
|
||||
|
||||
//$cli->command($cli['alchemy_worker.commands.run_dispatcher_command']);
|
||||
//$cli->command($cli['alchemy_worker.commands.run_worker_command']);
|
||||
//$cli->command($cli['alchemy_worker.commands.show_configuration']);
|
||||
$cli->command(new WorkerExecuteCommand());
|
||||
$cli->command(new WorkerRunServiceCommand());
|
||||
$cli->command(new WorkerShowConfigCommand());
|
||||
|
||||
$cli->loadPlugins();
|
||||
|
||||
|
@@ -133,7 +133,8 @@
|
||||
"facebook/graph-sdk": "^5.6",
|
||||
"box/spout": "^2.7",
|
||||
"paragonie/random-lib": "^2.0",
|
||||
"czproject/git-php": "^3.17"
|
||||
"czproject/git-php": "^3.17",
|
||||
"php-amqplib/php-amqplib": "^2.9"
|
||||
},
|
||||
"require-dev": {
|
||||
"mikey179/vfsstream": "~1.5",
|
||||
|
112
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "008ff0b5d3d13b4f0ce5d34348ded83a",
|
||||
"content-hash": "d986d21a2ad9125f83251d4c3943ffd7",
|
||||
"packages": [
|
||||
{
|
||||
"name": "alchemy-fr/tcpdf-clone",
|
||||
@@ -5145,6 +5145,83 @@
|
||||
],
|
||||
"time": "2019-03-20T17:19:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "php-amqplib/php-amqplib",
|
||||
"version": "v2.11.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-amqplib/php-amqplib.git",
|
||||
"reference": "6353c5d2d3021a301914bc6566e695c99cfeb742"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/6353c5d2d3021a301914bc6566e695c99cfeb742",
|
||||
"reference": "6353c5d2d3021a301914bc6566e695c99cfeb742",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"ext-sockets": "*",
|
||||
"php": ">=5.6.3",
|
||||
"phpseclib/phpseclib": "^2.0.0"
|
||||
},
|
||||
"conflict": {
|
||||
"php": "7.4.0 - 7.4.1"
|
||||
},
|
||||
"replace": {
|
||||
"videlalvaro/php-amqplib": "self.version"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-curl": "*",
|
||||
"nategood/httpful": "^0.2.20",
|
||||
"phpunit/phpunit": "^5.7|^6.5|^7.0",
|
||||
"squizlabs/php_codesniffer": "^2.5"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.11-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PhpAmqpLib\\": "PhpAmqpLib/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-2.1-or-later"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Alvaro Videla",
|
||||
"role": "Original Maintainer"
|
||||
},
|
||||
{
|
||||
"name": "Raúl Araya",
|
||||
"email": "nubeiro@gmail.com",
|
||||
"role": "Maintainer"
|
||||
},
|
||||
{
|
||||
"name": "Luke Bakken",
|
||||
"email": "luke@bakken.io",
|
||||
"role": "Maintainer"
|
||||
},
|
||||
{
|
||||
"name": "Ramūnas Dronga",
|
||||
"email": "github@ramuno.lt",
|
||||
"role": "Maintainer"
|
||||
}
|
||||
],
|
||||
"description": "Formerly videlalvaro/php-amqplib. This library is a pure PHP implementation of the AMQP protocol. It's been tested against RabbitMQ.",
|
||||
"homepage": "https://github.com/php-amqplib/php-amqplib/",
|
||||
"keywords": [
|
||||
"message",
|
||||
"queue",
|
||||
"rabbitmq"
|
||||
],
|
||||
"time": "2020-05-13T13:56:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "php-ffmpeg/php-ffmpeg",
|
||||
"version": "v0.15",
|
||||
@@ -7978,39 +8055,6 @@
|
||||
],
|
||||
"time": "2016-11-25T06:54:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpexiftool/exiftool",
|
||||
"version": "10.10",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/alchemy-fr/exiftool.git",
|
||||
"reference": "0833cab894c890353192a83011428525a318bedf"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/alchemy-fr/exiftool/zipball/0833cab894c890353192a83011428525a318bedf",
|
||||
"reference": "0833cab894c890353192a83011428525a318bedf",
|
||||
"shasum": ""
|
||||
},
|
||||
"type": "library",
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Perl Licensing"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Phil Harvey",
|
||||
"email": "phil@owl.phy.queensu.ca",
|
||||
"homepage": "http://www.sno.phy.queensu.ca/~phil/exiftool/"
|
||||
}
|
||||
],
|
||||
"description": "Exiftool is a library for reading, writing and editing meta information. This package is not PHP, but required for the main PHP driver : PHP Exiftool",
|
||||
"keywords": [
|
||||
"exiftool",
|
||||
"metadatas"
|
||||
],
|
||||
"time": "2016-01-25T11:10:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpspec/prophecy",
|
||||
"version": "v1.6.2",
|
||||
|
@@ -313,6 +313,7 @@ geocoding-providers:
|
||||
- '2.335062'
|
||||
default-zoom: 5
|
||||
marker-default-zoom: 9
|
||||
position-fields: []
|
||||
geonames-field-mapping: true
|
||||
cityfields: City, Ville
|
||||
provincefields: Province
|
||||
@@ -330,3 +331,5 @@ workers:
|
||||
user_account:
|
||||
deleting_policies:
|
||||
email_confirmation: true
|
||||
|
||||
Console_logger_enabled_environments: [test]
|
||||
|
@@ -87,6 +87,8 @@ use Alchemy\Phrasea\Media\MediaAccessorResolver;
|
||||
use Alchemy\Phrasea\Media\PermalinkMediaResolver;
|
||||
use Alchemy\Phrasea\Media\TechnicalDataServiceProvider;
|
||||
use Alchemy\Phrasea\Model\Entities\User;
|
||||
use Alchemy\Phrasea\WorkerManager\Provider\AlchemyWorkerServiceProvider;
|
||||
use Alchemy\Phrasea\WorkerManager\Provider\QueueWorkerServiceProvider;
|
||||
use Alchemy\QueueProvider\QueueServiceProvider;
|
||||
use Alchemy\WorkerProvider\WorkerServiceProvider;
|
||||
use Doctrine\DBAL\Event\ConnectionEventArgs;
|
||||
@@ -268,6 +270,11 @@ class Application extends SilexApplication
|
||||
$this->register(new OrderServiceProvider());
|
||||
$this->register(new WebhookServiceProvider());
|
||||
|
||||
if ($this['configuration.store']->isSetup()) {
|
||||
$this->register(new QueueWorkerServiceProvider());
|
||||
$this->register(new AlchemyWorkerServiceProvider());
|
||||
}
|
||||
|
||||
$this['monolog'] = $this->share(
|
||||
$this->extend('monolog', function (LoggerInterface $logger, Application $app) {
|
||||
|
||||
|
@@ -6,6 +6,7 @@ use Alchemy\EmbedProvider\EmbedServiceProvider;
|
||||
use Alchemy\Phrasea\Application;
|
||||
use Alchemy\Phrasea\ControllerProvider as Providers;
|
||||
use Alchemy\Phrasea\Report\ControllerProvider\ProdReportControllerProvider;
|
||||
use Alchemy\Phrasea\WorkerManager\Provider\ControllerServiceProvider as WorkerManagerProvider;
|
||||
use Assert\Assertion;
|
||||
use Silex\ControllerProviderInterface;
|
||||
|
||||
@@ -28,6 +29,7 @@ class RouteLoader
|
||||
'/admin/setup' => Providers\Admin\Setup::class,
|
||||
'/admin/subdefs' => Providers\Admin\Subdefs::class,
|
||||
'/admin/task-manager' => Providers\Admin\TaskManager::class,
|
||||
'/admin/worker-manager' => WorkerManagerProvider::class,
|
||||
'/admin/users' => Providers\Admin\Users::class,
|
||||
'/client/' => Providers\Client\Root::class,
|
||||
'/datafiles' => Providers\Datafiles::class,
|
||||
|
@@ -368,6 +368,7 @@ class RootController extends Controller
|
||||
'collection',
|
||||
'user',
|
||||
'users',
|
||||
'workermanager'
|
||||
];
|
||||
|
||||
$feature = 'connected';
|
||||
|
@@ -87,6 +87,8 @@ use Alchemy\Phrasea\SearchEngine\SearchEngineResult;
|
||||
use Alchemy\Phrasea\Status\StatusStructure;
|
||||
use Alchemy\Phrasea\TaskManager\LiveInformation;
|
||||
use Alchemy\Phrasea\Utilities\NullableDateTime;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\AssetsCreateEvent;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\WorkerEvents;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Guzzle\Http\Client as Guzzle;
|
||||
use League\Fractal\Resource\Item;
|
||||
@@ -217,6 +219,30 @@ class V1Controller extends Controller
|
||||
return $this->showTaskAction($request, $task);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use with the uploader service
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function sendAssetsInQueue(Request $request)
|
||||
{
|
||||
$jsonBodyHelper = $this->getJsonBodyHelper();
|
||||
$schema = $this->app['json-schema.ref_resolver']->resolve($this->app['json-schema.base_uri']. 'assets_enqueue.json');
|
||||
$data = $request->getContent();
|
||||
|
||||
$errors = $jsonBodyHelper->validateJson(json_decode($data), $schema);
|
||||
|
||||
if (count($errors) > 0) {
|
||||
return Result::createError($request, 422, $errors[0])->createResponse();
|
||||
}
|
||||
|
||||
$this->dispatch(WorkerEvents::ASSETS_CREATE, new AssetsCreateEvent(json_decode($data, true)));
|
||||
|
||||
return Result::create($request, [
|
||||
"data" => json_decode($data),
|
||||
])->createResponse();
|
||||
}
|
||||
|
||||
private function getCacheInformation()
|
||||
{
|
||||
$caches = [
|
||||
|
@@ -284,6 +284,9 @@ class V1 extends Api implements ControllerProviderInterface, ServiceProviderInte
|
||||
$controllers->post('/accounts/unlock/{token}/', 'controller.api.v1:unlockAccount')
|
||||
->before('controller.api.v1:ensureUserManagementRights');
|
||||
|
||||
// the api route for the uploader service
|
||||
$controllers->post('/upload/enqueue/', 'controller.api.v1:sendAssetsInQueue');
|
||||
|
||||
return $controllers;
|
||||
}
|
||||
}
|
||||
|
@@ -54,6 +54,7 @@ class ControllerProviderServiceProvider implements ServiceProviderInterface
|
||||
Admin\Setup::class => [],
|
||||
Admin\Subdefs::class => [],
|
||||
Admin\TaskManager::class => [],
|
||||
\Alchemy\Phrasea\WorkerManager\Provider\ControllerServiceProvider::class => [],
|
||||
Admin\Users::class => [],
|
||||
Client\Root::class => [],
|
||||
Datafiles::class => [],
|
||||
|
@@ -50,7 +50,8 @@ class DisplaySettingService
|
||||
'bask_val_order' => 'nat',
|
||||
'basket_caption_display' => '0',
|
||||
'basket_status_display' => '0',
|
||||
'basket_title_display' => '0'
|
||||
'basket_title_display' => '0',
|
||||
'basket_type_display' => '0'
|
||||
];
|
||||
|
||||
/**
|
||||
|
@@ -181,19 +181,14 @@ class RegistryFormManipulator
|
||||
],
|
||||
'custom-links' => [
|
||||
[
|
||||
'linkName' => 'Phraseanet store',
|
||||
'linkLanguage' => 'fr',
|
||||
'linkUrl' => 'https://alchemy.odoo.com/shop',
|
||||
'linkLocation' => 'help-menu',
|
||||
'linkOrder' => '1',
|
||||
],
|
||||
[
|
||||
'linkName' => 'Phraseanet store',
|
||||
'linkLanguage' => 'en',
|
||||
'linkUrl' => 'https://alchemy.odoo.com/en_US/shop',
|
||||
'linkLocation' => 'help-menu',
|
||||
'linkOrder' => '1',
|
||||
],
|
||||
'linkName' => 'Phraseanet store',
|
||||
'linkLanguage' => 'all',
|
||||
'linkUrl' => 'https://store.alchemy.fr',
|
||||
'linkLocation' => 'help-menu',
|
||||
'linkOrder' => 1,
|
||||
'linkBold' => false,
|
||||
'linkColor' => ''
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
@@ -47,65 +47,10 @@ class ExportSubscriber extends AbstractNotificationSubscriber
|
||||
$this->app['event-manager']->notify($params['usr_id'], 'eventsmanager_notify_downloadmailfail', $datas, $mailed);
|
||||
}
|
||||
|
||||
public function onCreateExportMail(ExportMailEvent $event)
|
||||
{
|
||||
$destMails = $event->getDestinationMails();
|
||||
|
||||
$params = $event->getParams();
|
||||
|
||||
/** @var UserRepository $userRepository */
|
||||
$userRepository = $this->app['repo.users'];
|
||||
|
||||
$user = $userRepository->find($event->getEmitterUserId());
|
||||
|
||||
/** @var TokenRepository $tokenRepository */
|
||||
$tokenRepository = $this->app['repo.tokens'];
|
||||
|
||||
/** @var Token $token */
|
||||
$token = $tokenRepository->findValidToken($event->getTokenValue());
|
||||
|
||||
$list = unserialize($token->getData());
|
||||
|
||||
//zip documents
|
||||
\set_export::build_zip(
|
||||
$this->app,
|
||||
$token,
|
||||
$list,
|
||||
$this->app['tmp.download.path'].'/'. $token->getValue() . '.zip'
|
||||
);
|
||||
|
||||
$remaingEmails = $destMails;
|
||||
|
||||
$emitter = new Emitter($user->getDisplayName(), $user->getEmail());
|
||||
|
||||
foreach ($destMails as $key => $mail) {
|
||||
try {
|
||||
$receiver = new Receiver(null, trim($mail));
|
||||
} catch (InvalidArgumentException $e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mail = MailRecordsExport::create($this->app, $receiver, $emitter, $params['textmail']);
|
||||
$mail->setButtonUrl($params['url']);
|
||||
$mail->setExpiration($token->getExpiration());
|
||||
|
||||
$this->deliver($mail, $params['reading_confirm']);
|
||||
unset($remaingEmails[$key]);
|
||||
}
|
||||
|
||||
//some mails failed
|
||||
if (count($remaingEmails) > 0) {
|
||||
foreach ($remaingEmails as $mail) {
|
||||
$this->app['dispatcher']->dispatch(PhraseaEvents::EXPORT_MAIL_FAILURE, new ExportFailureEvent($user, $params['ssttid'], $params['lst'], \eventsmanager_notify_downloadmailfail::MAIL_FAIL, $mail));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
PhraseaEvents::EXPORT_MAIL_FAILURE => 'onMailExportFailure',
|
||||
PhraseaEvents::EXPORT_MAIL_CREATE => 'onCreateExportMail',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@@ -33,7 +33,6 @@ class RecordEditSubscriber implements EventSubscriberInterface
|
||||
RecordEvents::ROTATE => 'onRecordChange',
|
||||
RecordEvents::COLLECTION_CHANGED => 'onCollectionChanged',
|
||||
RecordEvents::SUBDEFINITION_CREATE => 'onSubdefinitionCreate',
|
||||
RecordEvents::DELETE => 'onDelete',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,12 +58,6 @@ class RecordEditSubscriber implements EventSubscriberInterface
|
||||
$recordAdapter->rebuild_subdefs();
|
||||
}
|
||||
|
||||
public function onDelete(DeleteEvent $event)
|
||||
{
|
||||
$recordAdapter = $this->convertToRecordAdapter($event->getRecord());
|
||||
$recordAdapter->delete();
|
||||
}
|
||||
|
||||
public function onEdit(RecordEdit $event)
|
||||
{
|
||||
static $into = false;
|
||||
|
@@ -147,6 +147,15 @@ class RepositoriesServiceProvider implements ServiceProviderInterface
|
||||
$app['repo.webhook-delivery'] = $app->share(function (PhraseaApplication $app) {
|
||||
return $app['orm.em']->getRepository('Phraseanet:WebhookEventDelivery');
|
||||
});
|
||||
$app['repo.worker-running-job'] = $app->share(function (PhraseaApplication $app) {
|
||||
return $app['orm.em']->getRepository('Phraseanet:WorkerRunningJob');
|
||||
});
|
||||
$app['repo.worker-running-populate'] = $app->share(function (PhraseaApplication $app) {
|
||||
return $app['orm.em']->getRepository('Phraseanet:WorkerRunningPopulate');
|
||||
});
|
||||
$app['repo.worker-running-uploader'] = $app->share(function (PhraseaApplication $app) {
|
||||
return $app['orm.em']->getRepository('Phraseanet:WorkerRunningUploader');
|
||||
});
|
||||
|
||||
$app['repo.databoxes'] = $app->share(function (PhraseaApplication $app) {
|
||||
$appbox = $app->getApplicationBox();
|
||||
|
@@ -120,9 +120,9 @@ class TwigServiceProvider implements ServiceProviderInterface
|
||||
|
||||
$twig->addFilter(new \Twig_SimpleFilter('linkify', function (\Twig_Environment $twig, $string) use ($app) {
|
||||
return preg_replace(
|
||||
"(([^']{1})((https?|file):((/{2,4})|(\\{2,4}))[\w:#%/;$()~_?/\-=\\\.&]*)([^']{1}))"
|
||||
"/(\\W|^)(https?:\/{2,4}[\\w:#%\/;$()~_?\/\-=\\\.&]+)/m"
|
||||
,
|
||||
'$1 $2 <a title="' . $app['translator']->trans('Open the URL in a new window') . '" class="ui-icon ui-icon-extlink" href="$2" style="display:inline;padding:2px 5px;margin:0 4px 0 2px;" target="_blank"> </a>$7'
|
||||
'$1$2 <a title="' . $app['translator']->trans('Open the URL in a new window') . '" class=" fa fa-external-link" href="$2" style="font-size:1.2em;display:inline;padding:2px 5px;margin:0 4px 0 2px;" target="_blank"> </a>$7'
|
||||
, $string
|
||||
);
|
||||
}, ['needs_environment' => true, 'is_safe' => ['html']]));
|
||||
|
@@ -16,8 +16,8 @@ class Version
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
|
||||
private $number = '4.1.0-alpha.26a';
|
||||
|
||||
private $number = '4.1.0-alpha.29a';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
|
239
lib/Alchemy/Phrasea/Model/Entities/WorkerRunningJob.php
Normal file
@@ -0,0 +1,239 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\Model\Entities;
|
||||
|
||||
use Alchemy\Phrasea\Core\PhraseaTokens;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Gedmo\Mapping\Annotation as Gedmo;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="WorkerRunningJob",
|
||||
* indexes={
|
||||
* @ORM\index(name="databox_id", columns={"databox_id"}),
|
||||
* @ORM\index(name="record_id", columns={"record_id"}),
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="Alchemy\Phrasea\Model\Repositories\WorkerRunningJobRepository")
|
||||
*/
|
||||
class WorkerRunningJob
|
||||
{
|
||||
const FINISHED = 'finished';
|
||||
const RUNNING = 'running';
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
*/
|
||||
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="integer", name="databox_id")
|
||||
*/
|
||||
private $databoxId;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="integer", name="record_id")
|
||||
*/
|
||||
private $recordId;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="integer", name="work")
|
||||
*/
|
||||
private $work;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", name="work_on")
|
||||
*/
|
||||
private $workOn;
|
||||
|
||||
/**
|
||||
* @Gedmo\Timestampable(on="create")
|
||||
* @ORM\Column(type="datetime")
|
||||
*/
|
||||
private $created;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime")
|
||||
*/
|
||||
private $published;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime", nullable=true)
|
||||
*/
|
||||
private $finished;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", name="status")
|
||||
*/
|
||||
private $status;
|
||||
|
||||
/**
|
||||
* @return integer
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $databoxId
|
||||
* @return $this
|
||||
*/
|
||||
public function setDataboxId($databoxId)
|
||||
{
|
||||
$this->databoxId = $databoxId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDataboxId()
|
||||
{
|
||||
return $this->databoxId;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param $recordId
|
||||
* @return $this
|
||||
*/
|
||||
public function setRecordId($recordId)
|
||||
{
|
||||
$this->recordId = $recordId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRecordId()
|
||||
{
|
||||
return $this->recordId;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param $work
|
||||
* @return $this
|
||||
*/
|
||||
public function setWork($work)
|
||||
{
|
||||
$this->work = $work;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getWork()
|
||||
{
|
||||
return $this->work;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param $workOn
|
||||
* @return $this
|
||||
*/
|
||||
public function setWorkOn($workOn)
|
||||
{
|
||||
$this->workOn = $workOn;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getWorkOn()
|
||||
{
|
||||
return $this->workOn;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getCreated()
|
||||
{
|
||||
return $this->created;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $published
|
||||
* @return $this
|
||||
*/
|
||||
public function setPublished(\DateTime $published)
|
||||
{
|
||||
$this->published = $published;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getPublished()
|
||||
{
|
||||
return $this->published;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $finished
|
||||
* @return $this
|
||||
*/
|
||||
public function setFinished(\DateTime $finished)
|
||||
{
|
||||
$this->finished = $finished;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFinished()
|
||||
{
|
||||
return $this->finished;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $status
|
||||
* @return $this
|
||||
*/
|
||||
public function setStatus($status)
|
||||
{
|
||||
$this->status = $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getStatus()
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function getWorkName()
|
||||
{
|
||||
switch ($this->work) {
|
||||
case PhraseaTokens::MAKE_SUBDEF:
|
||||
return 'MAKE_SUBDEF';
|
||||
case PhraseaTokens::WRITE_META_DOC:
|
||||
return 'WRITE_META_DOC';
|
||||
case PhraseaTokens::WRITE_META_SUBDEF:
|
||||
return 'WRITE_META_SUBDEF';
|
||||
default:
|
||||
return $this->work;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
219
lib/Alchemy/Phrasea/Model/Entities/WorkerRunningPopulate.php
Normal file
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\Model\Entities;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Gedmo\Mapping\Annotation as Gedmo;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="WorkerRunningPopulate",
|
||||
* indexes={
|
||||
* @ORM\index(name="host", columns={"host"}),
|
||||
* @ORM\index(name="port", columns={"port"}),
|
||||
* @ORM\index(name="index_name", columns={"index_name"}),
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="Alchemy\Phrasea\Model\Repositories\WorkerRunningPopulateRepository")
|
||||
*/
|
||||
class WorkerRunningPopulate
|
||||
{
|
||||
const FINISHED = 'finished';
|
||||
const RUNNING = 'running';
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", name="host")
|
||||
*/
|
||||
private $host;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", name="port")
|
||||
*/
|
||||
private $port;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", name="index_name")
|
||||
*/
|
||||
private $indexName;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="integer", name="databox_id")
|
||||
*/
|
||||
private $databoxId;
|
||||
|
||||
/**
|
||||
* @Gedmo\Timestampable(on="create")
|
||||
* @ORM\Column(type="datetime")
|
||||
*/
|
||||
private $created;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime")
|
||||
*/
|
||||
private $published;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime", nullable=true)
|
||||
*/
|
||||
private $finished;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", name="status")
|
||||
*/
|
||||
private $status;
|
||||
|
||||
/**
|
||||
* @return integer
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $host
|
||||
* @return $this
|
||||
*/
|
||||
public function setHost($host)
|
||||
{
|
||||
$this->host = $host;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getHost()
|
||||
{
|
||||
return $this->host;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $port
|
||||
* @return $this
|
||||
*/
|
||||
public function setPort($port)
|
||||
{
|
||||
$this->port = $port;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getPort()
|
||||
{
|
||||
return $this->port;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $indexName
|
||||
* @return $this
|
||||
*/
|
||||
public function setIndexName($indexName)
|
||||
{
|
||||
$this->indexName = $indexName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getIndexName()
|
||||
{
|
||||
return $this->indexName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $databoxId
|
||||
* @return $this
|
||||
*/
|
||||
public function setDataboxId($databoxId)
|
||||
{
|
||||
$this->databoxId = $databoxId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDataboxId()
|
||||
{
|
||||
return $this->databoxId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getCreated()
|
||||
{
|
||||
return $this->created;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $published
|
||||
* @return $this
|
||||
*/
|
||||
public function setPublished(\DateTime $published)
|
||||
{
|
||||
$this->published = $published;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getPublished()
|
||||
{
|
||||
return $this->published;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $finished
|
||||
* @return $this
|
||||
*/
|
||||
public function setFinished(\DateTime $finished)
|
||||
{
|
||||
$this->finished = $finished;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFinished()
|
||||
{
|
||||
return $this->finished;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $status
|
||||
* @return $this
|
||||
*/
|
||||
public function setStatus($status)
|
||||
{
|
||||
$this->status = $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getStatus()
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
}
|
198
lib/Alchemy/Phrasea/Model/Entities/WorkerRunningUploader.php
Normal file
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\Model\Entities;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Gedmo\Mapping\Annotation as Gedmo;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="WorkerRunningUploader",
|
||||
* indexes={
|
||||
* @ORM\index(name="commit_id", columns={"commit_id"}),
|
||||
* @ORM\index(name="asset_id", columns={"asset_id"}),
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="Alchemy\Phrasea\Model\Repositories\WorkerRunningUploaderRepository")
|
||||
*/
|
||||
class WorkerRunningUploader
|
||||
{
|
||||
const DOWNLOADED = 'downloaded';
|
||||
const RUNNING = 'running';
|
||||
|
||||
const TYPE_PULL = 'pull';
|
||||
const TYPE_PUSH = 'push';
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", name="commit_id")
|
||||
*/
|
||||
private $commitId;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", name="asset_id")
|
||||
*/
|
||||
private $assetId;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", name="type")
|
||||
*/
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* @Gedmo\Timestampable(on="create")
|
||||
* @ORM\Column(type="datetime")
|
||||
*/
|
||||
private $created;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime")
|
||||
*/
|
||||
private $published;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime", nullable=true)
|
||||
*/
|
||||
private $finished;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", name="status")
|
||||
*/
|
||||
private $status;
|
||||
|
||||
|
||||
/**
|
||||
* @return integer
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $commitId
|
||||
* @return $this
|
||||
*/
|
||||
public function setCommitId($commitId)
|
||||
{
|
||||
$this->commitId = $commitId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCommitId()
|
||||
{
|
||||
return $this->commitId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $assetId
|
||||
* @return $this
|
||||
*/
|
||||
public function setAssetId($assetId)
|
||||
{
|
||||
$this->assetId = $assetId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAssetId()
|
||||
{
|
||||
return $this->assetId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getCreated()
|
||||
{
|
||||
return $this->created;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $published
|
||||
* @return $this
|
||||
*/
|
||||
public function setPublished(\DateTime $published)
|
||||
{
|
||||
$this->published = $published;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getPublished()
|
||||
{
|
||||
return $this->published;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $finished
|
||||
* @return $this
|
||||
*/
|
||||
public function setFinished(\DateTime $finished)
|
||||
{
|
||||
$this->finished = $finished;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFinished()
|
||||
{
|
||||
return $this->finished;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $status
|
||||
* @return $this
|
||||
*/
|
||||
public function setStatus($status)
|
||||
{
|
||||
$this->status = $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getStatus()
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $type
|
||||
* @return $this
|
||||
*/
|
||||
public function setType($type)
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
}
|
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\Model\Repositories;
|
||||
|
||||
use Alchemy\Phrasea\Core\PhraseaTokens;
|
||||
use Alchemy\Phrasea\Model\Entities\WorkerRunningJob;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
|
||||
class WorkerRunningJobRepository extends EntityRepository
|
||||
{
|
||||
/**
|
||||
* return true if we can create subdef
|
||||
* @param $subdefName
|
||||
* @param $recordId
|
||||
* @param $databoxId
|
||||
* @return bool
|
||||
*/
|
||||
public function canCreateSubdef($subdefName, $recordId, $databoxId)
|
||||
{
|
||||
$rsm = $this->createResultSetMappingBuilder('w');
|
||||
$rsm->addScalarResult('work_on','work_on');
|
||||
|
||||
$sql = 'SELECT work_on
|
||||
FROM WorkerRunningJob
|
||||
WHERE ((work & :write_meta) > 0 OR ((work & :make_subdef) > 0 AND work_on = :work_on) )
|
||||
AND record_id = :record_id
|
||||
AND databox_id = :databox_id
|
||||
AND status = :status';
|
||||
|
||||
$query = $this->_em->createNativeQuery($sql, $rsm);
|
||||
$query->setParameters([
|
||||
'write_meta' => PhraseaTokens::WRITE_META,
|
||||
'make_subdef'=> PhraseaTokens::MAKE_SUBDEF,
|
||||
'work_on' => $subdefName,
|
||||
'record_id' => $recordId,
|
||||
'databox_id' => $databoxId,
|
||||
'status' => WorkerRunningJob::RUNNING
|
||||
]
|
||||
);
|
||||
|
||||
return count($query->getResult()) == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* return true if we can write meta
|
||||
*
|
||||
* @param $subdefName
|
||||
* @param $recordId
|
||||
* @param $databoxId
|
||||
* @return bool
|
||||
*/
|
||||
public function canWriteMetadata($subdefName, $recordId, $databoxId)
|
||||
{
|
||||
$rsm = $this->createResultSetMappingBuilder('w');
|
||||
$rsm->addScalarResult('work_on','work_on');
|
||||
|
||||
$sql = 'SELECT work_on
|
||||
FROM WorkerRunningJob
|
||||
WHERE ((work & :make_subdef) > 0 OR ((work & :write_meta) > 0 AND work_on = :work_on) )
|
||||
AND record_id = :record_id
|
||||
AND databox_id = :databox_id
|
||||
AND status = :status';
|
||||
|
||||
$query = $this->_em->createNativeQuery($sql, $rsm);
|
||||
$query->setParameters([
|
||||
'make_subdef'=> PhraseaTokens::MAKE_SUBDEF,
|
||||
'write_meta' => PhraseaTokens::WRITE_META,
|
||||
'work_on' => $subdefName,
|
||||
'record_id' => $recordId,
|
||||
'databox_id' => $databoxId,
|
||||
'status' => WorkerRunningJob::RUNNING
|
||||
]
|
||||
);
|
||||
|
||||
return count($query->getResult()) == 0;
|
||||
}
|
||||
|
||||
public function truncateWorkerTable()
|
||||
{
|
||||
$connection = $this->_em->getConnection();
|
||||
$platform = $connection->getDatabasePlatform();
|
||||
$this->_em->beginTransaction();
|
||||
try {
|
||||
$connection->executeUpdate($platform->getTruncateTableSQL('WorkerRunningJob'));
|
||||
} catch (\Exception $e) {
|
||||
$this->_em->rollback();
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteFinishedWorks()
|
||||
{
|
||||
$this->_em->beginTransaction();
|
||||
try {
|
||||
$this->_em->getConnection()->delete('WorkerRunningJob', ['status' => WorkerRunningJob::FINISHED]);
|
||||
$this->_em->commit();
|
||||
} catch (\Exception $e) {
|
||||
$this->_em->rollback();
|
||||
}
|
||||
}
|
||||
|
||||
public function getEntityManager()
|
||||
{
|
||||
return parent::getEntityManager();
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\Model\Repositories;
|
||||
|
||||
use Alchemy\Phrasea\Model\Entities\WorkerRunningPopulate;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
|
||||
class WorkerRunningPopulateRepository extends EntityRepository
|
||||
{
|
||||
public function getEntityManager()
|
||||
{
|
||||
return parent::getEntityManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $databoxIds
|
||||
* @return int
|
||||
*/
|
||||
public function checkPopulateStatusByDataboxIds(array $databoxIds)
|
||||
{
|
||||
$qb = $this->createQueryBuilder('w');
|
||||
$qb->where($qb->expr()->in('w.databoxId', $databoxIds))
|
||||
->andWhere('w.status = :status')
|
||||
->setParameter('status', WorkerRunningPopulate::RUNNING)
|
||||
;
|
||||
|
||||
return count($qb->getQuery()->getResult());
|
||||
}
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\Model\Repositories;
|
||||
|
||||
use Alchemy\Phrasea\Model\Entities\WorkerRunningUploader;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
|
||||
class WorkerRunningUploaderRepository extends EntityRepository
|
||||
{
|
||||
public function getEntityManager()
|
||||
{
|
||||
return parent::getEntityManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $commitId
|
||||
* @return bool
|
||||
*/
|
||||
public function canAck($commitId)
|
||||
{
|
||||
$qb = $this->createQueryBuilder('w');
|
||||
$res = $qb
|
||||
->where('w.commitId = :commitId')
|
||||
->andWhere('w.status != :status')
|
||||
->setParameters([
|
||||
'commitId' => $commitId,
|
||||
'status' => WorkerRunningUploader::DOWNLOADED
|
||||
])
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
|
||||
return count($res) == 0;
|
||||
}
|
||||
}
|
@@ -198,7 +198,7 @@ class Installer
|
||||
$config['main']['database']['driver'] = 'pdo_mysql';
|
||||
$config['main']['database']['charset'] = 'UTF8';
|
||||
|
||||
$config['main']['binaries'] = $binaryData;
|
||||
$config['main']['binaries'] = array_merge($config['main']['binaries'], $binaryData);
|
||||
|
||||
$config['servername'] = $serverName;
|
||||
$config['main']['key'] = $this->app['random.medium']->generateString(16);
|
||||
|
@@ -181,11 +181,12 @@ class ArchiveJob extends AbstractJob
|
||||
|
||||
$dom = new \DOMDocument();
|
||||
$dom->formatOutput = true;
|
||||
/** @var \DOMElement $root */
|
||||
$root = $dom->appendChild($dom->createElement('root'));
|
||||
|
||||
$nnew = $this->listFilesPhase1($app, $dom, $root, $path_in, $server_coll_id, 0, $TColls);
|
||||
if ($app['debug']) {
|
||||
$this->log('debug', "=========== listFilesPhase1 ========== (returned " . $nnew . ")\n" . $dom->saveXML());
|
||||
$this->log('debug', "== listFilesPhase1 returned " . $nnew . ")\n" . $dom->saveXML());
|
||||
}
|
||||
|
||||
if (!$this->isStarted()) {
|
||||
@@ -193,15 +194,16 @@ class ArchiveJob extends AbstractJob
|
||||
}
|
||||
|
||||
// wait for files to be cold
|
||||
$this->pause($cold);
|
||||
|
||||
if (!$this->isStarted()) {
|
||||
return;
|
||||
for($i=0; $i<($cold*2); $i++) {
|
||||
if (!$this->isStarted()) {
|
||||
return;
|
||||
}
|
||||
$this->pause(0.5);
|
||||
}
|
||||
|
||||
$this->listFilesPhase2($app, $dom, $root, $path_in, 0);
|
||||
if ($app['debug']) {
|
||||
$this->log('debug', "=========== listFilesPhase2 ========== : \n" . $dom->saveXML());
|
||||
$this->log('debug', "== listFilesPhase2\n" . $dom->saveXML());
|
||||
}
|
||||
|
||||
if (!$this->isStarted()) {
|
||||
@@ -210,31 +212,35 @@ class ArchiveJob extends AbstractJob
|
||||
|
||||
$this->makePairs($dom, $root, $path_in, $path_archived, $path_error, false, 0, $tmask, $tmaskgrp);
|
||||
if ($app['debug']) {
|
||||
$this->log('debug', "=========== makePairs ========== : \n" . $dom->saveXML());
|
||||
}
|
||||
|
||||
$r = $this->removeBadGroups($app, $dom, $root, $path_in, $path_archived, $path_error, 0, $moveError);
|
||||
if ($app['debug']) {
|
||||
$this->log('debug', "=========== removeBadGroups ========== (returned " . ((Boolean) $r ? 'true' : 'false') . ") : \n" . $dom->saveXML());
|
||||
}
|
||||
|
||||
$this->archive($app, $databox, $dom, $root, $path_in, $path_archived, $path_error, 0, $moveError, $moveArchived, $stat0, $stat1);
|
||||
if ($app['debug']) {
|
||||
$this->log('debug', "=========== archive ========== : \n" . $dom->saveXML());
|
||||
$this->log('debug', "== makePairs\n" . $dom->saveXML());
|
||||
}
|
||||
|
||||
if (!$this->isStarted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->removeBadGroups($app, $dom, $root, $path_in, $path_archived, $path_error, 0, $moveError);
|
||||
if ($app['debug']) {
|
||||
$this->log('debug', "== removeBadGroups\n" . $dom->saveXML());
|
||||
}
|
||||
|
||||
if (!$this->isStarted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->archive($app, $databox, $dom, $root, $path_in, $path_archived, $path_error, 0, $moveError, $moveArchived, $stat0, $stat1);
|
||||
if ($app['debug']) {
|
||||
$this->log('debug', "== archive\n" . $dom->saveXML());
|
||||
}
|
||||
|
||||
$this->bubbleResults($dom, $root, $path_in, 0, \p4field::isyes($settings->copy_spe));
|
||||
if ($app['debug']) {
|
||||
$this->log('debug', "=========== bubbleResults ========== : \n" . $dom->saveXML());
|
||||
$this->log('debug', "== bubbleResults\n" . $dom->saveXML());
|
||||
}
|
||||
|
||||
$moved = $this->moveFiles($app, $dom, $root, $path_in, $path_archived, $path_error, 0, $moveArchived, $moveError);
|
||||
if ($app['debug']) {
|
||||
$this->log('debug', "=========== moveFiles ========== (returned " . ($moved ? 'true' : 'false') . ") : \n" . $dom->saveXML());
|
||||
$this->log('debug', "== moveFiles returned " . ($moved ? 'true' : 'false') . "\n" . $dom->saveXML());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,15 +249,17 @@ class ArchiveJob extends AbstractJob
|
||||
{
|
||||
$nnew = 0;
|
||||
|
||||
if (false !== $sxDotPhrasea = @simplexml_load_file($path . '/.phrasea.xml')) {
|
||||
$magicfile = $magicmethod = null;
|
||||
|
||||
if (($sxDotPhrasea = @simplexml_load_file($path . '/.phrasea.xml')) !== false) {
|
||||
|
||||
// test for magic file
|
||||
if (($magicfile = trim((string) ($sxDotPhrasea->magicfile))) != '') {
|
||||
$magicmethod = strtoupper($sxDotPhrasea->magicfile['method']);
|
||||
if ($magicmethod == 'LOCK' && true === $app['filesystem']->exists($path . '/' . $magicfile)) {
|
||||
return;
|
||||
} elseif ($magicmethod == 'UNLOCK' && false === $app['filesystem']->exists($path . '/' . $magicfile)) {
|
||||
return;
|
||||
if ($magicmethod == 'LOCK' && ($app['filesystem']->exists($path . '/' . $magicfile) === true)) {
|
||||
return 0;
|
||||
} elseif ($magicmethod == 'UNLOCK' && ($app['filesystem']->exists($path . '/' . $magicfile) === false)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,6 +286,7 @@ class ArchiveJob extends AbstractJob
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var \DOMElement $n */
|
||||
if (is_dir($path . '/' . $file)) {
|
||||
$n = $node->appendChild($dom->createElement('file'));
|
||||
$n->setAttribute('isdir', '1');
|
||||
@@ -293,6 +302,16 @@ class ArchiveJob extends AbstractJob
|
||||
foreach (["size", "ctime", "mtime"] as $k) {
|
||||
$n->setAttribute($k, $stat[$k]);
|
||||
}
|
||||
// special file
|
||||
if($file == '.phrasea.xml') {
|
||||
$n->setAttribute('match', '*');
|
||||
}
|
||||
// special file
|
||||
if($file === $magicfile) {
|
||||
$n->setAttribute('match', '*');
|
||||
$node->setAttribute('magicfile', $magicfile);
|
||||
$node->setAttribute('magicmethod', $magicmethod);
|
||||
}
|
||||
$nnew++;
|
||||
}
|
||||
$n->setAttribute('cid', $server_coll_id);
|
||||
@@ -335,27 +354,31 @@ class ArchiveJob extends AbstractJob
|
||||
$dnl = @$xp->query('./file[@name="' . $file . '"]', $node);
|
||||
if ($dnl && $dnl->length == 0) {
|
||||
if (is_dir($path . '/' . $file)) {
|
||||
/** @var \DOMElement $n */
|
||||
$n = $node->appendChild($dom->createElement('file'));
|
||||
$n->setAttribute('isdir', '1');
|
||||
$n->setAttribute('name', $file);
|
||||
|
||||
$nnew += $this->listFilesPhase2($app, $dom, $n, $path . '/' . $file, $depth + 1);
|
||||
} else {
|
||||
/** @var \DOMElement $n */
|
||||
$n = $node->appendChild($dom->createElement('file'));
|
||||
$n->setAttribute('name', $file);
|
||||
$nnew++;
|
||||
}
|
||||
$this->setBranchHot($dom, $n);
|
||||
$this->setBranchHot($n);
|
||||
} elseif ($dnl && $dnl->length == 1) {
|
||||
$dnl->item(0)->setAttribute('temperature', 'cold');
|
||||
/** @var \DOMElement $n */
|
||||
$n = $dnl->item(0);
|
||||
$n->setAttribute('temperature', 'cold');
|
||||
|
||||
if (is_dir($path . '/' . $file)) {
|
||||
$this->listFilesPhase2($app, $dom, $dnl->item(0), $path . '/' . $file, $depth + 1);
|
||||
$this->listFilesPhase2($app, $dom, $n, $path . '/' . $file, $depth + 1);
|
||||
} else {
|
||||
$stat = stat($path . '/' . $file);
|
||||
foreach (["size", "ctime", "mtime"] as $k) {
|
||||
if ($dnl->item(0)->getAttribute($k) != $stat[$k]) {
|
||||
$this->setBranchHot($dom, $dnl->item(0));
|
||||
if ($n->getAttribute($k) != $stat[$k]) {
|
||||
$this->setBranchHot($n);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -399,13 +422,16 @@ class ArchiveJob extends AbstractJob
|
||||
if ($dnl->length == 1) {
|
||||
// this group is old (don't care about any linked files), just flag it
|
||||
$n->setAttribute('grp', 'tocomplete');
|
||||
$dnl->item(0)->setAttribute('match', '*');
|
||||
/** @var \DOMElement $_n */
|
||||
$_n = $dnl->item(0);
|
||||
$_n->setAttribute('match', '*');
|
||||
// recurse only if group is ok
|
||||
$this->makePairs($dom, $n, $path . '/' . $name, $path_archived, $path_error, true, $depth + 1, $tmask, $tmaskgrp);
|
||||
} else {
|
||||
// this group in new (to be created)
|
||||
// do we need one (or both) linked file ? (caption or representation)
|
||||
$err = false;
|
||||
/** @var \DOMElement[] $flink */
|
||||
$flink = ['caption' => null, 'representation' => null];
|
||||
|
||||
foreach ($flink as $linkName => $v) {
|
||||
@@ -473,12 +499,12 @@ class ArchiveJob extends AbstractJob
|
||||
// this is a file
|
||||
if (!$n->getAttribute('match')) {
|
||||
// because match can be set before
|
||||
if ($name == '.phrasea.xml') {
|
||||
// special file(s) always ok
|
||||
$n->setAttribute('match', '*');
|
||||
} else {
|
||||
// if ($name == '.phrasea.xml') {
|
||||
// // special file(s) always ok
|
||||
// $n->setAttribute('match', '*');
|
||||
// } else {
|
||||
$this->checkMatch($dom, $n, $tmask);
|
||||
}
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -503,7 +529,7 @@ class ArchiveJob extends AbstractJob
|
||||
|
||||
// if root of hotfolder if hot, die...
|
||||
if ($depth == 0 && $node->getAttribute('temperature') == 'hot') {
|
||||
return $ret;
|
||||
return;
|
||||
}
|
||||
|
||||
$nodesToDel = [];
|
||||
@@ -522,7 +548,7 @@ class ArchiveJob extends AbstractJob
|
||||
$name = $n->getAttribute('name');
|
||||
|
||||
if ($n->getAttribute('isdir')) {
|
||||
$ret |= $this->removeBadGroups($app, $dom, $n, $path . '/' . $name
|
||||
$this->removeBadGroups($app, $dom, $n, $path . '/' . $name
|
||||
, $path_archived . '/' . $name
|
||||
, $path_error . '/' . $name
|
||||
, $depth + 1, $moveError);
|
||||
@@ -642,7 +668,7 @@ class ArchiveJob extends AbstractJob
|
||||
}
|
||||
|
||||
if ($node->getAttribute('temperature') == 'hot') {
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
|
||||
$ret = 0;
|
||||
@@ -758,7 +784,7 @@ class ArchiveJob extends AbstractJob
|
||||
}
|
||||
}
|
||||
|
||||
if (!$n->getAttribute('keep')) {
|
||||
if (!$n->getAttribute('keep') && !$n->getAttribute('match')) {
|
||||
$this->log('debug', sprintf('delete \'%s\'', $path . '/' . $name));
|
||||
|
||||
try {
|
||||
@@ -792,7 +818,9 @@ class ArchiveJob extends AbstractJob
|
||||
if ($dnl->length == 1) {
|
||||
// the caption file exists
|
||||
$node->setAttribute('match', $captionFileName);
|
||||
$dnl->item(0)->setAttribute('match', '*');
|
||||
/** @var \DOMElement $n */
|
||||
$n = $dnl->item(0);
|
||||
$n->setAttribute('match', '*');
|
||||
} else {
|
||||
// the caption file is missing
|
||||
$node->setAttribute('match', '?');
|
||||
@@ -817,7 +845,7 @@ class ArchiveJob extends AbstractJob
|
||||
return ($f[0] == '.' && $f != '.phrasea.xml' && $f != '.grouping.xml') || $f == 'thumbs.db' || $f == 'par-system';
|
||||
}
|
||||
|
||||
private function setBranchHot(\DOMDocument $dom, \DOMElement $node)
|
||||
private function setBranchHot(\DOMElement $node)
|
||||
{
|
||||
for ($n = $node; $n; $n = $n->parentNode) {
|
||||
if ($n->nodeType == XML_ELEMENT_NODE) {
|
||||
@@ -847,8 +875,10 @@ class ArchiveJob extends AbstractJob
|
||||
|
||||
if ($node->getAttribute('grp') == 'tocreate') {
|
||||
$representationFileName = null;
|
||||
/** @var \DOMElement $representationFileNode */
|
||||
$representationFileNode = null;
|
||||
$captionFileName = null;
|
||||
/** @var \DOMElement $captionFileNode */
|
||||
$captionFileNode = null;
|
||||
$cid = $node->getAttribute('cid');
|
||||
$genericdoc = null;
|
||||
@@ -903,6 +933,7 @@ class ArchiveJob extends AbstractJob
|
||||
}
|
||||
|
||||
file_put_contents($groupingFile, '<?xml version="1.0" encoding="ISO-8859-1" ?><record grouping="' . $rid . '" />');
|
||||
/** @var \DOMElement $n */
|
||||
$n = $node->appendChild($dom->createElement('file'));
|
||||
$n->setAttribute('name', '.grouping.xml');
|
||||
$n->setAttribute('temperature', 'cold');
|
||||
@@ -1025,6 +1056,8 @@ class ArchiveJob extends AbstractJob
|
||||
}
|
||||
|
||||
$story = \record_adapter::createStory($app, $collection);
|
||||
|
||||
$story->setStatus($status);
|
||||
$app['subdef.substituer']->substituteDocument($story, $media);
|
||||
|
||||
$story->set_metadatas($metadatas->toMetadataArray($metadatasStructure), true);
|
||||
@@ -1080,12 +1113,14 @@ class ArchiveJob extends AbstractJob
|
||||
|
||||
$file->addAttribute(new BorderAttribute\Status($app, $status));
|
||||
|
||||
$file->addAttribute(new BorderAttribute\Metadata(new Metadata(new PhraseaTag\TfFilepath(), new MonoValue($media->getFile()->getRealPath()))));
|
||||
$file->addAttribute(new BorderAttribute\Metadata(new Metadata(new PhraseaTag\TfDirname(), new MonoValue(dirname($media->getFile()->getRealPath())))));
|
||||
/** @var \MediaVorus\File $mediaFile */
|
||||
$mediaFile = $media->getFile();
|
||||
$file->addAttribute(new BorderAttribute\Metadata(new Metadata(new PhraseaTag\TfFilepath(), new MonoValue($mediaFile->getRealPath()))));
|
||||
$file->addAttribute(new BorderAttribute\Metadata(new Metadata(new PhraseaTag\TfDirname(), new MonoValue(dirname($mediaFile->getRealPath())))));
|
||||
|
||||
$file->addAttribute(new BorderAttribute\Metadata(new Metadata(new PhraseaTag\TfAtime(), new MonoValue($media->getFile()->getATime()))));
|
||||
$file->addAttribute(new BorderAttribute\Metadata(new Metadata(new PhraseaTag\TfMtime(), new MonoValue($media->getFile()->getMTime()))));
|
||||
$file->addAttribute(new BorderAttribute\Metadata(new Metadata(new PhraseaTag\TfCtime(), new MonoValue($media->getFile()->getCTime()))));
|
||||
$file->addAttribute(new BorderAttribute\Metadata(new Metadata(new PhraseaTag\TfAtime(), new MonoValue($mediaFile->getATime()))));
|
||||
$file->addAttribute(new BorderAttribute\Metadata(new Metadata(new PhraseaTag\TfMtime(), new MonoValue($mediaFile->getMTime()))));
|
||||
$file->addAttribute(new BorderAttribute\Metadata(new Metadata(new PhraseaTag\TfCtime(), new MonoValue($mediaFile->getCTime()))));
|
||||
|
||||
foreach ($metadatas as $meta) {
|
||||
$file->addAttribute(new BorderAttribute\Metadata($meta));
|
||||
@@ -1102,8 +1137,13 @@ class ArchiveJob extends AbstractJob
|
||||
$record = null;
|
||||
|
||||
$postProcess = function ($element, $visa, $code) use (&$record) {
|
||||
$record = $element;
|
||||
};
|
||||
$r = isset($visa); // one way to avoid "variable not used" with phpstorm 10. ugly.
|
||||
unset($r); //
|
||||
$r = isset($code); // one way to avoid "variable not used" with phpstorm 10. ugly.
|
||||
unset($r); //
|
||||
|
||||
$record = $element;
|
||||
};
|
||||
|
||||
/** @var borderManager $borderManager */
|
||||
$borderManager = $app['border-manager'];
|
||||
@@ -1206,12 +1246,13 @@ class ArchiveJob extends AbstractJob
|
||||
}
|
||||
}
|
||||
|
||||
$this->archiveFileAndCaption($app, $databox, $dom, $node, $captionFileNode, $path, $path_archived, $path_error, $grp_rid, $nodesToDel, $stat0, $stat1, $moveError, $moveArchived);
|
||||
$this->archiveFileAndCaption($app, $databox, $node, $captionFileNode, $path, $path_archived, $path_error, $grp_rid, $nodesToDel, $stat0, $stat1, $moveError, $moveArchived);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param \DOMDOcument $dom
|
||||
* @param Application $app
|
||||
* @param \databox $databox
|
||||
* @param \DOMElement $node
|
||||
* @param \DOMElement $captionFileNode
|
||||
* @param string $path
|
||||
@@ -1219,8 +1260,12 @@ class ArchiveJob extends AbstractJob
|
||||
* @param string $path_error
|
||||
* @param integer $grp_rid
|
||||
* @param array $nodesToDel out, filled with files to delete
|
||||
* @param $stat0
|
||||
* @param $stat1
|
||||
* @param $moveError
|
||||
* @param $moveArchived
|
||||
*/
|
||||
private function archiveFileAndCaption(Application $app, \databox $databox, \DOMDocument $dom, \DOMElement $node, \DOMElement $captionFileNode = null, $path, $path_archived, $path_error, $grp_rid, array &$nodesToDel, $stat0, $stat1, $moveError, $moveArchived)
|
||||
private function archiveFileAndCaption(Application $app, \databox $databox, \DOMElement $node, \DOMElement $captionFileNode = null, $path, $path_archived, $path_error, $grp_rid, array &$nodesToDel, $stat0, $stat1, $moveError, $moveArchived)
|
||||
{
|
||||
// quick fix to reconnect if mysql is lost
|
||||
$app->getApplicationBox()->get_connection();
|
||||
@@ -1422,6 +1467,7 @@ class ArchiveJob extends AbstractJob
|
||||
{
|
||||
$ret = new MetadataBag();
|
||||
|
||||
/** @var \databox_field $databox_field */
|
||||
foreach ($metadatasStructure as $databox_field) {
|
||||
if ($bag->containsKey($databox_field->get_tag()->getTagname())) {
|
||||
$ret->set($databox_field->get_name(), $bag->get($databox_field->get_tag()->getTagname()));
|
||||
|
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Command;
|
||||
|
||||
use Alchemy\Phrasea\Command\Command;
|
||||
use Alchemy\Phrasea\WorkerManager\Queue\AMQPConnection;
|
||||
use Alchemy\Phrasea\WorkerManager\Queue\MessageHandler;
|
||||
use Alchemy\Phrasea\WorkerManager\Worker\WorkerInvoker;
|
||||
use PhpAmqpLib\Channel\AMQPChannel;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class WorkerExecuteCommand extends Command
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('worker:execute');
|
||||
|
||||
$this->setDescription('Listen queues define on configuration, launch corresponding service for execution')
|
||||
->addOption('preserve-payload', 'p', InputOption::VALUE_NONE, 'Preserve temporary payload file')
|
||||
->addOption('queue-name', '', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'The name of queues to be consuming')
|
||||
->addOption('max-processes', 'm', InputOption::VALUE_REQUIRED, 'The max number of process allow to run (default 4) ')
|
||||
->addOption('MWG', '', InputOption::VALUE_NONE, 'Enable MWG metadata compatibility (use only for write metadata service)')
|
||||
->addOption('clear-metadatas', '', InputOption::VALUE_NONE, 'Delete metadatas from documents if not compliant with Database structure (use only for write metadata service)')
|
||||
->setHelp('');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function doExecute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$MWG = false;
|
||||
$clearMetadatas = false;
|
||||
|
||||
$argQueueName = $input->getOption('queue-name');
|
||||
$maxProcesses = intval($input->getOption('max-processes'));
|
||||
|
||||
/** @var AMQPConnection $serverConnection */
|
||||
$serverConnection = $this->container['alchemy_worker.amqp.connection'];
|
||||
|
||||
/** @var AMQPChannel $channel */
|
||||
$channel = $serverConnection->getChannel();
|
||||
|
||||
if ($channel == null) {
|
||||
$output->writeln("Can't connect to rabbit, check configuration!");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$serverConnection->declareExchange();
|
||||
|
||||
/** @var WorkerInvoker $workerInvoker */
|
||||
$workerInvoker = $this->container['alchemy_worker.worker_invoker'];
|
||||
|
||||
if ($input->getOption('max-processes') != null && $maxProcesses == 0) {
|
||||
$output->writeln('<error>Invalid max-processes option.Need an integer</error>');
|
||||
|
||||
return;
|
||||
} elseif($maxProcesses) {
|
||||
$workerInvoker->setMaxProcessPoolValue($maxProcesses);
|
||||
}
|
||||
|
||||
if ($input->getOption('MWG')) {
|
||||
$MWG = true;
|
||||
}
|
||||
|
||||
if ($input->getOption('clear-metadatas')) {
|
||||
$clearMetadatas = true;
|
||||
}
|
||||
|
||||
if ($input->getOption('preserve-payload')) {
|
||||
$workerInvoker->preservePayloads();
|
||||
}
|
||||
|
||||
/** @var MessageHandler $messageHandler */
|
||||
$messageHandler = $this->container['alchemy_worker.message.handler'];
|
||||
$messageHandler->consume($serverConnection, $workerInvoker, $argQueueName, $maxProcesses);
|
||||
|
||||
while (count($channel->callbacks)) {
|
||||
$output->writeln("[*] Waiting for messages. To exit press CTRL+C");
|
||||
$channel->wait();
|
||||
}
|
||||
|
||||
$serverConnection->connectionClose();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Command;
|
||||
|
||||
use Alchemy\Phrasea\Command\Command;
|
||||
use Alchemy\Phrasea\WorkerManager\Worker\Resolver\WorkerResolverInterface;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class WorkerRunServiceCommand extends Command
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('worker:run-service');
|
||||
|
||||
$this->setDescription('Execute a service')
|
||||
->addArgument('type')
|
||||
->addArgument('body')
|
||||
->addOption('preserve-payload', 'p', InputOption::VALUE_NONE, 'Preserve temporary payload file');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function doExecute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
/** @var WorkerResolverInterface $workerResolver */
|
||||
$workerResolver = $this->container['alchemy_worker.type_based_worker_resolver'];
|
||||
|
||||
$type = $input->getArgument('type');
|
||||
$body = file_get_contents($input->getArgument('body'));
|
||||
|
||||
if ($body === false) {
|
||||
$output->writeln('Unable to read payload file');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$body = json_decode($body, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$output->writeln('<error>Invalid message body</error>');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$worker = $workerResolver->getWorker($type, $body);
|
||||
|
||||
$worker->process($body);
|
||||
|
||||
if (! $input->getOption('preserve-payload')) {
|
||||
unlink($input->getArgument('body'));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Command;
|
||||
|
||||
use Alchemy\Phrasea\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class WorkerShowConfigCommand extends Command
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('worker:show-configuration');
|
||||
|
||||
$this->setDescription('Show queues configuration');
|
||||
}
|
||||
|
||||
public function doExecute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$serverConfiguration = $this->container['conf']->get(['workers', 'queue', 'worker-queue']);
|
||||
|
||||
$output->writeln(['', 'Configured server: ']);
|
||||
|
||||
$output->writeln(['Rabbit Server : ' . Yaml::dump($serverConfiguration, 0), '']);
|
||||
}
|
||||
}
|
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Controller;
|
||||
|
||||
use Alchemy\Phrasea\Application as PhraseaApplication;
|
||||
use Alchemy\Phrasea\Controller\Controller;
|
||||
use Alchemy\Phrasea\Model\Entities\WorkerRunningJob;
|
||||
use Alchemy\Phrasea\Model\Repositories\WorkerRunningJobRepository;
|
||||
use Alchemy\Phrasea\Model\Repositories\WorkerRunningPopulateRepository;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\ElasticsearchOptions;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\PopulateIndexEvent;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\WorkerEvents;
|
||||
use Alchemy\Phrasea\WorkerManager\Form\WorkerConfigurationType;
|
||||
use Alchemy\Phrasea\WorkerManager\Form\WorkerPullAssetsType;
|
||||
use Alchemy\Phrasea\WorkerManager\Form\WorkerSearchengineType;
|
||||
use Alchemy\Phrasea\WorkerManager\Queue\AMQPConnection;
|
||||
use Alchemy\Phrasea\WorkerManager\Queue\MessagePublisher;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class AdminConfigurationController extends Controller
|
||||
{
|
||||
public function indexAction(PhraseaApplication $app, Request $request)
|
||||
{
|
||||
/** @var AMQPConnection $serverConnection */
|
||||
$serverConnection = $this->app['alchemy_worker.amqp.connection'];
|
||||
|
||||
/** @var WorkerRunningJobRepository $repoWorker */
|
||||
$repoWorker = $app['repo.worker-running-job'];
|
||||
|
||||
return $this->render('admin/worker-manager/index.html.twig', [
|
||||
'isConnected' => ($serverConnection->getChannel() != null) ? true : false,
|
||||
'workerRunningJob' => $repoWorker->findAll(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PhraseaApplication $app
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function configurationAction(PhraseaApplication $app, Request $request)
|
||||
{
|
||||
$retryQueueConfig = $this->getRetryQueueConfiguration();
|
||||
|
||||
$form = $app->form(new WorkerConfigurationType(), $retryQueueConfig);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isValid()) {
|
||||
// save config in file
|
||||
$app['conf']->set(['workers', 'retry_queue'], $form->getData());
|
||||
|
||||
$queues = array_intersect_key(AMQPConnection::$defaultQueues, $retryQueueConfig);
|
||||
$retryQueuesToReset = array_intersect_key(AMQPConnection::$defaultRetryQueues, array_flip($queues));
|
||||
|
||||
/** @var AMQPConnection $serverConnection */
|
||||
$serverConnection = $this->app['alchemy_worker.amqp.connection'];
|
||||
// change the queue TTL
|
||||
$serverConnection->reinitializeQueue($retryQueuesToReset);
|
||||
$serverConnection->reinitializeQueue(AMQPConnection::$defaultDelayedQueues);
|
||||
|
||||
return $app->redirectPath('worker_admin');
|
||||
}
|
||||
|
||||
return $this->render('admin/worker-manager/worker_configuration.html.twig', [
|
||||
'form' => $form->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
public function infoAction(PhraseaApplication $app, Request $request)
|
||||
{
|
||||
/** @var WorkerRunningJobRepository $repoWorker */
|
||||
$repoWorker = $app['repo.worker-running-job'];
|
||||
|
||||
$workerRunningJob = [];
|
||||
|
||||
$reload = ($request->query->get('reload')) == 1 ? true : false ;
|
||||
|
||||
if ($request->query->get('running') == 1 && $request->query->get('finished') == 1) {
|
||||
$workerRunningJob = $repoWorker->findAll();
|
||||
} elseif ($request->query->get('running') == 1) {
|
||||
$workerRunningJob = $repoWorker->findBy(['status' => WorkerRunningJob::RUNNING]);
|
||||
} elseif ($request->query->get('finished') == 1) {
|
||||
$workerRunningJob = $repoWorker->findBy(['status' => WorkerRunningJob::FINISHED]);
|
||||
}
|
||||
|
||||
return $this->render('admin/worker-manager/worker_info.html.twig', [
|
||||
'workerRunningJob' => $workerRunningJob,
|
||||
'reload' => $reload
|
||||
]);
|
||||
}
|
||||
|
||||
public function truncateTableAction(PhraseaApplication $app, Request $request)
|
||||
{
|
||||
/** @var WorkerRunningJobRepository $repoWorker */
|
||||
$repoWorker = $app['repo.worker-running-job'];
|
||||
$repoWorker->truncateWorkerTable();
|
||||
|
||||
return $app->redirectPath('worker_admin');
|
||||
}
|
||||
|
||||
public function deleteFinishedAction(PhraseaApplication $app, Request $request)
|
||||
{
|
||||
/** @var WorkerRunningJobRepository $repoWorker */
|
||||
$repoWorker = $app['repo.worker-running-job'];
|
||||
$repoWorker->deleteFinishedWorks();
|
||||
|
||||
return $app->redirectPath('worker_admin');
|
||||
}
|
||||
|
||||
public function searchengineAction(PhraseaApplication $app, Request $request)
|
||||
{
|
||||
$options = $this->getElasticsearchOptions();
|
||||
|
||||
$form = $app->form(new WorkerSearchengineType(), $options);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isValid()) {
|
||||
$populateInfo = $this->getData($form);
|
||||
|
||||
$this->getDispatcher()->dispatch(WorkerEvents::POPULATE_INDEX, new PopulateIndexEvent($populateInfo));
|
||||
|
||||
return $app->redirectPath('worker_admin');
|
||||
}
|
||||
|
||||
return $this->render('admin/worker-manager/worker_searchengine.html.twig', [
|
||||
'form' => $form->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
public function subviewAction(PhraseaApplication $app)
|
||||
{
|
||||
return $this->render('admin/worker-manager/worker_subview.html.twig', [
|
||||
]);
|
||||
}
|
||||
|
||||
public function metadataAction(PhraseaApplication $app)
|
||||
{
|
||||
return $this->render('admin/worker-manager/worker_metadata.html.twig', [
|
||||
]);
|
||||
}
|
||||
|
||||
public function populateStatusAction(PhraseaApplication $app, Request $request)
|
||||
{
|
||||
$databoxIds = $request->get('sbasIds');
|
||||
|
||||
/** @var WorkerRunningPopulateRepository $repoWorkerPopulate */
|
||||
$repoWorkerPopulate = $app['repo.worker-running-populate'];
|
||||
|
||||
return $repoWorkerPopulate->checkPopulateStatusByDataboxIds($databoxIds);
|
||||
}
|
||||
|
||||
public function pullAssetsAction(PhraseaApplication $app, Request $request)
|
||||
{
|
||||
$pullAssetsConfig = $this->getPullAssetsConfiguration();
|
||||
$form = $app->form(new WorkerPullAssetsType(), $pullAssetsConfig);
|
||||
|
||||
$form->handleRequest($request);
|
||||
if ($form->isValid()) {
|
||||
/** @var AMQPConnection $serverConnection */
|
||||
$serverConnection = $this->app['alchemy_worker.amqp.connection'];
|
||||
$serverConnection->setQueue(MessagePublisher::PULL_QUEUE);
|
||||
|
||||
// save new pull config
|
||||
$app['conf']->set(['workers', 'pull_assets'], array_merge($pullAssetsConfig, $form->getData()));
|
||||
|
||||
// reinitialize the pull queues
|
||||
$serverConnection->reinitializeQueue([MessagePublisher::PULL_QUEUE]);
|
||||
$this->app['alchemy_worker.message.publisher']->initializePullAssets();
|
||||
|
||||
return $app->redirectPath('worker_admin');
|
||||
}
|
||||
|
||||
return $this->render('admin/worker-manager/worker_pull_assets.html.twig', [
|
||||
'form' => $form->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EventDispatcherInterface
|
||||
*/
|
||||
private function getDispatcher()
|
||||
{
|
||||
return $this->app['dispatcher'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ElasticsearchOptions
|
||||
*/
|
||||
private function getElasticsearchOptions()
|
||||
{
|
||||
return $this->app['elasticsearch.options'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormInterface $form
|
||||
* @return array
|
||||
*/
|
||||
private function getData(FormInterface $form)
|
||||
{
|
||||
/** @var ElasticsearchOptions $options */
|
||||
$options = $form->getData();
|
||||
|
||||
$data['host'] = $options->getHost();
|
||||
$data['port'] = $options->getPort();
|
||||
$data['indexName'] = $options->getIndexName();
|
||||
$data['databoxIds'] = $form->getExtraData()['sbas'];
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function getPullAssetsConfiguration()
|
||||
{
|
||||
return $this->app['conf']->get(['workers', 'pull_assets'], []);
|
||||
}
|
||||
|
||||
private function getRetryQueueConfiguration()
|
||||
{
|
||||
return $this->app['conf']->get(['workers', 'retry_queue'], []);
|
||||
}
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Event;
|
||||
|
||||
use Symfony\Component\EventDispatcher\Event as SfEvent;
|
||||
|
||||
class AssetsCreateEvent extends SfEvent
|
||||
{
|
||||
private $data;
|
||||
|
||||
public function __construct($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
public function getData()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Event;
|
||||
|
||||
use Symfony\Component\EventDispatcher\Event as SfEvent;
|
||||
|
||||
class AssetsCreationFailureEvent extends SfEvent
|
||||
{
|
||||
private $payload;
|
||||
private $workerMessage;
|
||||
private $count;
|
||||
|
||||
public function __construct($payload, $workerMessage, $count = 2)
|
||||
{
|
||||
$this->payload = $payload;
|
||||
$this->workerMessage = $workerMessage;
|
||||
$this->count = $count;
|
||||
}
|
||||
|
||||
public function getPayload()
|
||||
{
|
||||
return $this->payload;
|
||||
}
|
||||
|
||||
public function getWorkerMessage()
|
||||
{
|
||||
return $this->workerMessage;
|
||||
}
|
||||
|
||||
public function getCount()
|
||||
{
|
||||
return $this->count;
|
||||
}
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Event;
|
||||
|
||||
use Symfony\Component\EventDispatcher\Event as SfEvent;
|
||||
|
||||
class AssetsCreationRecordFailureEvent extends SfEvent
|
||||
{
|
||||
/** @var array */
|
||||
private $payload;
|
||||
private $workerMessage;
|
||||
private $count;
|
||||
|
||||
public function __construct($payload, $workerMessage = '', $count = 2)
|
||||
{
|
||||
$this->payload = $payload;
|
||||
$this->workerMessage = $workerMessage;
|
||||
$this->count = $count;
|
||||
}
|
||||
|
||||
public function getPayload()
|
||||
{
|
||||
return $this->payload;
|
||||
}
|
||||
|
||||
public function getWorkerMessage()
|
||||
{
|
||||
return $this->workerMessage;
|
||||
}
|
||||
|
||||
public function getCount()
|
||||
{
|
||||
return $this->count;
|
||||
}
|
||||
}
|
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Event;
|
||||
|
||||
use Symfony\Component\EventDispatcher\Event as SfEvent;
|
||||
|
||||
class ExportMailFailureEvent extends SfEvent
|
||||
{
|
||||
private $emitterUserId;
|
||||
private $tokenValue;
|
||||
private $destinationMails;
|
||||
private $params;
|
||||
private $workerMessage;
|
||||
private $count;
|
||||
|
||||
public function __construct($emitterUserId, $tokenValue, $destinationMails, $params, $workerMessage = '', $count = 2)
|
||||
{
|
||||
$this->emitterUserId = $emitterUserId;
|
||||
$this->tokenValue = $tokenValue;
|
||||
$this->destinationMails = $destinationMails;
|
||||
$this->params = $params;
|
||||
$this->workerMessage = $workerMessage;
|
||||
$this->count = $count;
|
||||
}
|
||||
|
||||
public function getEmitterUserId()
|
||||
{
|
||||
return $this->emitterUserId;
|
||||
}
|
||||
|
||||
public function getTokenValue()
|
||||
{
|
||||
return $this->tokenValue;
|
||||
}
|
||||
|
||||
public function getDestinationMails()
|
||||
{
|
||||
return $this->destinationMails;
|
||||
}
|
||||
|
||||
public function getParams()
|
||||
{
|
||||
return $this->params;
|
||||
}
|
||||
|
||||
public function getWorkerMessage()
|
||||
{
|
||||
return $this->workerMessage;
|
||||
}
|
||||
|
||||
public function getCount()
|
||||
{
|
||||
return $this->count;
|
||||
}
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Event;
|
||||
|
||||
use Symfony\Component\EventDispatcher\Event as SfEvent;
|
||||
|
||||
class PopulateIndexEvent extends SfEvent
|
||||
{
|
||||
/** @var array */
|
||||
private $data;
|
||||
|
||||
public function __construct($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getData()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Event;
|
||||
|
||||
use Symfony\Component\EventDispatcher\Event as SfEvent;
|
||||
|
||||
class PopulateIndexFailureEvent extends SfEvent
|
||||
{
|
||||
private $host;
|
||||
private $port;
|
||||
private $indexName;
|
||||
private $databoxId;
|
||||
private $workerMessage;
|
||||
private $count;
|
||||
|
||||
public function __construct($host, $port, $indexName, $databoxId, $workerMessage = '', $count = 2)
|
||||
{
|
||||
$this->host = $host;
|
||||
$this->port = $port;
|
||||
$this->indexName = $indexName;
|
||||
$this->databoxId = $databoxId;
|
||||
$this->workerMessage = $workerMessage;
|
||||
$this->count = $count;
|
||||
}
|
||||
|
||||
public function getHost()
|
||||
{
|
||||
return $this->host;
|
||||
}
|
||||
|
||||
public function getPort()
|
||||
{
|
||||
return $this->port;
|
||||
}
|
||||
|
||||
public function getIndexName()
|
||||
{
|
||||
return $this->indexName;
|
||||
}
|
||||
|
||||
public function getDataboxId()
|
||||
{
|
||||
return $this->databoxId;
|
||||
}
|
||||
|
||||
public function getWorkerMessage()
|
||||
{
|
||||
return $this->workerMessage;
|
||||
}
|
||||
|
||||
public function getCount()
|
||||
{
|
||||
return $this->count;
|
||||
}
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Event;
|
||||
|
||||
use Symfony\Component\EventDispatcher\Event as SfEvent;
|
||||
|
||||
class StoryCreateCoverEvent extends SfEvent
|
||||
{
|
||||
private $data;
|
||||
|
||||
public function __construct($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
public function getData()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Event;
|
||||
|
||||
use Alchemy\Phrasea\Core\Event\Record\RecordEvent;
|
||||
use Alchemy\Phrasea\Model\RecordInterface;
|
||||
|
||||
class SubdefinitionCreationFailureEvent extends RecordEvent
|
||||
{
|
||||
private $subdefName;
|
||||
private $workerMessage;
|
||||
private $count;
|
||||
|
||||
public function __construct(RecordInterface $record, $subdefName, $workerMessage = '', $count = 2)
|
||||
{
|
||||
parent::__construct($record);
|
||||
|
||||
$this->subdefName = $subdefName;
|
||||
$this->workerMessage = $workerMessage;
|
||||
$this->count = $count;
|
||||
}
|
||||
|
||||
public function getSubdefName()
|
||||
{
|
||||
return $this->subdefName;
|
||||
}
|
||||
|
||||
public function getWorkerMessage()
|
||||
{
|
||||
return $this->workerMessage;
|
||||
}
|
||||
|
||||
public function getCount()
|
||||
{
|
||||
return $this->count;
|
||||
}
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Event;
|
||||
|
||||
use Alchemy\Phrasea\Core\Event\Record\RecordEvent;
|
||||
use Alchemy\Phrasea\Model\RecordInterface;
|
||||
|
||||
class SubdefinitionWritemetaEvent extends RecordEvent
|
||||
{
|
||||
const CREATE = 'create';
|
||||
const FAILED = 'failed';
|
||||
|
||||
private $status;
|
||||
private $subdefName;
|
||||
private $workerMessage;
|
||||
private $count;
|
||||
|
||||
public function __construct(RecordInterface $record, $subdefName, $status = self::CREATE, $workerMessage = '', $count = 2)
|
||||
{
|
||||
parent::__construct($record);
|
||||
|
||||
$this->subdefName = $subdefName;
|
||||
$this->status = $status;
|
||||
$this->workerMessage = $workerMessage;
|
||||
$this->count = $count;
|
||||
}
|
||||
|
||||
public function getSubdefName()
|
||||
{
|
||||
return $this->subdefName;
|
||||
}
|
||||
|
||||
public function getStatus()
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function getWorkerMessage()
|
||||
{
|
||||
return $this->workerMessage;
|
||||
}
|
||||
|
||||
public function getCount()
|
||||
{
|
||||
return $this->count;
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Event;
|
||||
|
||||
use Symfony\Component\EventDispatcher\Event as SfEvent;
|
||||
|
||||
class WebhookDeliverFailureEvent extends SfEvent
|
||||
{
|
||||
private $webhookEventId;
|
||||
private $workerMessage;
|
||||
private $count;
|
||||
private $deleveryId;
|
||||
|
||||
public function __construct($webhookEventId, $workerMessage, $count = 2, $deleveryId = null)
|
||||
{
|
||||
$this->webhookEventId = $webhookEventId;
|
||||
$this->workerMessage = $workerMessage;
|
||||
$this->count = $count;
|
||||
$this->deleveryId = $deleveryId;
|
||||
}
|
||||
|
||||
public function getWebhookEventId()
|
||||
{
|
||||
return $this->webhookEventId;
|
||||
}
|
||||
|
||||
public function getWorkerMessage()
|
||||
{
|
||||
return $this->workerMessage;
|
||||
}
|
||||
|
||||
public function getCount()
|
||||
{
|
||||
return $this->count;
|
||||
}
|
||||
|
||||
public function getDeleveryId()
|
||||
{
|
||||
return $this->deleveryId;
|
||||
}
|
||||
}
|
22
lib/Alchemy/Phrasea/WorkerManager/Event/WorkerEvents.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Event;
|
||||
|
||||
final class WorkerEvents
|
||||
{
|
||||
const ASSETS_CREATE = 'assets.create';
|
||||
const ASSETS_CREATION_FAILURE = 'assets.create_failure';
|
||||
const ASSETS_CREATION_RECORD_FAILURE = 'assets.creation_record_failure';
|
||||
|
||||
const STORY_CREATE_COVER = 'story.create_cover';
|
||||
|
||||
const POPULATE_INDEX = 'populate.index';
|
||||
const POPULATE_INDEX_FAILURE = "populate.index_failure";
|
||||
|
||||
const SUBDEFINITION_WRITE_META = 'subdefinition.write_meta';
|
||||
const SUBDEFINITION_CREATION_FAILURE = 'subdefinition.creation_failure';
|
||||
|
||||
const EXPORT_MAIL_FAILURE = 'export.mail_failure';
|
||||
|
||||
const WEBHOOK_DELIVER_FAILURE = 'webhook.deliver_failure';
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Form;
|
||||
|
||||
use Alchemy\Phrasea\WorkerManager\Queue\MessagePublisher;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class WorkerConfigurationType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
parent::buildForm($builder, $options);
|
||||
|
||||
$builder
|
||||
->add(MessagePublisher::ASSETS_INGEST_TYPE, 'text', [
|
||||
'label' => 'admin::workermanager:tab:workerconfig: Ingest retry delay in ms'
|
||||
])
|
||||
->add(MessagePublisher::CREATE_RECORD_TYPE, 'text', [
|
||||
'label' => 'admin::workermanager:tab:workerconfig: Create record retry delay in ms'
|
||||
])
|
||||
->add(MessagePublisher::SUBDEF_CREATION_TYPE, 'text', [
|
||||
'label' => 'admin::workermanager:tab:workerconfig: Subdefinition retry delay in ms'
|
||||
])
|
||||
->add(MessagePublisher::WRITE_METADATAS_TYPE, 'text', [
|
||||
'label' => 'admin::workermanager:tab:workerconfig: Metadatas retry delay in ms'
|
||||
])
|
||||
->add(MessagePublisher::WEBHOOK_TYPE, 'text', [
|
||||
'label' => 'admin::workermanager:tab:workerconfig: Webhook retry delay in ms'
|
||||
])
|
||||
->add(MessagePublisher::EXPORT_MAIL_TYPE, 'text', [
|
||||
'label' => 'admin::workermanager:tab:workerconfig: Export mail retry delay in ms'
|
||||
])
|
||||
->add(MessagePublisher::POPULATE_INDEX_TYPE, 'text', [
|
||||
'label' => 'admin::workermanager:tab:workerconfig: Populate Index retry delay in ms'
|
||||
])
|
||||
->add('delayedSubdef', 'text', [
|
||||
'label' => 'admin::workermanager:tab:workerconfig: Subdef delay in ms'
|
||||
])
|
||||
->add('delayedWriteMeta', 'text', [
|
||||
'label' => 'admin::workermanager:tab:workerconfig: Write meta delay in ms'
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return 'worker_configuration';
|
||||
}
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Form;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class WorkerPullAssetsType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
parent::buildForm($builder, $options);
|
||||
|
||||
$builder
|
||||
->add('endpointCommit', 'text', [
|
||||
'label' => 'admin::workermanager:tab:pullassets: Endpoint get commit'
|
||||
])
|
||||
->add('endpointToken', 'text', [
|
||||
'label' => 'admin::workermanager:tab:pullassets: Endpoint get token'
|
||||
])
|
||||
->add('clientSecret', 'text', [
|
||||
'label' => 'admin::workermanager:tab:pullassets: Client secret'
|
||||
])
|
||||
->add('clientId', 'text', [
|
||||
'label' => 'admin::workermanager:tab:pullassets: Client ID'
|
||||
])
|
||||
->add('pullInterval', 'text', [
|
||||
'label' => 'admin::workermanager:tab:pullassets: Fetching interval in second'
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return 'worker_pullAssets';
|
||||
}
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Form;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\Constraints\Range;
|
||||
|
||||
class WorkerSearchengineType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
parent::buildForm($builder, $options);
|
||||
|
||||
$builder
|
||||
->add('host', 'text', [
|
||||
'label' => 'admin::workermanager:tab:searchengine: Elasticsearch server host',
|
||||
'constraints' => new NotBlank(),
|
||||
])
|
||||
->add('port', 'integer', [
|
||||
'label' => 'admin::workermanager:tab:searchengine: Elasticsearch service port',
|
||||
'constraints' => [
|
||||
new Range(['min' => 1, 'max' => 65535]),
|
||||
new NotBlank()
|
||||
]
|
||||
])
|
||||
->add('indexName', 'text', [
|
||||
'label' => 'admin::workermanager:tab:searchengine: Elasticsearch index name',
|
||||
'constraints' => new NotBlank(),
|
||||
'attr' =>['data-class'=>'inline']
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function setDefaultOptions(OptionsResolverInterface $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'allow_extra_fields' => true
|
||||
]);
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return 'worker_searchengine';
|
||||
}
|
||||
}
|
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Provider;
|
||||
|
||||
use Alchemy\Phrasea\Application as PhraseaApplication;
|
||||
use Alchemy\Phrasea\Core\LazyLocator;
|
||||
use Alchemy\Phrasea\Plugin\PluginProviderInterface;
|
||||
use Alchemy\Phrasea\WorkerManager\Queue\MessagePublisher;
|
||||
use Alchemy\Phrasea\WorkerManager\Worker\AssetsIngestWorker;
|
||||
use Alchemy\Phrasea\WorkerManager\Worker\CreateRecordWorker;
|
||||
use Alchemy\Phrasea\WorkerManager\Worker\DeleteRecordWorker;
|
||||
use Alchemy\Phrasea\WorkerManager\Worker\ExportMailWorker;
|
||||
use Alchemy\Phrasea\WorkerManager\Worker\Factory\CallableWorkerFactory;
|
||||
use Alchemy\Phrasea\WorkerManager\Worker\PopulateIndexWorker;
|
||||
use Alchemy\Phrasea\WorkerManager\Worker\ProcessPool;
|
||||
use Alchemy\Phrasea\WorkerManager\Worker\PullAssetsWorker;
|
||||
use Alchemy\Phrasea\WorkerManager\Worker\Resolver\TypeBasedWorkerResolver;
|
||||
use Alchemy\Phrasea\WorkerManager\Worker\SubdefCreationWorker;
|
||||
use Alchemy\Phrasea\WorkerManager\Worker\WebhookWorker;
|
||||
use Alchemy\Phrasea\WorkerManager\Worker\WorkerInvoker;
|
||||
use Alchemy\Phrasea\WorkerManager\Worker\WriteMetadatasWorker;
|
||||
use Monolog\Handler\RotatingFileHandler;
|
||||
use Monolog\Logger;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Silex\Application;
|
||||
|
||||
class AlchemyWorkerServiceProvider implements PluginProviderInterface
|
||||
{
|
||||
public function register(Application $app)
|
||||
{
|
||||
$app['alchemy_worker.type_based_worker_resolver'] = $app->share(function () {
|
||||
return new TypeBasedWorkerResolver();
|
||||
});
|
||||
|
||||
$app['alchemy_worker.logger'] = $app->share(function (Application $app) {
|
||||
$logger = new $app['monolog.logger.class']('alchemy-service logger');
|
||||
$logger->pushHandler(new RotatingFileHandler(
|
||||
$app['log.path'] . DIRECTORY_SEPARATOR . 'worker_service.log',
|
||||
10,
|
||||
Logger::INFO
|
||||
));
|
||||
|
||||
return $logger;
|
||||
});
|
||||
|
||||
// use the console logger
|
||||
$loggerSetter = function (LoggerAwareInterface $loggerAware) use ($app) {
|
||||
if (isset($app['logger'])) {
|
||||
$loggerAware->setLogger($app['logger']);
|
||||
}
|
||||
|
||||
return $loggerAware;
|
||||
};
|
||||
|
||||
$app['alchemy_worker.process_pool'] = $app->share(function (Application $app) use ($loggerSetter) {
|
||||
return $loggerSetter(new ProcessPool());
|
||||
});
|
||||
|
||||
$app['alchemy_worker.worker_invoker'] = $app->share(function (Application $app) use ($loggerSetter) {
|
||||
return $loggerSetter(new WorkerInvoker($app['alchemy_worker.process_pool']));
|
||||
});
|
||||
|
||||
|
||||
// register workers
|
||||
$app['alchemy_worker.type_based_worker_resolver']->addFactory(MessagePublisher::SUBDEF_CREATION_TYPE, new CallableWorkerFactory(function () use ($app) {
|
||||
return (new SubdefCreationWorker(
|
||||
$app['subdef.generator'],
|
||||
$app['alchemy_worker.message.publisher'],
|
||||
$app['alchemy_worker.logger'],
|
||||
$app['dispatcher'],
|
||||
$app['phraseanet.filesystem'],
|
||||
$app['repo.worker-running-job'],
|
||||
$app['elasticsearch.indexer']
|
||||
))
|
||||
->setApplicationBox($app['phraseanet.appbox'])
|
||||
;
|
||||
}));
|
||||
|
||||
$app['alchemy_worker.type_based_worker_resolver']->addFactory(MessagePublisher::WRITE_METADATAS_TYPE, new CallableWorkerFactory(function () use ($app) {
|
||||
return (new WriteMetadatasWorker(
|
||||
$app['exiftool.writer'],
|
||||
$app['alchemy_worker.logger'],
|
||||
$app['alchemy_worker.message.publisher'],
|
||||
$app['repo.worker-running-job']
|
||||
))
|
||||
->setApplicationBox($app['phraseanet.appbox'])
|
||||
->setDispatcher($app['dispatcher'])
|
||||
->setEntityManagerLocator(new LazyLocator($app, 'orm.em'))
|
||||
;
|
||||
}));
|
||||
|
||||
$app['alchemy_worker.type_based_worker_resolver']->addFactory(MessagePublisher::EXPORT_MAIL_TYPE, new CallableWorkerFactory(function () use ($app) {
|
||||
return (new ExportMailWorker($app))
|
||||
->setDelivererLocator(new LazyLocator($app, 'notification.deliverer'));
|
||||
}));
|
||||
|
||||
$app['alchemy_worker.type_based_worker_resolver']->addFactory(MessagePublisher::ASSETS_INGEST_TYPE, new CallableWorkerFactory(function () use ($app) {
|
||||
return (new AssetsIngestWorker($app))
|
||||
->setEntityManagerLocator(new LazyLocator($app, 'orm.em'));
|
||||
}));
|
||||
|
||||
$app['alchemy_worker.type_based_worker_resolver']->addFactory(MessagePublisher::WEBHOOK_TYPE, new CallableWorkerFactory(function () use ($app) {
|
||||
return (new WebhookWorker($app))
|
||||
->setDispatcher($app['dispatcher']);
|
||||
}));
|
||||
|
||||
$app['alchemy_worker.type_based_worker_resolver']->addFactory(MessagePublisher::CREATE_RECORD_TYPE, new CallableWorkerFactory(function () use ($app) {
|
||||
return (new CreateRecordWorker($app))
|
||||
->setApplicationBox($app['phraseanet.appbox'])
|
||||
->setBorderManagerLocator(new LazyLocator($app, 'border-manager'))
|
||||
->setEntityManagerLocator(new LazyLocator($app, 'orm.em'))
|
||||
->setFileSystemLocator(new LazyLocator($app, 'filesystem'))
|
||||
->setTemporaryFileSystemLocator(new LazyLocator($app, 'temporary-filesystem'))
|
||||
->setDispatcher($app['dispatcher']);
|
||||
}));
|
||||
|
||||
$app['alchemy_worker.type_based_worker_resolver']->addFactory(MessagePublisher::POPULATE_INDEX_TYPE, new CallableWorkerFactory(function () use ($app) {
|
||||
return (new PopulateIndexWorker($app['alchemy_worker.message.publisher'], $app['elasticsearch.indexer'], $app['repo.worker-running-populate']))
|
||||
->setApplicationBox($app['phraseanet.appbox'])
|
||||
->setDispatcher($app['dispatcher']);
|
||||
}));
|
||||
|
||||
$app['alchemy_worker.type_based_worker_resolver']->addFactory(MessagePublisher::PULL_ASSETS_TYPE, new CallableWorkerFactory(function () use ($app) {
|
||||
return new PullAssetsWorker($app['alchemy_worker.message.publisher'], $app['conf'], $app['repo.worker-running-uploader']);
|
||||
}));
|
||||
|
||||
$app['alchemy_worker.type_based_worker_resolver']->addFactory(MessagePublisher::DELETE_RECORD_TYPE, new CallableWorkerFactory(function () use ($app) {
|
||||
return (new DeleteRecordWorker())
|
||||
->setApplicationBox($app['phraseanet.appbox']);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function boot(Application $app)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(PhraseaApplication $app)
|
||||
{
|
||||
return new static();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Provider;
|
||||
|
||||
use Alchemy\Phrasea\Application as PhraseaApplication;
|
||||
use Alchemy\Phrasea\ControllerProvider\ControllerProviderTrait;
|
||||
use Alchemy\Phrasea\Security\Firewall;
|
||||
use Alchemy\Phrasea\WorkerManager\Controller\AdminConfigurationController;
|
||||
use Alchemy\Phrasea\WorkerManager\Queue\MessagePublisher;
|
||||
use Silex\Application;
|
||||
use Silex\ControllerProviderInterface;
|
||||
use Silex\ServiceProviderInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class ControllerServiceProvider implements ControllerProviderInterface, ServiceProviderInterface
|
||||
{
|
||||
use ControllerProviderTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function register(Application $app)
|
||||
{
|
||||
$app['controller.worker.admin.configuration'] = $app->share(function (PhraseaApplication $app) {
|
||||
return new AdminConfigurationController($app);
|
||||
});
|
||||
|
||||
// example of route to check webhook
|
||||
$app->post('/webhook', array($this, 'getWebhookData'));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function boot(Application $app)
|
||||
{
|
||||
}
|
||||
|
||||
public function connect(Application $app)
|
||||
{
|
||||
$controllers = $this->createAuthenticatedCollection($app);
|
||||
$firewall = $this->getFirewall($app);
|
||||
|
||||
$controllers->before(function () use ($firewall) {
|
||||
$firewall->requireRight(\ACL::TASKMANAGER);
|
||||
});
|
||||
|
||||
$controllers->match('/', 'controller.worker.admin.configuration:indexAction')
|
||||
->method('GET')
|
||||
->bind('worker_admin');
|
||||
|
||||
$controllers->match('/configuration', 'controller.worker.admin.configuration:configurationAction')
|
||||
->method('GET|POST')
|
||||
->bind('worker_admin_configuration');
|
||||
|
||||
$controllers->match('/info', 'controller.worker.admin.configuration:infoAction')
|
||||
->method('GET')
|
||||
->bind('worker_admin_info');
|
||||
|
||||
$controllers->match('/truncate', 'controller.worker.admin.configuration:truncateTableAction')
|
||||
->method('POST')
|
||||
->bind('worker_admin_truncate');
|
||||
|
||||
$controllers->match('/delete-finished', 'controller.worker.admin.configuration:deleteFinishedAction')
|
||||
->method('POST')
|
||||
->bind('worker_admin_delete_finished');
|
||||
|
||||
$controllers->match('/searchengine', 'controller.worker.admin.configuration:searchengineAction')
|
||||
->method('GET|POST')
|
||||
->bind('worker_admin_searchengine');
|
||||
|
||||
$controllers->match('/subview', 'controller.worker.admin.configuration:subviewAction')
|
||||
->method('GET|POST')
|
||||
->bind('worker_admin_subview');
|
||||
|
||||
$controllers->match('/metadata', 'controller.worker.admin.configuration:metadataAction')
|
||||
->method('GET|POST')
|
||||
->bind('worker_admin_metadata');
|
||||
|
||||
$controllers->get('/populate-status', 'controller.worker.admin.configuration:populateStatusAction')
|
||||
->bind('worker_admin_populate_status');
|
||||
|
||||
$controllers->match('/pull-assets', 'controller.worker.admin.configuration:pullAssetsAction')
|
||||
->method('GET|POST')
|
||||
->bind('worker_admin_pullAssets');
|
||||
|
||||
return $controllers;
|
||||
}
|
||||
|
||||
public function getWebhookData(Application $app, Request $request)
|
||||
{
|
||||
$messagePubliser = $this->getMessagePublisher($app);
|
||||
$messagePubliser->pushLog("RECEIVED ON phraseanet WEBHOOK URL TEST = ". $request->getUri() . " DATA : ". $request->getContent());
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Application $app
|
||||
* @return Firewall
|
||||
*/
|
||||
private function getFirewall(Application $app)
|
||||
{
|
||||
return $app['firewall'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Application $app
|
||||
* @return MessagePublisher
|
||||
*/
|
||||
private function getMessagePublisher(Application $app)
|
||||
{
|
||||
return $app['alchemy_worker.message.publisher'];
|
||||
}
|
||||
}
|
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Phraseanet graylog plugin
|
||||
*
|
||||
* (c) 2005-2019 Alchemy
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Provider;
|
||||
|
||||
use Alchemy\Phrasea\Core\LazyLocator;
|
||||
use Alchemy\Phrasea\Model\Manipulator\WebhookEventManipulator;
|
||||
use Alchemy\Phrasea\Plugin\PluginProviderInterface;
|
||||
use Alchemy\Phrasea\Application as PhraseaApplication;
|
||||
use Alchemy\Phrasea\WorkerManager\Queue\AMQPConnection;
|
||||
use Alchemy\Phrasea\WorkerManager\Queue\MessageHandler;
|
||||
use Alchemy\Phrasea\WorkerManager\Queue\MessagePublisher;
|
||||
use Alchemy\Phrasea\WorkerManager\Queue\WebhookPublisher;
|
||||
use Alchemy\Phrasea\WorkerManager\Subscriber\AssetsIngestSubscriber;
|
||||
use Alchemy\Phrasea\WorkerManager\Subscriber\ExportSubscriber;
|
||||
use Alchemy\Phrasea\WorkerManager\Subscriber\RecordSubscriber;
|
||||
use Alchemy\Phrasea\WorkerManager\Subscriber\SearchengineSubscriber;
|
||||
use Alchemy\Phrasea\WorkerManager\Subscriber\WebhookSubscriber;
|
||||
use Silex\Application;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class QueueWorkerServiceProvider implements PluginProviderInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function register(Application $app)
|
||||
{
|
||||
$app['alchemy_worker.amqp.connection'] = $app->share(function (Application $app) {
|
||||
return new AMQPConnection($app['conf']);
|
||||
});
|
||||
|
||||
$app['alchemy_worker.message.handler'] = $app->share(function (Application $app) {
|
||||
return new MessageHandler($app['alchemy_worker.message.publisher']);
|
||||
});
|
||||
|
||||
$app['alchemy_worker.message.publisher'] = $app->share(function (Application $app) {
|
||||
return new MessagePublisher($app['alchemy_worker.amqp.connection'], $app['alchemy_worker.logger']);
|
||||
});
|
||||
|
||||
$app['alchemy_worker.webhook.publisher'] = $app->share(function (Application $app) {
|
||||
return new WebhookPublisher($app['alchemy_worker.message.publisher']);
|
||||
});
|
||||
|
||||
$app['manipulator.webhook-event'] = $app->share(function (Application $app) {
|
||||
return new WebhookEventManipulator(
|
||||
$app['orm.em'],
|
||||
$app['repo.webhook-event'],
|
||||
$app['alchemy_worker.webhook.publisher']
|
||||
);
|
||||
});
|
||||
|
||||
$app['dispatcher'] = $app->share(
|
||||
$app->extend('dispatcher', function (EventDispatcherInterface $dispatcher, Application $app) {
|
||||
|
||||
$dispatcher->addSubscriber(
|
||||
new RecordSubscriber($app, new LazyLocator($app, 'phraseanet.appbox'))
|
||||
);
|
||||
$dispatcher->addSubscriber(new ExportSubscriber($app['alchemy_worker.message.publisher']));
|
||||
$dispatcher->addSubscriber(new AssetsIngestSubscriber($app['alchemy_worker.message.publisher']));
|
||||
$dispatcher->addSubscriber(new SearchengineSubscriber($app['alchemy_worker.message.publisher']));
|
||||
$dispatcher->addSubscriber(new WebhookSubscriber($app['alchemy_worker.message.publisher']));
|
||||
|
||||
return $dispatcher;
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function boot(Application $app)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(PhraseaApplication $app)
|
||||
{
|
||||
return new static();
|
||||
}
|
||||
|
||||
}
|
259
lib/Alchemy/Phrasea/WorkerManager/Queue/AMQPConnection.php
Normal file
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Queue;
|
||||
|
||||
use Alchemy\Phrasea\Core\Configuration\PropertyAccess;
|
||||
use PhpAmqpLib\Channel\AMQPChannel;
|
||||
use PhpAmqpLib\Connection\AMQPStreamConnection;
|
||||
use PhpAmqpLib\Wire\AMQPTable;
|
||||
|
||||
class AMQPConnection
|
||||
{
|
||||
const ALCHEMY_EXCHANGE = 'alchemy-exchange';
|
||||
const RETRY_ALCHEMY_EXCHANGE = 'retry-alchemy-exchange';
|
||||
|
||||
/** @var AMQPStreamConnection */
|
||||
private $connection;
|
||||
/** @var AMQPChannel */
|
||||
private $channel;
|
||||
|
||||
private $hostConfig;
|
||||
private $conf;
|
||||
|
||||
public static $defaultQueues = [
|
||||
MessagePublisher::WRITE_METADATAS_TYPE => MessagePublisher::METADATAS_QUEUE,
|
||||
MessagePublisher::SUBDEF_CREATION_TYPE => MessagePublisher::SUBDEF_QUEUE,
|
||||
MessagePublisher::EXPORT_MAIL_TYPE => MessagePublisher::EXPORT_QUEUE,
|
||||
MessagePublisher::WEBHOOK_TYPE => MessagePublisher::WEBHOOK_QUEUE,
|
||||
MessagePublisher::ASSETS_INGEST_TYPE => MessagePublisher::ASSETS_INGEST_QUEUE,
|
||||
MessagePublisher::CREATE_RECORD_TYPE => MessagePublisher::CREATE_RECORD_QUEUE,
|
||||
MessagePublisher::PULL_QUEUE => MessagePublisher::PULL_QUEUE,
|
||||
MessagePublisher::POPULATE_INDEX_TYPE => MessagePublisher::POPULATE_INDEX_QUEUE,
|
||||
MessagePublisher::DELETE_RECORD_TYPE => MessagePublisher::DELETE_RECORD_QUEUE
|
||||
];
|
||||
|
||||
// the corresponding worker queues and retry queues, loop queue
|
||||
public static $defaultRetryQueues = [
|
||||
MessagePublisher::METADATAS_QUEUE => MessagePublisher::RETRY_METADATAS_QUEUE,
|
||||
MessagePublisher::SUBDEF_QUEUE => MessagePublisher::RETRY_SUBDEF_QUEUE,
|
||||
MessagePublisher::EXPORT_QUEUE => MessagePublisher::RETRY_EXPORT_QUEUE,
|
||||
MessagePublisher::WEBHOOK_QUEUE => MessagePublisher::RETRY_WEBHOOK_QUEUE,
|
||||
MessagePublisher::ASSETS_INGEST_QUEUE => MessagePublisher::RETRY_ASSETS_INGEST_QUEUE,
|
||||
MessagePublisher::CREATE_RECORD_QUEUE => MessagePublisher::RETRY_CREATE_RECORD_QUEUE,
|
||||
MessagePublisher::POPULATE_INDEX_QUEUE => MessagePublisher::RETRY_POPULATE_INDEX_QUEUE,
|
||||
MessagePublisher::PULL_QUEUE => MessagePublisher::LOOP_PULL_QUEUE
|
||||
];
|
||||
|
||||
// default message TTL in retry queue in millisecond
|
||||
public static $defaultFailedQueues = [
|
||||
MessagePublisher::WRITE_METADATAS_TYPE => MessagePublisher::FAILED_METADATAS_QUEUE,
|
||||
MessagePublisher::SUBDEF_CREATION_TYPE => MessagePublisher::FAILED_SUBDEF_QUEUE,
|
||||
MessagePublisher::EXPORT_MAIL_TYPE => MessagePublisher::FAILED_EXPORT_QUEUE,
|
||||
MessagePublisher::WEBHOOK_TYPE => MessagePublisher::FAILED_WEBHOOK_QUEUE,
|
||||
MessagePublisher::ASSETS_INGEST_TYPE => MessagePublisher::FAILED_ASSETS_INGEST_QUEUE,
|
||||
MessagePublisher::CREATE_RECORD_TYPE => MessagePublisher::FAILED_CREATE_RECORD_QUEUE,
|
||||
MessagePublisher::POPULATE_INDEX_TYPE => MessagePublisher::FAILED_POPULATE_INDEX_QUEUE
|
||||
];
|
||||
|
||||
public static $defaultDelayedQueues = [
|
||||
MessagePublisher::METADATAS_QUEUE => MessagePublisher::DELAYED_METADATAS_QUEUE,
|
||||
MessagePublisher::SUBDEF_QUEUE => MessagePublisher::DELAYED_SUBDEF_QUEUE
|
||||
];
|
||||
|
||||
// default message TTL in retry queue in millisecond
|
||||
const RETRY_DELAY = 10000;
|
||||
|
||||
// default message TTL in delayed queue in millisecond
|
||||
const DELAY = 5000;
|
||||
|
||||
public function __construct(PropertyAccess $conf)
|
||||
{
|
||||
$defaultConfiguration = [
|
||||
'host' => 'localhost',
|
||||
'port' => 5672,
|
||||
'user' => 'guest',
|
||||
'password' => 'guest',
|
||||
'vhost' => '/'
|
||||
];
|
||||
|
||||
$this->hostConfig = $conf->get(['workers', 'queue', 'worker-queue'], $defaultConfiguration);
|
||||
$this->conf = $conf;
|
||||
}
|
||||
|
||||
public function getConnection()
|
||||
{
|
||||
if (!isset($this->connection)) {
|
||||
try{
|
||||
$this->connection = new AMQPStreamConnection(
|
||||
$this->hostConfig['host'],
|
||||
$this->hostConfig['port'],
|
||||
$this->hostConfig['user'],
|
||||
$this->hostConfig['password'],
|
||||
$this->hostConfig['vhost']
|
||||
);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
public function getChannel()
|
||||
{
|
||||
if (!isset($this->channel)) {
|
||||
$this->getConnection();
|
||||
if (isset($this->connection)) {
|
||||
$this->channel = $this->connection->channel();
|
||||
|
||||
return $this->channel;
|
||||
}
|
||||
|
||||
return null;
|
||||
} else {
|
||||
return $this->channel;
|
||||
}
|
||||
}
|
||||
|
||||
public function declareExchange()
|
||||
{
|
||||
if (isset($this->channel)) {
|
||||
$this->channel->exchange_declare(self::ALCHEMY_EXCHANGE, 'direct', false, true, false);
|
||||
$this->channel->exchange_declare(self::RETRY_ALCHEMY_EXCHANGE, 'direct', false, true, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $queueName
|
||||
* @return AMQPChannel|null
|
||||
*/
|
||||
public function setQueue($queueName)
|
||||
{
|
||||
if (!isset($this->channel)) {
|
||||
$this->getChannel();
|
||||
if (!isset($this->channel)) {
|
||||
// can't connect to rabbit
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->declareExchange();
|
||||
}
|
||||
|
||||
if (isset(self::$defaultRetryQueues[$queueName])) {
|
||||
$this->channel->queue_declare($queueName, false, true, false, false, false, new AMQPTable([
|
||||
'x-dead-letter-exchange' => self::RETRY_ALCHEMY_EXCHANGE, // the exchange to which republish a 'dead' message
|
||||
'x-dead-letter-routing-key' => self::$defaultRetryQueues[$queueName] // the routing key to apply to this 'dead' message
|
||||
]));
|
||||
|
||||
$this->channel->queue_bind($queueName, self::ALCHEMY_EXCHANGE, $queueName);
|
||||
|
||||
// declare also the corresponding retry queue
|
||||
// use this to delay the delivery of a message to the alchemy-exchange
|
||||
$this->channel->queue_declare(self::$defaultRetryQueues[$queueName], false, true, false, false, false, new AMQPTable([
|
||||
'x-dead-letter-exchange' => AMQPConnection::ALCHEMY_EXCHANGE,
|
||||
'x-dead-letter-routing-key' => $queueName,
|
||||
'x-message-ttl' => $this->getTtlRetryPerRouting($queueName)
|
||||
]));
|
||||
|
||||
$this->channel->queue_bind(self::$defaultRetryQueues[$queueName], AMQPConnection::RETRY_ALCHEMY_EXCHANGE, self::$defaultRetryQueues[$queueName]);
|
||||
|
||||
} elseif (in_array($queueName, self::$defaultRetryQueues)) {
|
||||
// if it's a retry queue
|
||||
$routing = array_search($queueName, AMQPConnection::$defaultRetryQueues);
|
||||
$this->channel->queue_declare($queueName, false, true, false, false, false, new AMQPTable([
|
||||
'x-dead-letter-exchange' => AMQPConnection::ALCHEMY_EXCHANGE,
|
||||
'x-dead-letter-routing-key' => $routing,
|
||||
'x-message-ttl' => $this->getTtlRetryPerRouting($routing)
|
||||
]));
|
||||
|
||||
$this->channel->queue_bind($queueName, AMQPConnection::RETRY_ALCHEMY_EXCHANGE, $queueName);
|
||||
} elseif (in_array($queueName, self::$defaultFailedQueues)) {
|
||||
// if it's a failed queue
|
||||
$this->channel->queue_declare($queueName, false, true, false, false, false);
|
||||
|
||||
$this->channel->queue_bind($queueName, AMQPConnection::RETRY_ALCHEMY_EXCHANGE, $queueName);
|
||||
} elseif (in_array($queueName, self::$defaultDelayedQueues)) {
|
||||
// if it's a delayed queue
|
||||
$routing = array_search($queueName, AMQPConnection::$defaultDelayedQueues);
|
||||
$this->channel->queue_declare($queueName, false, true, false, false, false, new AMQPTable([
|
||||
'x-dead-letter-exchange' => AMQPConnection::ALCHEMY_EXCHANGE,
|
||||
'x-dead-letter-routing-key' => $routing,
|
||||
'x-message-ttl' => $this->getTtlDelayedPerRouting($routing)
|
||||
]));
|
||||
|
||||
$this->channel->queue_bind($queueName, AMQPConnection::RETRY_ALCHEMY_EXCHANGE, $queueName);
|
||||
} else {
|
||||
$this->channel->queue_declare($queueName, false, true, false, false, false);
|
||||
|
||||
$this->channel->queue_bind($queueName, AMQPConnection::ALCHEMY_EXCHANGE, $queueName);
|
||||
}
|
||||
|
||||
return $this->channel;
|
||||
}
|
||||
|
||||
public function reinitializeQueue(array $queuNames)
|
||||
{
|
||||
if (!isset($this->channel)) {
|
||||
$this->getChannel();
|
||||
$this->declareExchange();
|
||||
}
|
||||
foreach ($queuNames as $queuName) {
|
||||
if (in_array($queuName, self::$defaultQueues)) {
|
||||
$this->channel->queue_purge($queuName);
|
||||
} else {
|
||||
$this->channel->queue_delete($queuName);
|
||||
}
|
||||
|
||||
if (isset(self::$defaultRetryQueues[$queuName])) {
|
||||
$this->channel->queue_delete(self::$defaultRetryQueues[$queuName]);
|
||||
}
|
||||
|
||||
$this->setQueue($queuName);
|
||||
}
|
||||
}
|
||||
|
||||
public function connectionClose()
|
||||
{
|
||||
$this->channel->close();
|
||||
$this->connection->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $routing
|
||||
* @return int
|
||||
*/
|
||||
private function getTtlRetryPerRouting($routing)
|
||||
{
|
||||
$config = $this->conf->get(['workers']);
|
||||
|
||||
if ($routing == MessagePublisher::PULL_QUEUE &&
|
||||
isset($config['pull_assets']) &&
|
||||
isset($config['pull_assets']['pullInterval']) ) {
|
||||
// convert in milli second
|
||||
return (int)($config['pull_assets']['pullInterval']) * 1000;
|
||||
} elseif (isset($config['retry_queue']) &&
|
||||
isset($config['retry_queue'][array_search($routing, AMQPConnection::$defaultQueues)])) {
|
||||
|
||||
return (int)($config['retry_queue'][array_search($routing, AMQPConnection::$defaultQueues)]);
|
||||
}
|
||||
|
||||
return self::RETRY_DELAY;
|
||||
}
|
||||
|
||||
private function getTtlDelayedPerRouting($routing)
|
||||
{
|
||||
$delayed = [
|
||||
MessagePublisher::METADATAS_QUEUE => 'delayedWriteMeta',
|
||||
MessagePublisher::SUBDEF_QUEUE => 'delayedSubdef'
|
||||
];
|
||||
|
||||
$config = $this->conf->get(['workers']);
|
||||
|
||||
if (isset($config['retry_queue']) && isset($config['retry_queue'][$delayed[$routing]])) {
|
||||
return (int)$config['retry_queue'][$delayed[$routing]];
|
||||
}
|
||||
|
||||
return self::DELAY;
|
||||
}
|
||||
}
|
119
lib/Alchemy/Phrasea/WorkerManager/Queue/MessageHandler.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Queue;
|
||||
|
||||
use Alchemy\Phrasea\WorkerManager\Worker\ProcessPool;
|
||||
use Alchemy\Phrasea\WorkerManager\Worker\WorkerInvoker;
|
||||
use PhpAmqpLib\Message\AMQPMessage;
|
||||
use PhpAmqpLib\Wire\AMQPTable;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class MessageHandler
|
||||
{
|
||||
const MAX_OF_TRY = 3;
|
||||
|
||||
private $messagePublisher;
|
||||
|
||||
public function __construct(MessagePublisher $messagePublisher)
|
||||
{
|
||||
$this->messagePublisher = $messagePublisher;
|
||||
}
|
||||
|
||||
public function consume(AMQPConnection $serverConnection, WorkerInvoker $workerInvoker, $argQueueName, $maxProcesses)
|
||||
{
|
||||
$publisher = $this->messagePublisher;
|
||||
|
||||
$channel = $serverConnection->getChannel();
|
||||
|
||||
if ($channel == null) {
|
||||
$this->messagePublisher->pushLog("Can't connect to rabbit, check configuration!", "error");
|
||||
|
||||
return ;
|
||||
}
|
||||
|
||||
$serverConnection->declareExchange();
|
||||
|
||||
// define consume callbacks
|
||||
$callback = function (AMQPMessage $message) use ($channel, $workerInvoker, $publisher) {
|
||||
|
||||
$data = json_decode($message->getBody(), true);
|
||||
|
||||
$count = 0;
|
||||
|
||||
if ($message->has('application_headers')) {
|
||||
/** @var AMQPTable $headers */
|
||||
$headers = $message->get('application_headers');
|
||||
|
||||
$headerData = $headers->getNativeData();
|
||||
if (isset($headerData['x-death'])) {
|
||||
$xDeathHeader = $headerData['x-death'];
|
||||
|
||||
foreach ($xDeathHeader as $xdeath) {
|
||||
$queue = $xdeath['queue'];
|
||||
if (!in_array($queue, AMQPConnection::$defaultQueues)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$count = $xdeath['count'];
|
||||
$data['payload']['count'] = $count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if message is yet executed 3 times, save the unprocessed message in the corresponding failed queues
|
||||
if ($count > self::MAX_OF_TRY && $data['message_type'] != MessagePublisher::PULL_ASSETS_TYPE) {
|
||||
$this->messagePublisher->publishFailedMessage($data['payload'], $headers, AMQPConnection::$defaultFailedQueues[$data['message_type']]);
|
||||
|
||||
$logMessage = sprintf("Rabbit message executed 3 times, it's to be saved in %s , payload >>> %s",
|
||||
AMQPConnection::$defaultFailedQueues[$data['message_type']],
|
||||
json_encode($data['payload'])
|
||||
);
|
||||
$this->messagePublisher->pushLog($logMessage);
|
||||
|
||||
$channel->basic_ack($message->delivery_info['delivery_tag']);
|
||||
} else {
|
||||
try {
|
||||
$workerInvoker->invokeWorker($data['message_type'], json_encode($data['payload']));
|
||||
|
||||
if ($data['message_type'] == MessagePublisher::PULL_ASSETS_TYPE) {
|
||||
// make a loop for the pull assets
|
||||
$channel->basic_nack($message->delivery_info['delivery_tag']);
|
||||
} else {
|
||||
$channel->basic_ack($message->delivery_info['delivery_tag']);
|
||||
}
|
||||
|
||||
$oldPayload = $data['payload'];
|
||||
$message = $data['message_type'].' to be consumed! >> Payload ::'. json_encode($oldPayload);
|
||||
|
||||
$publisher->pushLog($message);
|
||||
} catch (\Exception $e) {
|
||||
$channel->basic_nack($message->delivery_info['delivery_tag']);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$prefetchCount = ProcessPool::MAX_PROCESSES;
|
||||
|
||||
if ($maxProcesses) {
|
||||
$prefetchCount = $maxProcesses;
|
||||
}
|
||||
|
||||
foreach (AMQPConnection::$defaultQueues as $queueName) {
|
||||
if ($argQueueName ) {
|
||||
if (in_array($queueName, $argQueueName)) {
|
||||
$serverConnection->setQueue($queueName);
|
||||
|
||||
// give prefetch message to a worker consumer at a time
|
||||
$channel->basic_qos(null, $prefetchCount, null);
|
||||
$channel->basic_consume($queueName, Uuid::uuid4(), false, false, false, false, $callback);
|
||||
}
|
||||
} else {
|
||||
$serverConnection->setQueue($queueName);
|
||||
|
||||
// give prefetch message to a worker consumer at a time
|
||||
$channel->basic_qos(null, $prefetchCount, null);
|
||||
$channel->basic_consume($queueName, Uuid::uuid4(), false, false, false, false, $callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
156
lib/Alchemy/Phrasea/WorkerManager/Queue/MessagePublisher.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Queue;
|
||||
|
||||
use Monolog\Logger;
|
||||
use PhpAmqpLib\Message\AMQPMessage;
|
||||
use PhpAmqpLib\Wire\AMQPTable;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class MessagePublisher
|
||||
{
|
||||
const EXPORT_MAIL_TYPE = 'exportMail';
|
||||
const SUBDEF_CREATION_TYPE = 'subdefCreation';
|
||||
const WRITE_METADATAS_TYPE = 'writeMetadatas';
|
||||
const ASSETS_INGEST_TYPE = 'assetsIngest';
|
||||
const CREATE_RECORD_TYPE = 'createRecord';
|
||||
const DELETE_RECORD_TYPE = 'deleteRecord';
|
||||
const WEBHOOK_TYPE = 'webhook';
|
||||
const POPULATE_INDEX_TYPE = 'populateIndex';
|
||||
const PULL_ASSETS_TYPE = 'pullAssets';
|
||||
|
||||
// worker queue to be consumed, when no ack , it is requeued to the retry queue
|
||||
const EXPORT_QUEUE = 'export-queue';
|
||||
const SUBDEF_QUEUE = 'subdef-queue';
|
||||
const METADATAS_QUEUE = 'metadatas-queue';
|
||||
const WEBHOOK_QUEUE = 'webhook-queue';
|
||||
const ASSETS_INGEST_QUEUE = 'ingest-queue';
|
||||
const CREATE_RECORD_QUEUE = 'createrecord-queue';
|
||||
const DELETE_RECORD_QUEUE = 'deleterecord-queue';
|
||||
const POPULATE_INDEX_QUEUE = 'populateindex-queue';
|
||||
const PULL_QUEUE = 'pull-queue';
|
||||
|
||||
// retry queue
|
||||
// we can use these retry queue with TTL, so when message expires it is requeued to the corresponding worker queue
|
||||
const RETRY_EXPORT_QUEUE = 'retry-export-queue';
|
||||
const RETRY_SUBDEF_QUEUE = 'retry-subdef-queue';
|
||||
const RETRY_METADATAS_QUEUE = 'retry-metadatas-queue';
|
||||
const RETRY_WEBHOOK_QUEUE = 'retry-webhook-queue';
|
||||
const RETRY_ASSETS_INGEST_QUEUE = 'retry-ingest-queue';
|
||||
const RETRY_CREATE_RECORD_QUEUE = 'retry-createrecord-queue';
|
||||
const RETRY_POPULATE_INDEX_QUEUE = 'retry-populateindex-queue';
|
||||
// use this queue to make a loop on a consumer
|
||||
const LOOP_PULL_QUEUE = 'loop-pull-queue';
|
||||
|
||||
// all failed queue, if message is treated over 3 times it goes to the failed queue
|
||||
const FAILED_EXPORT_QUEUE = 'failed-export-queue';
|
||||
const FAILED_SUBDEF_QUEUE = 'failed-subdef-queue';
|
||||
const FAILED_METADATAS_QUEUE = 'failed-metadatas-queue';
|
||||
const FAILED_WEBHOOK_QUEUE = 'failed-webhook-queue';
|
||||
const FAILED_ASSETS_INGEST_QUEUE = 'failed-ingest-queue';
|
||||
const FAILED_CREATE_RECORD_QUEUE = 'failed-createrecord-queue';
|
||||
const FAILED_POPULATE_INDEX_QUEUE = 'failed-populateindex-queue';
|
||||
|
||||
// delayed queue when record is locked
|
||||
const DELAYED_SUBDEF_QUEUE = 'delayed-subdef-queue';
|
||||
const DELAYED_METADATAS_QUEUE = 'delayed-metadatas-queue';
|
||||
|
||||
const NEW_RECORD_MESSAGE = 'newrecord';
|
||||
|
||||
|
||||
/** @var AMQPConnection $serverConnection */
|
||||
private $serverConnection;
|
||||
|
||||
/** @var Logger */
|
||||
private $logger;
|
||||
|
||||
public function __construct(AMQPConnection $serverConnection, LoggerInterface $logger)
|
||||
{
|
||||
$this->serverConnection = $serverConnection;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function publishMessage(array $payload, $queueName, $retryCount = null, $workerMessage = '')
|
||||
{
|
||||
// add published timestamp to all message payload
|
||||
$payload['payload']['published'] = time();
|
||||
$msg = new AMQPMessage(json_encode($payload));
|
||||
$routing = array_search($queueName, AMQPConnection::$defaultRetryQueues);
|
||||
|
||||
if (count($retryCount) && $routing != false) {
|
||||
// add a message header information
|
||||
$headers = new AMQPTable([
|
||||
'x-death' => [
|
||||
[
|
||||
'count' => $retryCount,
|
||||
'exchange' => AMQPConnection::ALCHEMY_EXCHANGE,
|
||||
'queue' => $routing,
|
||||
'routing-keys' => $routing,
|
||||
'reason' => 'rejected', // rejected is sended like nack
|
||||
'time' => new \DateTime('now', new \DateTimeZone('UTC'))
|
||||
]
|
||||
],
|
||||
'worker-message' => $workerMessage
|
||||
]);
|
||||
|
||||
$msg->set('application_headers', $headers);
|
||||
}
|
||||
|
||||
$channel = $this->serverConnection->setQueue($queueName);
|
||||
|
||||
if ($channel == null) {
|
||||
$this->pushLog("Can't connect to rabbit, check configuration!", "error");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$exchange = in_array($queueName, AMQPConnection::$defaultQueues) ? AMQPConnection::ALCHEMY_EXCHANGE : AMQPConnection::RETRY_ALCHEMY_EXCHANGE;
|
||||
$channel->basic_publish($msg, $exchange, $queueName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function initializePullAssets()
|
||||
{
|
||||
$payload = [
|
||||
'message_type' => self::PULL_ASSETS_TYPE,
|
||||
'payload' => [
|
||||
'initTimestamp' => new \DateTime('now', new \DateTimeZone('UTC'))
|
||||
]
|
||||
];
|
||||
|
||||
$this->publishMessage($payload, self::PULL_QUEUE);
|
||||
}
|
||||
|
||||
public function connectionClose()
|
||||
{
|
||||
$this->serverConnection->connectionClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $message
|
||||
* @param string $method
|
||||
* @param array $context
|
||||
*/
|
||||
public function pushLog($message, $method = 'info', $context = [])
|
||||
{
|
||||
// write logs directly in file
|
||||
|
||||
call_user_func(array($this->logger, $method), $message, $context);
|
||||
}
|
||||
|
||||
public function publishFailedMessage(array $payload, AMQPTable $headers, $queueName)
|
||||
{
|
||||
$msg = new AMQPMessage(json_encode($payload));
|
||||
$msg->set('application_headers', $headers);
|
||||
|
||||
$channel = $this->serverConnection->setQueue($queueName);
|
||||
if ($channel == null) {
|
||||
$this->pushLog("Can't connect to rabbit, check configuration!", "error");
|
||||
|
||||
return ;
|
||||
}
|
||||
|
||||
$channel->basic_publish($msg, AMQPConnection::RETRY_ALCHEMY_EXCHANGE, $queueName);
|
||||
}
|
||||
}
|
29
lib/Alchemy/Phrasea/WorkerManager/Queue/WebhookPublisher.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Queue;
|
||||
|
||||
use Alchemy\Phrasea\Model\Entities\WebhookEvent;
|
||||
use Alchemy\Phrasea\Webhook\WebhookPublisherInterface;
|
||||
|
||||
class WebhookPublisher implements WebhookPublisherInterface
|
||||
{
|
||||
/** @var MessagePublisher $messagePublisher */
|
||||
private $messagePublisher;
|
||||
|
||||
public function __construct(MessagePublisher $messagePublisher)
|
||||
{
|
||||
$this->messagePublisher = $messagePublisher;
|
||||
}
|
||||
|
||||
public function publishWebhookEvent(WebhookEvent $event)
|
||||
{
|
||||
$payload = [
|
||||
'message_type' => MessagePublisher::WEBHOOK_TYPE,
|
||||
'payload' => [
|
||||
'id' => $event->getId()
|
||||
]
|
||||
];
|
||||
|
||||
$this->messagePublisher->publishMessage($payload, MessagePublisher::WEBHOOK_QUEUE);
|
||||
}
|
||||
}
|
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Subscriber;
|
||||
|
||||
use Alchemy\Phrasea\Model\Entities\WorkerRunningUploader;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\AssetsCreateEvent;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\AssetsCreationFailureEvent;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\AssetsCreationRecordFailureEvent;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\WorkerEvents;
|
||||
use Alchemy\Phrasea\WorkerManager\Queue\MessagePublisher;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class AssetsIngestSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
/** @var MessagePublisher $messagePublisher */
|
||||
private $messagePublisher;
|
||||
|
||||
public function __construct(MessagePublisher $messagePublisher)
|
||||
{
|
||||
$this->messagePublisher = $messagePublisher;
|
||||
}
|
||||
|
||||
public function onAssetsCreate(AssetsCreateEvent $event)
|
||||
{
|
||||
// this is an uploader PUSH mode
|
||||
$payload = [
|
||||
'message_type' => MessagePublisher::ASSETS_INGEST_TYPE,
|
||||
'payload' => array_merge($event->getData(), ['type' => WorkerRunningUploader::TYPE_PUSH])
|
||||
];
|
||||
|
||||
|
||||
$this->messagePublisher->publishMessage($payload, MessagePublisher::ASSETS_INGEST_QUEUE);
|
||||
}
|
||||
|
||||
public function onAssetsCreationFailure(AssetsCreationFailureEvent $event)
|
||||
{
|
||||
$payload = [
|
||||
'message_type' => MessagePublisher::ASSETS_INGEST_TYPE,
|
||||
'payload' => $event->getPayload()
|
||||
];
|
||||
|
||||
$this->messagePublisher->publishMessage(
|
||||
$payload,
|
||||
MessagePublisher::RETRY_ASSETS_INGEST_QUEUE,
|
||||
$event->getCount(),
|
||||
$event->getWorkerMessage()
|
||||
);
|
||||
}
|
||||
|
||||
public function onAssetsCreationRecordFailure(AssetsCreationRecordFailureEvent $event)
|
||||
{
|
||||
$payload = [
|
||||
'message_type' => MessagePublisher::CREATE_RECORD_TYPE,
|
||||
'payload' => $event->getPayload()
|
||||
];
|
||||
|
||||
$this->messagePublisher->publishMessage(
|
||||
$payload,
|
||||
MessagePublisher::RETRY_CREATE_RECORD_QUEUE,
|
||||
$event->getCount(),
|
||||
$event->getWorkerMessage()
|
||||
);
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
WorkerEvents::ASSETS_CREATE => 'onAssetsCreate',
|
||||
WorkerEvents::ASSETS_CREATION_FAILURE => 'onAssetsCreationFailure',
|
||||
WorkerEvents::ASSETS_CREATION_RECORD_FAILURE => 'onAssetsCreationRecordFailure'
|
||||
];
|
||||
}
|
||||
}
|
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Subscriber;
|
||||
|
||||
use Alchemy\Phrasea\Core\Event\ExportMailEvent;
|
||||
use Alchemy\Phrasea\Core\PhraseaEvents;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\ExportMailFailureEvent;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\WorkerEvents;
|
||||
use Alchemy\Phrasea\WorkerManager\Queue\MessagePublisher;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class ExportSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
/** @var MessagePublisher $messagePublisher */
|
||||
private $messagePublisher;
|
||||
|
||||
public function __construct(MessagePublisher $messagePublisher)
|
||||
{
|
||||
$this->messagePublisher = $messagePublisher;
|
||||
}
|
||||
|
||||
public function onExportMailCreate(ExportMailEvent $event)
|
||||
{
|
||||
$payload = [
|
||||
'message_type' => MessagePublisher::EXPORT_MAIL_TYPE,
|
||||
'payload' => [
|
||||
'emitterUserId' => $event->getEmitterUserId(),
|
||||
'tokenValue' => $event->getTokenValue(),
|
||||
'destinationMails' => serialize($event->getDestinationMails()),
|
||||
'params' => serialize($event->getParams())
|
||||
]
|
||||
];
|
||||
|
||||
$this->messagePublisher->publishMessage($payload, MessagePublisher::EXPORT_QUEUE);
|
||||
}
|
||||
|
||||
public function onExportMailFailure(ExportMailFailureEvent $event)
|
||||
{
|
||||
$payload = [
|
||||
'message_type' => MessagePublisher::EXPORT_MAIL_TYPE,
|
||||
'payload' => [
|
||||
'emitterUserId' => $event->getEmitterUserId(),
|
||||
'tokenValue' => $event->getTokenValue(),
|
||||
'destinationMails' => serialize($event->getDestinationMails()),
|
||||
'params' => serialize($event->getParams())
|
||||
]
|
||||
];
|
||||
|
||||
$this->messagePublisher->publishMessage(
|
||||
$payload,
|
||||
MessagePublisher::RETRY_EXPORT_QUEUE,
|
||||
$event->getCount(),
|
||||
$event->getWorkerMessage()
|
||||
);
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
PhraseaEvents::EXPORT_MAIL_CREATE => 'onExportMailCreate',
|
||||
WorkerEvents::EXPORT_MAIL_FAILURE => 'onExportMailFailure'
|
||||
];
|
||||
}
|
||||
}
|
@@ -0,0 +1,331 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Subscriber;
|
||||
|
||||
use Alchemy\Phrasea\Application;
|
||||
use Alchemy\Phrasea\Core\Event\Record\DeletedEvent;
|
||||
use Alchemy\Phrasea\Core\Event\Record\DeleteEvent;
|
||||
use Alchemy\Phrasea\Core\Event\Record\MetadataChangedEvent;
|
||||
use Alchemy\Phrasea\Core\Event\Record\RecordEvent;
|
||||
use Alchemy\Phrasea\Core\Event\Record\RecordEvents;
|
||||
use Alchemy\Phrasea\Core\Event\Record\SubdefinitionCreateEvent;
|
||||
use Alchemy\Phrasea\Core\PhraseaTokens;
|
||||
use Alchemy\Phrasea\Databox\Subdef\MediaSubdefRepository;
|
||||
use Alchemy\Phrasea\Model\Entities\WorkerRunningJob;
|
||||
use Alchemy\Phrasea\Model\Repositories\WorkerRunningJobRepository;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\StoryCreateCoverEvent;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\SubdefinitionCreationFailureEvent;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\SubdefinitionWritemetaEvent;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\WorkerEvents;
|
||||
use Alchemy\Phrasea\WorkerManager\Queue\MessagePublisher;
|
||||
use Alchemy\Phrasea\WorkerManager\Worker\CreateRecordWorker;
|
||||
use Alchemy\Phrasea\WorkerManager\Worker\Factory\WorkerFactoryInterface;
|
||||
use Alchemy\Phrasea\WorkerManager\Worker\Resolver\TypeBasedWorkerResolver;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class RecordSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
/** @var MessagePublisher $messagePublisher */
|
||||
private $messagePublisher;
|
||||
|
||||
/** @var TypeBasedWorkerResolver $workerResolver*/
|
||||
private $workerResolver;
|
||||
|
||||
/** @var Application */
|
||||
private $app;
|
||||
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
private $appboxLocator;
|
||||
|
||||
public function __construct(Application $app, callable $appboxLocator)
|
||||
{
|
||||
$this->messagePublisher = $app['alchemy_worker.message.publisher'];
|
||||
$this->workerResolver = $app['alchemy_worker.type_based_worker_resolver'];
|
||||
$this->app = $app;
|
||||
$this->appboxLocator = $appboxLocator;
|
||||
}
|
||||
|
||||
public function onSubdefinitionCreate(SubdefinitionCreateEvent $event)
|
||||
{
|
||||
$record = $this->getApplicationBox()->get_databox($event->getRecord()->getDataboxId())->get_record($event->getRecord()->getRecordId());
|
||||
|
||||
if (!$record->isStory()) {
|
||||
$subdefs = $record->getDatabox()->get_subdef_structure()->getSubdefGroup($record->getType());
|
||||
|
||||
foreach ($subdefs as $subdef) {
|
||||
$payload = [
|
||||
'message_type' => MessagePublisher::SUBDEF_CREATION_TYPE,
|
||||
'payload' => [
|
||||
'recordId' => $event->getRecord()->getRecordId(),
|
||||
'databoxId' => $event->getRecord()->getDataboxId(),
|
||||
'subdefName' => $subdef->get_name(),
|
||||
'status' => $event->isNewRecord() ? MessagePublisher::NEW_RECORD_MESSAGE : ''
|
||||
]
|
||||
];
|
||||
|
||||
$this->messagePublisher->publishMessage($payload, MessagePublisher::SUBDEF_QUEUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onDelete(DeleteEvent $event)
|
||||
{
|
||||
// first remove record from the grid answer, so first delete the record in the index elastic
|
||||
$this->app['dispatcher']->dispatch(RecordEvents::DELETED, new DeletedEvent($event->getRecord()));
|
||||
|
||||
// publish payload to queue
|
||||
$payload = [
|
||||
'message_type' => MessagePublisher::DELETE_RECORD_TYPE,
|
||||
'payload' => [
|
||||
'recordId' => $event->getRecord()->getRecordId(),
|
||||
'databoxId' => $event->getRecord()->getDataboxId(),
|
||||
]
|
||||
];
|
||||
|
||||
$this->messagePublisher->publishMessage($payload, MessagePublisher::DELETE_RECORD_QUEUE);
|
||||
}
|
||||
|
||||
public function onSubdefinitionCreationFailure(SubdefinitionCreationFailureEvent $event)
|
||||
{
|
||||
$payload = [
|
||||
'message_type' => MessagePublisher::SUBDEF_CREATION_TYPE,
|
||||
'payload' => [
|
||||
'recordId' => $event->getRecord()->getRecordId(),
|
||||
'databoxId' => $event->getRecord()->getDataboxId(),
|
||||
'subdefName' => $event->getSubdefName(),
|
||||
'status' => ''
|
||||
]
|
||||
];
|
||||
|
||||
$repoWorker = $this->getRepoWorker();
|
||||
$em = $repoWorker->getEntityManager();
|
||||
$workerRunningJob = $repoWorker->findOneBy([
|
||||
'databoxId' => $event->getRecord()->getDataboxId(),
|
||||
'recordId' => $event->getRecord()->getRecordId(),
|
||||
'work' => PhraseaTokens::MAKE_SUBDEF,
|
||||
'workOn' => $event->getSubdefName()
|
||||
]);
|
||||
|
||||
$em->beginTransaction();
|
||||
try {
|
||||
$em->remove($workerRunningJob);
|
||||
$em->flush();
|
||||
$em->commit();
|
||||
} catch (\Exception $e) {
|
||||
$em->rollback();
|
||||
}
|
||||
|
||||
$this->messagePublisher->publishMessage(
|
||||
$payload,
|
||||
MessagePublisher::RETRY_SUBDEF_QUEUE,
|
||||
$event->getCount(),
|
||||
$event->getWorkerMessage()
|
||||
);
|
||||
}
|
||||
|
||||
public function onRecordCreated(RecordEvent $event)
|
||||
{
|
||||
$this->messagePublisher->pushLog(sprintf('The %s= %d was successfully created',
|
||||
($event->getRecord()->isStory() ? "story story_id" : "record record_id"),
|
||||
$event->getRecord()->getRecordId()
|
||||
));
|
||||
}
|
||||
|
||||
public function onMetadataChanged(MetadataChangedEvent $event)
|
||||
{
|
||||
$databoxId = $event->getRecord()->getDataboxId();
|
||||
$recordId = $event->getRecord()->getRecordId();
|
||||
|
||||
$mediaSubdefRepository = $this->getMediaSubdefRepository($databoxId);
|
||||
$mediaSubdefs = $mediaSubdefRepository->findByRecordIdsAndNames([$recordId]);
|
||||
|
||||
$databox = $this->getApplicationBox()->get_databox($databoxId);
|
||||
$record = $databox->get_record($recordId);
|
||||
$type = $record->getType();
|
||||
|
||||
foreach ($mediaSubdefs as $subdef) {
|
||||
// check subdefmetadatarequired from the subview setup in admin
|
||||
if ( $subdef->get_name() == 'document' || $this->isSubdefMetadataUpdateRequired($databox, $type, $subdef->get_name())) {
|
||||
if ($subdef->is_physically_present()) {
|
||||
$payload = [
|
||||
'message_type' => MessagePublisher::WRITE_METADATAS_TYPE,
|
||||
'payload' => [
|
||||
'recordId' => $recordId,
|
||||
'databoxId' => $databoxId,
|
||||
'subdefName' => $subdef->get_name()
|
||||
]
|
||||
];
|
||||
|
||||
$this->messagePublisher->publishMessage($payload, MessagePublisher::METADATAS_QUEUE);
|
||||
} else {
|
||||
$payload = [
|
||||
'message_type' => MessagePublisher::WRITE_METADATAS_TYPE,
|
||||
'payload' => [
|
||||
'recordId' => $recordId,
|
||||
'databoxId' => $databoxId,
|
||||
'subdefName' => $subdef->get_name()
|
||||
]
|
||||
];
|
||||
|
||||
$logMessage = sprintf("Subdef %s is not physically present! to be passed in the %s ! payload >>> %s",
|
||||
$subdef->get_name(),
|
||||
MessagePublisher::RETRY_METADATAS_QUEUE,
|
||||
json_encode($payload)
|
||||
);
|
||||
$this->messagePublisher->pushLog($logMessage);
|
||||
|
||||
$this->messagePublisher->publishMessage(
|
||||
$payload,
|
||||
MessagePublisher::RETRY_METADATAS_QUEUE,
|
||||
2,
|
||||
'Subdef is not physically present!'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function onStoryCreateCover(StoryCreateCoverEvent $event)
|
||||
{
|
||||
/** @var WorkerFactoryInterface[] $factories */
|
||||
$factories = $this->workerResolver->getFactories();
|
||||
|
||||
/** @var CreateRecordWorker $createRecordWorker */
|
||||
$createRecordWorker = $factories[MessagePublisher::CREATE_RECORD_TYPE]->createWorker();
|
||||
|
||||
$createRecordWorker->setStoryCover($event->getData());
|
||||
}
|
||||
|
||||
public function onSubdefinitionWritemeta(SubdefinitionWritemetaEvent $event)
|
||||
{
|
||||
if ($event->getStatus() == SubdefinitionWritemetaEvent::FAILED) {
|
||||
$payload = [
|
||||
'message_type' => MessagePublisher::WRITE_METADATAS_TYPE,
|
||||
'payload' => [
|
||||
'recordId' => $event->getRecord()->getRecordId(),
|
||||
'databoxId' => $event->getRecord()->getDataboxId(),
|
||||
'subdefName' => $event->getSubdefName()
|
||||
]
|
||||
];
|
||||
|
||||
$logMessage = sprintf("Subdef %s write meta failed, error : %s ! to be passed in the %s ! payload >>> %s",
|
||||
$event->getSubdefName(),
|
||||
$event->getWorkerMessage(),
|
||||
MessagePublisher::RETRY_METADATAS_QUEUE,
|
||||
json_encode($payload)
|
||||
);
|
||||
$this->messagePublisher->pushLog($logMessage);
|
||||
|
||||
$jeton = ($event->getSubdefName() == "document") ? PhraseaTokens::WRITE_META_DOC : PhraseaTokens::WRITE_META_SUBDEF;
|
||||
|
||||
$repoWorker = $this->getRepoWorker();
|
||||
$em = $repoWorker->getEntityManager();
|
||||
$workerRunningJob = $repoWorker->findOneBy([
|
||||
'databoxId' => $event->getRecord()->getDataboxId(),
|
||||
'recordId' => $event->getRecord()->getRecordId(),
|
||||
'work' => $jeton,
|
||||
'workOn' => $event->getSubdefName()
|
||||
]);
|
||||
|
||||
$em->beginTransaction();
|
||||
try {
|
||||
$em->remove($workerRunningJob);
|
||||
$em->flush();
|
||||
$em->commit();
|
||||
} catch (\Exception $e) {
|
||||
$em->rollback();
|
||||
}
|
||||
|
||||
$this->messagePublisher->publishMessage(
|
||||
$payload,
|
||||
MessagePublisher::RETRY_METADATAS_QUEUE,
|
||||
$event->getCount(),
|
||||
$event->getWorkerMessage()
|
||||
);
|
||||
|
||||
} else {
|
||||
$databoxId = $event->getRecord()->getDataboxId();
|
||||
$recordId = $event->getRecord()->getRecordId();
|
||||
|
||||
$databox = $this->getApplicationBox()->get_databox($databoxId);
|
||||
$record = $databox->get_record($recordId);
|
||||
$type = $record->getType();
|
||||
|
||||
$subdef = $record->get_subdef($event->getSubdefName());
|
||||
|
||||
// only the required writemetadata from admin > subview setup is to be writing
|
||||
if ($subdef->get_name() == 'document' || $this->isSubdefMetadataUpdateRequired($databox, $type, $subdef->get_name())) {
|
||||
$payload = [
|
||||
'message_type' => MessagePublisher::WRITE_METADATAS_TYPE,
|
||||
'payload' => [
|
||||
'recordId' => $recordId,
|
||||
'databoxId' => $databoxId,
|
||||
'subdefName' => $event->getSubdefName()
|
||||
]
|
||||
];
|
||||
|
||||
$this->messagePublisher->publishMessage($payload, MessagePublisher::METADATAS_QUEUE);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
RecordEvents::CREATED => 'onRecordCreated',
|
||||
RecordEvents::SUBDEFINITION_CREATE => 'onSubdefinitionCreate',
|
||||
RecordEvents::DELETE => 'onDelete',
|
||||
WorkerEvents::SUBDEFINITION_CREATION_FAILURE => 'onSubdefinitionCreationFailure',
|
||||
RecordEvents::METADATA_CHANGED => 'onMetadataChanged',
|
||||
WorkerEvents::STORY_CREATE_COVER => 'onStoryCreateCover',
|
||||
WorkerEvents::SUBDEFINITION_WRITE_META => 'onSubdefinitionWritemeta'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $databoxId
|
||||
*
|
||||
* @return MediaSubdefRepository
|
||||
*/
|
||||
private function getMediaSubdefRepository($databoxId)
|
||||
{
|
||||
return $this->app['provider.repo.media_subdef']->getRepositoryForDatabox($databoxId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \databox $databox
|
||||
* @param string $subdefType
|
||||
* @param string $subdefName
|
||||
* @return bool
|
||||
*/
|
||||
private function isSubdefMetadataUpdateRequired(\databox $databox, $subdefType, $subdefName)
|
||||
{
|
||||
if ($databox->get_subdef_structure()->hasSubdef($subdefType, $subdefName)) {
|
||||
return $databox->get_subdef_structure()->get_subdef($subdefType, $subdefName)->isMetadataUpdateRequired();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \appbox
|
||||
*/
|
||||
private function getApplicationBox()
|
||||
{
|
||||
$callable = $this->appboxLocator;
|
||||
|
||||
return $callable();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return WorkerRunningJobRepository
|
||||
*/
|
||||
private function getRepoWorker()
|
||||
{
|
||||
return $this->app['repo.worker-running-job'];
|
||||
}
|
||||
}
|
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Subscriber;
|
||||
|
||||
use Alchemy\Phrasea\WorkerManager\Event\PopulateIndexEvent;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\PopulateIndexFailureEvent;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\WorkerEvents;
|
||||
use Alchemy\Phrasea\WorkerManager\Queue\MessagePublisher;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class SearchengineSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
/** @var MessagePublisher $messagePublisher */
|
||||
private $messagePublisher;
|
||||
|
||||
public function __construct(MessagePublisher $messagePublisher)
|
||||
{
|
||||
$this->messagePublisher = $messagePublisher;
|
||||
}
|
||||
|
||||
public function onPopulateIndex(PopulateIndexEvent $event)
|
||||
{
|
||||
$populateInfo = $event->getData();
|
||||
|
||||
// make payload per databoxId
|
||||
foreach ($populateInfo['databoxIds'] as $databoxId) {
|
||||
$payload = [
|
||||
'message_type' => MessagePublisher::POPULATE_INDEX_TYPE,
|
||||
'payload' => [
|
||||
'host' => $populateInfo['host'],
|
||||
'port' => $populateInfo['port'],
|
||||
'indexName' => $populateInfo['indexName'],
|
||||
'databoxId' => $databoxId
|
||||
]
|
||||
];
|
||||
|
||||
$this->messagePublisher->publishMessage($payload, MessagePublisher::POPULATE_INDEX_QUEUE);
|
||||
}
|
||||
}
|
||||
|
||||
public function onPopulateIndexFailure(PopulateIndexFailureEvent $event)
|
||||
{
|
||||
$payload = [
|
||||
'message_type' => MessagePublisher::POPULATE_INDEX_TYPE,
|
||||
'payload' => [
|
||||
'host' => $event->getHost(),
|
||||
'port' => $event->getPort(),
|
||||
'indexName' => $event->getIndexName(),
|
||||
'databoxId' => $event->getDataboxId(),
|
||||
]
|
||||
];
|
||||
|
||||
$this->messagePublisher->publishMessage(
|
||||
$payload,
|
||||
MessagePublisher::RETRY_POPULATE_INDEX_QUEUE,
|
||||
$event->getCount(),
|
||||
$event->getWorkerMessage()
|
||||
);
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
WorkerEvents::POPULATE_INDEX => 'onPopulateIndex',
|
||||
WorkerEvents::POPULATE_INDEX_FAILURE => 'onPopulateIndexFailure'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Subscriber;
|
||||
|
||||
use Alchemy\Phrasea\WorkerManager\Event\WebhookDeliverFailureEvent;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\WorkerEvents;
|
||||
use Alchemy\Phrasea\WorkerManager\Queue\MessagePublisher;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class WebhookSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
/** @var MessagePublisher $messagePublisher */
|
||||
private $messagePublisher;
|
||||
|
||||
public function __construct(MessagePublisher $messagePublisher)
|
||||
{
|
||||
$this->messagePublisher = $messagePublisher;
|
||||
}
|
||||
|
||||
public function onWebhookDeliverFailure(WebhookDeliverFailureEvent $event)
|
||||
{
|
||||
// count = 0 mean do not retry because no api application defined
|
||||
if ($event->getCount() != 0) {
|
||||
$payload = [
|
||||
'message_type' => MessagePublisher::WEBHOOK_TYPE,
|
||||
'payload' => [
|
||||
'id' => $event->getWebhookEventId(),
|
||||
'delivery_id' => $event->getDeleveryId(),
|
||||
]
|
||||
];
|
||||
|
||||
$this->messagePublisher->publishMessage(
|
||||
$payload,
|
||||
MessagePublisher::RETRY_WEBHOOK_QUEUE,
|
||||
$event->getCount(),
|
||||
$event->getWorkerMessage()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
WorkerEvents::WEBHOOK_DELIVER_FAILURE => 'onWebhookDeliverFailure',
|
||||
];
|
||||
}
|
||||
}
|
166
lib/Alchemy/Phrasea/WorkerManager/Worker/AssetsIngestWorker.php
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Worker;
|
||||
|
||||
use Alchemy\Phrasea\Application\Helper\EntityManagerAware;
|
||||
use Alchemy\Phrasea\Application as PhraseaApplication;
|
||||
use Alchemy\Phrasea\Model\Entities\StoryWZ;
|
||||
use Alchemy\Phrasea\Model\Entities\WorkerRunningUploader;
|
||||
use Alchemy\Phrasea\Model\Repositories\UserRepository;
|
||||
use Alchemy\Phrasea\Model\Repositories\WorkerRunningUploaderRepository;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\AssetsCreationFailureEvent;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\WorkerEvents;
|
||||
use Alchemy\Phrasea\WorkerManager\Queue\MessagePublisher;
|
||||
use GuzzleHttp\Client;
|
||||
|
||||
class AssetsIngestWorker implements WorkerInterface
|
||||
{
|
||||
use EntityManagerAware;
|
||||
|
||||
private $app;
|
||||
|
||||
/** @var MessagePublisher $messagePublisher */
|
||||
private $messagePublisher;
|
||||
|
||||
/** @var WorkerRunningUploaderRepository $repoWorkerUploader */
|
||||
private $repoWorkerUploader;
|
||||
|
||||
public function __construct(PhraseaApplication $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
$this->messagePublisher = $this->app['alchemy_worker.message.publisher'];
|
||||
}
|
||||
|
||||
public function process(array $payload)
|
||||
{
|
||||
$assets = $payload['assets'];
|
||||
$this->repoWorkerUploader = $this->getWorkerRunningUploaderRepository();
|
||||
|
||||
$this->saveAssetsList($payload['commit_id'], $assets, $payload['published'], $payload['type']);
|
||||
|
||||
$uploaderClient = new Client(['base_uri' => $payload['base_url']]);
|
||||
|
||||
//get first asset informations to check if it's a story
|
||||
try {
|
||||
$body = $uploaderClient->get('/assets/'.$assets[0], [
|
||||
'headers' => [
|
||||
'Authorization' => 'AssetToken '.$payload['token']
|
||||
]
|
||||
])->getBody()->getContents();
|
||||
} catch(\Exception $e) {
|
||||
$count = isset($payload['count']) ? $payload['count'] + 1 : 2 ;
|
||||
|
||||
$this->app['dispatcher']->dispatch(WorkerEvents::ASSETS_CREATION_FAILURE, new AssetsCreationFailureEvent(
|
||||
$payload,
|
||||
'Error when getting assets information !' . $e->getMessage(),
|
||||
$count
|
||||
));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$body = json_decode($body,true);
|
||||
|
||||
$storyId = null;
|
||||
|
||||
if (!empty($body['formData']['is_story'])) {
|
||||
$storyId = $this->createStory($body);
|
||||
}
|
||||
|
||||
foreach ($assets as $assetId) {
|
||||
$createRecordMessage['message_type'] = MessagePublisher::CREATE_RECORD_TYPE;
|
||||
$createRecordMessage['payload'] = [
|
||||
'asset' => $assetId,
|
||||
'publisher' => $payload['publisher'],
|
||||
'assetToken' => $payload['token'],
|
||||
'storyId' => $storyId,
|
||||
'base_url' => $payload['base_url'],
|
||||
'commit_id' => $payload['commit_id']
|
||||
];
|
||||
|
||||
$this->messagePublisher->publishMessage($createRecordMessage, MessagePublisher::CREATE_RECORD_QUEUE);
|
||||
}
|
||||
}
|
||||
|
||||
private function createStory(array $body)
|
||||
{
|
||||
$storyId = null;
|
||||
|
||||
$userRepository = $this->getUserRepository();
|
||||
$user = null;
|
||||
|
||||
if (!empty($body['formData']['phraseanet_submiter_email'])) {
|
||||
$user = $userRepository->findByEmail($body['formData']['phraseanet_submiter_email']);
|
||||
}
|
||||
|
||||
if ($user === null && !empty($body['formData']['phraseanet_user_submiter_id'])) {
|
||||
$user = $userRepository->find($body['formData']['phraseanet_user_submiter_id']);
|
||||
}
|
||||
|
||||
if ($user !== null) {
|
||||
$base_id = $body['formData']['collection_destination'];
|
||||
|
||||
$collection = \collection::getByBaseId($this->app, $base_id);
|
||||
|
||||
$story = \record_adapter::createStory($this->app, $collection);
|
||||
$storyId = $story->getRecordId();
|
||||
|
||||
$storyWZ = new StoryWZ();
|
||||
|
||||
$storyWZ->setUser($user);
|
||||
$storyWZ->setRecord($story);
|
||||
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($storyWZ);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
return $storyId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UserRepository
|
||||
*/
|
||||
private function getUserRepository()
|
||||
{
|
||||
return $this->app['repo.users'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return WorkerRunningUploaderRepository
|
||||
*/
|
||||
private function getWorkerRunningUploaderRepository()
|
||||
{
|
||||
return $this->app['repo.worker-running-uploader'];
|
||||
}
|
||||
|
||||
private function saveAssetsList($commitId, $assetsId, $published, $type)
|
||||
{
|
||||
$em = $this->repoWorkerUploader->getEntityManager();
|
||||
$em->beginTransaction();
|
||||
$date = new \DateTime();
|
||||
|
||||
try {
|
||||
foreach ($assetsId as $assetId) {
|
||||
$workerRunningUploader = new WorkerRunningUploader();
|
||||
$workerRunningUploader
|
||||
->setCommitId($commitId)
|
||||
->setAssetId($assetId)
|
||||
->setPublished($date->setTimestamp($published))
|
||||
->setStatus(WorkerRunningUploader::RUNNING)
|
||||
->setType($type)
|
||||
;
|
||||
|
||||
$em->persist($workerRunningUploader);
|
||||
|
||||
unset($workerRunningUploader);
|
||||
}
|
||||
|
||||
$em->flush();
|
||||
|
||||
$em->commit();
|
||||
} catch(\Exception $e) {
|
||||
$em->rollback();
|
||||
}
|
||||
}
|
||||
}
|
346
lib/Alchemy/Phrasea/WorkerManager/Worker/CreateRecordWorker.php
Normal file
@@ -0,0 +1,346 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Worker;
|
||||
|
||||
use Alchemy\Phrasea\Application\Helper\ApplicationBoxAware;
|
||||
use Alchemy\Phrasea\Application\Helper\EntityManagerAware;
|
||||
use Alchemy\Phrasea\Application\Helper\BorderManagerAware;
|
||||
use Alchemy\Phrasea\Application\Helper\DispatcherAware;
|
||||
use Alchemy\Phrasea\Application\Helper\FilesystemAware;
|
||||
use Alchemy\Phrasea\Application as PhraseaApplication;
|
||||
use Alchemy\Phrasea\Border\Attribute\MetaField;
|
||||
use Alchemy\Phrasea\Border\Attribute\Status;
|
||||
use Alchemy\Phrasea\Border\File;
|
||||
use Alchemy\Phrasea\Border\Visa;
|
||||
use Alchemy\Phrasea\Core\Event\LazaretEvent;
|
||||
use Alchemy\Phrasea\Core\Event\RecordEdit;
|
||||
use Alchemy\Phrasea\Core\PhraseaEvents;
|
||||
use Alchemy\Phrasea\Media\SubdefSubstituer;
|
||||
use Alchemy\Phrasea\Model\Entities\LazaretFile;
|
||||
use Alchemy\Phrasea\Model\Entities\LazaretSession;
|
||||
use Alchemy\Phrasea\Model\Entities\User;
|
||||
use Alchemy\Phrasea\Model\Entities\WorkerRunningUploader;
|
||||
use Alchemy\Phrasea\Model\Repositories\UserRepository;
|
||||
use Alchemy\Phrasea\Model\Repositories\WorkerRunningUploaderRepository;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\AssetsCreationRecordFailureEvent;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\WorkerEvents;
|
||||
use Alchemy\Phrasea\WorkerManager\Queue\MessagePublisher;
|
||||
use GuzzleHttp\Client;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
class CreateRecordWorker implements WorkerInterface
|
||||
{
|
||||
use ApplicationBoxAware;
|
||||
use EntityManagerAware;
|
||||
use BorderManagerAware;
|
||||
use DispatcherAware;
|
||||
use FilesystemAware;
|
||||
|
||||
private $app;
|
||||
private $logger;
|
||||
|
||||
/** @var MessagePublisher $messagePublisher */
|
||||
private $messagePublisher;
|
||||
|
||||
/** @var WorkerRunningUploaderRepository $repoWorkerUploader */
|
||||
private $repoWorkerUploader;
|
||||
|
||||
public function __construct(PhraseaApplication $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
$this->logger = $this->app['alchemy_worker.logger'];
|
||||
$this->messagePublisher = $this->app['alchemy_worker.message.publisher'];
|
||||
}
|
||||
|
||||
public function process(array $payload)
|
||||
{
|
||||
$this->repoWorkerUploader = $this->getWorkerRunningUploaderRepository();
|
||||
$em = $this->repoWorkerUploader->getEntityManager();
|
||||
|
||||
$uploaderClient = new Client(['base_uri' => $payload['base_url']]);
|
||||
|
||||
//get asset informations
|
||||
$body = $uploaderClient->get('/assets/'.$payload['asset'], [
|
||||
'headers' => [
|
||||
'Authorization' => 'AssetToken '.$payload['assetToken']
|
||||
]
|
||||
])->getBody()->getContents();
|
||||
|
||||
$body = json_decode($body,true);
|
||||
|
||||
$tempfile = $this->getTemporaryFilesystem()->createTemporaryFile('download_', null, pathinfo($body['originalName'], PATHINFO_EXTENSION));
|
||||
|
||||
|
||||
/** @var WorkerRunningUploader $workerRunningUploader */
|
||||
$workerRunningUploader = $this->repoWorkerUploader->findOneBy([
|
||||
'commitId' => $payload['commit_id'],
|
||||
'assetId' => $payload['asset']
|
||||
]);
|
||||
|
||||
//download the asset
|
||||
try {
|
||||
$res = $uploaderClient->get('/assets/'.$payload['asset'].'/download', [
|
||||
'headers' => [
|
||||
'Authorization' => 'AssetToken '.$payload['assetToken']
|
||||
],
|
||||
'save_to' => $tempfile
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$count = isset($payload['count']) ? $payload['count'] + 1 : 2 ;
|
||||
|
||||
// send to retry queue
|
||||
$this->dispatch(WorkerEvents::ASSETS_CREATION_RECORD_FAILURE, new AssetsCreationRecordFailureEvent(
|
||||
$payload,
|
||||
'Error when downloading assets!',
|
||||
$count
|
||||
));
|
||||
|
||||
$em->remove($workerRunningUploader);
|
||||
$em->flush();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if ($res->getStatusCode() !== 200) {
|
||||
$workerMessage = sprintf('Error %s downloading "%s"', $res->getStatusCode(), $payload['base_url'].'/assets/'.$payload['asset'].'/download');
|
||||
$this->logger->error($workerMessage);
|
||||
|
||||
$count = isset($payload['count']) ? $payload['count'] + 1 : 2 ;
|
||||
|
||||
// send to retry queue
|
||||
$this->dispatch(WorkerEvents::ASSETS_CREATION_RECORD_FAILURE, new AssetsCreationRecordFailureEvent(
|
||||
$payload,
|
||||
$workerMessage,
|
||||
$count
|
||||
));
|
||||
|
||||
$em->remove($workerRunningUploader);
|
||||
$em->flush();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($workerRunningUploader != null) {
|
||||
$em->beginTransaction();
|
||||
try {
|
||||
$workerRunningUploader->setStatus(WorkerRunningUploader::DOWNLOADED);
|
||||
|
||||
$em->persist($workerRunningUploader);
|
||||
|
||||
$em->flush();
|
||||
$em->commit();
|
||||
} catch (\Exception $e) {
|
||||
$em->rollback();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$canAck = $this->repoWorkerUploader->canAck($payload['commit_id']);
|
||||
|
||||
// if all assets in the commit are downloaded , send ack to the uploader
|
||||
if ($canAck) {
|
||||
// post ack to the uploader
|
||||
$uploaderClient->post('/commits/' . $payload['commit_id'] . '/ack', [
|
||||
'headers' => [
|
||||
'Authorization' => 'AssetToken '.$payload['assetToken']
|
||||
],
|
||||
'json' => [
|
||||
'acknowledged' => true
|
||||
]
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$lazaretSession = new LazaretSession();
|
||||
|
||||
$userRepository = $this->getUserRepository();
|
||||
$user = null;
|
||||
|
||||
if (!empty($body['formData']['phraseanet_submiter_email'])) {
|
||||
$user = $userRepository->findByEmail($body['formData']['phraseanet_submiter_email']);
|
||||
}
|
||||
|
||||
if ($user === null && !empty($body['formData']['phraseanet_user_submiter_id'])) {
|
||||
$user = $userRepository->find($body['formData']['phraseanet_user_submiter_id']);
|
||||
}
|
||||
|
||||
if ($user !== null) {
|
||||
$lazaretSession->setUser($user);
|
||||
}
|
||||
|
||||
$this->getEntityManager()->persist($lazaretSession);
|
||||
|
||||
|
||||
$renamedFilename = $tempfile;
|
||||
$media = $this->app->getMediaFromUri($renamedFilename);
|
||||
|
||||
if (!isset($body['formData']['collection_destination'])) {
|
||||
$this->messagePublisher->pushLog("The collection_destination is not defined");
|
||||
|
||||
return ;
|
||||
}
|
||||
|
||||
$base_id = $body['formData']['collection_destination'];
|
||||
$collection = \collection::getByBaseId($this->app, $base_id);
|
||||
$sbasId = $collection->get_sbas_id();
|
||||
|
||||
$packageFile = new File($this->app, $media, $collection, $body['originalName']);
|
||||
|
||||
// get metadata and status
|
||||
$statusbit = null;
|
||||
foreach ($body['formData'] as $key => $value) {
|
||||
if (strstr($key, 'metadata')) {
|
||||
$tMeta = explode('-', $key);
|
||||
|
||||
$metaField = $collection->get_databox()->get_meta_structure()->get_element($tMeta[1]);
|
||||
|
||||
$packageFile->addAttribute(new MetaField($metaField, [$value]));
|
||||
}
|
||||
|
||||
if (strstr($key, 'statusbit')) {
|
||||
$tStatus = explode('-', $key);
|
||||
$statusbit[$tStatus[1]] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_null($statusbit)) {
|
||||
$status = '';
|
||||
foreach (range(0, 31) as $i) {
|
||||
$status .= isset($statusbit[$i]) ? ($statusbit[$i] ? '1' : '0') : '0';
|
||||
}
|
||||
$packageFile->addAttribute(new Status($this->app, strrev($status)));
|
||||
}
|
||||
|
||||
$reasons = [];
|
||||
$elementCreated = null;
|
||||
|
||||
$callback = function ($element, Visa $visa) use (&$reasons, &$elementCreated) {
|
||||
foreach ($visa->getResponses() as $response) {
|
||||
if (!$response->isOk()) {
|
||||
$reasons[] = $response->getMessage($this->app['translator']);
|
||||
}
|
||||
}
|
||||
|
||||
$elementCreated = $element;
|
||||
};
|
||||
|
||||
$this->getBorderManager()->process($lazaretSession, $packageFile, $callback);
|
||||
|
||||
|
||||
if ($elementCreated instanceof \record_adapter) {
|
||||
$this->dispatch(PhraseaEvents::RECORD_UPLOAD, new RecordEdit($elementCreated));
|
||||
} else {
|
||||
$this->messagePublisher->pushLog(sprintf('The file was moved to the quarantine: %s', json_encode($reasons)));
|
||||
/** @var LazaretFile $elementCreated */
|
||||
$this->dispatch(PhraseaEvents::LAZARET_CREATE, new LazaretEvent($elementCreated));
|
||||
}
|
||||
|
||||
// add record in a story if story is defined
|
||||
|
||||
if (is_int($payload['storyId']) && $elementCreated instanceof \record_adapter) {
|
||||
$this->addRecordInStory($user, $elementCreated, $sbasId, $payload['storyId'], $body['formData']);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $data databoxId_storyId_recordId subdefName
|
||||
*/
|
||||
public function setStoryCover($data)
|
||||
{
|
||||
// get databoxId , storyId , recordId
|
||||
$tData = explode('_', $data);
|
||||
|
||||
$record = $this->findDataboxById($tData[0])->get_record($tData[2]);
|
||||
|
||||
$story = $this->findDataboxById($tData[0])->get_record($tData[1]);
|
||||
$subdefName = $tData[3];
|
||||
|
||||
$subdef = $record->get_subdef($tData[3]);
|
||||
$media = $this->app->getMediaFromUri($subdef->getRealPath());
|
||||
$this->getSubdefSubstituer()->substituteSubdef($story, $subdefName, $media); // subdefName = thumbnail | preview
|
||||
|
||||
$this->messagePublisher->pushLog(sprintf("Cover %s set for story story_id= %d with the record record_id = %d", $subdefName, $story->getRecordId(), $record->getRecordId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $user
|
||||
* @param \record_adapter $elementCreated
|
||||
* @param $sbasId
|
||||
* @param $storyId
|
||||
* @param $formData
|
||||
*/
|
||||
private function addRecordInStory($user, $elementCreated, $sbasId, $storyId, $formData)
|
||||
{
|
||||
$story = new \record_adapter($this->app, $sbasId, $storyId);
|
||||
|
||||
if (!$this->getAclForUser($user)->has_right_on_base($story->getBaseId(), \ACL::CANMODIFRECORD)) {
|
||||
$this->messagePublisher->pushLog(sprintf("The user %s can not add document to the story story_id = %d", $user->getLogin(), $story->getRecordId()));
|
||||
|
||||
throw new AccessDeniedHttpException('You can not add document to this Story');
|
||||
}
|
||||
|
||||
if (!$story->hasChild($elementCreated)) {
|
||||
$story->appendChild($elementCreated);
|
||||
|
||||
if (SubdefCreationWorker::checkIfFirstChild($story, $elementCreated)) {
|
||||
// add metadata to the story
|
||||
$metadatas = [];
|
||||
foreach ($formData as $key => $value) {
|
||||
if (strstr($key, 'metadata')) {
|
||||
$tMeta = explode('-', $key);
|
||||
|
||||
$metaField = $elementCreated->getDatabox()->get_meta_structure()->get_element($tMeta[1]);
|
||||
|
||||
$metadatas[] = [
|
||||
'meta_struct_id' => $metaField->get_id(),
|
||||
'meta_id' => null,
|
||||
'value' => $value,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$story->set_metadatas($metadatas)->rebuild_subdefs();
|
||||
}
|
||||
|
||||
$this->messagePublisher->pushLog(sprintf('The record record_id= %d was successfully added in the story record_id= %d', $elementCreated->getRecordId(), $story->getRecordId()));
|
||||
$this->dispatch(PhraseaEvents::RECORD_EDIT, new RecordEdit($story));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UserRepository
|
||||
*/
|
||||
private function getUserRepository()
|
||||
{
|
||||
return $this->app['repo.users'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @return \ACL
|
||||
*/
|
||||
private function getAclForUser(User $user)
|
||||
{
|
||||
$aclProvider = $this->app['acl'];
|
||||
|
||||
return $aclProvider->get($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SubdefSubstituer
|
||||
*/
|
||||
private function getSubdefSubstituer()
|
||||
{
|
||||
return $this->app['subdef.substituer'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return WorkerRunningUploaderRepository
|
||||
*/
|
||||
private function getWorkerRunningUploaderRepository()
|
||||
{
|
||||
return $this->app['repo.worker-running-uploader'];
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Worker;
|
||||
|
||||
use Alchemy\Phrasea\Application\Helper\ApplicationBoxAware;
|
||||
|
||||
class DeleteRecordWorker implements WorkerInterface
|
||||
{
|
||||
use ApplicationBoxAware;
|
||||
|
||||
public function process(array $payload)
|
||||
{
|
||||
$record = $this->findDataboxById($payload['databoxId'])->get_record($payload['recordId']);
|
||||
|
||||
$record->delete();
|
||||
}
|
||||
}
|
102
lib/Alchemy/Phrasea/WorkerManager/Worker/ExportMailWorker.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Worker;
|
||||
|
||||
use Alchemy\Phrasea\Application;
|
||||
use Alchemy\Phrasea\Core\Event\ExportFailureEvent;
|
||||
use Alchemy\Phrasea\Core\PhraseaEvents;
|
||||
use Alchemy\Phrasea\Exception\InvalidArgumentException;
|
||||
use Alchemy\Phrasea\Model\Entities\Token;
|
||||
use Alchemy\Phrasea\Model\Repositories\TokenRepository;
|
||||
use Alchemy\Phrasea\Model\Repositories\UserRepository;
|
||||
use Alchemy\Phrasea\Notification\Emitter;
|
||||
use Alchemy\Phrasea\Notification\Mail\MailRecordsExport;
|
||||
use Alchemy\Phrasea\Notification\Receiver;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\ExportMailFailureEvent;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\WorkerEvents;
|
||||
|
||||
class ExportMailWorker implements WorkerInterface
|
||||
{
|
||||
use Application\Helper\NotifierAware;
|
||||
|
||||
private $app;
|
||||
|
||||
public function __construct(Application $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
}
|
||||
|
||||
public function process(array $payload)
|
||||
{
|
||||
$destMails = unserialize($payload['destinationMails']);
|
||||
|
||||
$params = unserialize($payload['params']);
|
||||
|
||||
/** @var UserRepository $userRepository */
|
||||
$userRepository = $this->app['repo.users'];
|
||||
|
||||
$user = $userRepository->find($payload['emitterUserId']);
|
||||
|
||||
/** @var TokenRepository $tokenRepository */
|
||||
$tokenRepository = $this->app['repo.tokens'];
|
||||
|
||||
/** @var Token $token */
|
||||
$token = $tokenRepository->findValidToken($payload['tokenValue']);
|
||||
|
||||
$list = unserialize($token->getData());
|
||||
|
||||
//zip documents
|
||||
\set_export::build_zip(
|
||||
$this->app,
|
||||
$token,
|
||||
$list,
|
||||
$this->app['tmp.download.path'].'/'. $token->getValue() . '.zip'
|
||||
);
|
||||
|
||||
$remaingEmails = $destMails;
|
||||
|
||||
$emitter = new Emitter($user->getDisplayName(), $user->getEmail());
|
||||
|
||||
foreach ($destMails as $key => $mail) {
|
||||
try {
|
||||
$receiver = new Receiver(null, trim($mail));
|
||||
} catch (InvalidArgumentException $e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mail = MailRecordsExport::create($this->app, $receiver, $emitter, $params['textmail']);
|
||||
$mail->setButtonUrl($params['url']);
|
||||
$mail->setExpiration($token->getExpiration());
|
||||
|
||||
$this->deliver($mail, $params['reading_confirm']);
|
||||
unset($remaingEmails[$key]);
|
||||
}
|
||||
|
||||
//some mails failed
|
||||
if (count($remaingEmails) > 0) {
|
||||
$count = isset($payload['count']) ? $payload['count'] + 1 : 2 ;
|
||||
|
||||
// notify to send to the retry queue
|
||||
$this->app['dispatcher']->dispatch(WorkerEvents::EXPORT_MAIL_FAILURE, new ExportMailFailureEvent(
|
||||
$payload['emitterUserId'],
|
||||
$payload['tokenValue'],
|
||||
$remaingEmails,
|
||||
$payload['params'],
|
||||
'some mails failed',
|
||||
$count
|
||||
));
|
||||
|
||||
foreach ($remaingEmails as $mail) {
|
||||
$this->app['dispatcher']->dispatch(PhraseaEvents::EXPORT_MAIL_FAILURE, new ExportFailureEvent(
|
||||
$user,
|
||||
$params['ssttid'],
|
||||
$params['lst'],
|
||||
\eventsmanager_notify_downloadmailfail::MAIL_FAIL,
|
||||
$mail
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Worker\Factory;
|
||||
|
||||
use Alchemy\Phrasea\WorkerManager\Worker\WorkerInterface;
|
||||
|
||||
class CallableWorkerFactory implements WorkerFactoryInterface
|
||||
{
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
private $factory;
|
||||
|
||||
public function __construct(callable $factory)
|
||||
{
|
||||
$this->factory = $factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return WorkerInterface
|
||||
*/
|
||||
public function createWorker()
|
||||
{
|
||||
$factory = $this->factory;
|
||||
$worker = $factory();
|
||||
|
||||
if (! $worker instanceof WorkerInterface) {
|
||||
throw new \RuntimeException('Invalid worker created, expected an instance of \Alchemy\Phrasea\WorkerManager\Worker\WorkerInterface');
|
||||
}
|
||||
|
||||
return $worker;
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Worker\Factory;
|
||||
|
||||
use Alchemy\Phrasea\WorkerManager\Worker\WorkerInterface;
|
||||
|
||||
interface WorkerFactoryInterface
|
||||
{
|
||||
/**
|
||||
* @return WorkerInterface
|
||||
*/
|
||||
public function createWorker();
|
||||
}
|
138
lib/Alchemy/Phrasea/WorkerManager/Worker/PopulateIndexWorker.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Worker;
|
||||
|
||||
use Alchemy\Phrasea\Application\Helper\ApplicationBoxAware;
|
||||
use Alchemy\Phrasea\Application\Helper\DispatcherAware;
|
||||
use Alchemy\Phrasea\Model\Entities\WorkerRunningPopulate;
|
||||
use Alchemy\Phrasea\Model\Repositories\WorkerRunningPopulateRepository;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\ElasticsearchOptions;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Indexer;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\PopulateIndexFailureEvent;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\WorkerEvents;
|
||||
use Alchemy\Phrasea\WorkerManager\Queue\MessagePublisher;
|
||||
|
||||
class PopulateIndexWorker implements WorkerInterface
|
||||
{
|
||||
use ApplicationBoxAware;
|
||||
use DispatcherAware;
|
||||
|
||||
/** @var MessagePublisher $messagePublisher */
|
||||
private $messagePublisher;
|
||||
|
||||
/** @var Indexer $indexer */
|
||||
private $indexer;
|
||||
|
||||
/** @var WorkerRunningPopulateRepository $repoWorkerPopulate*/
|
||||
private $repoWorkerPopulate;
|
||||
|
||||
public function __construct(MessagePublisher $messagePublisher, Indexer $indexer, WorkerRunningPopulateRepository $repoWorkerPopulate)
|
||||
{
|
||||
$this->indexer = $indexer;
|
||||
$this->messagePublisher = $messagePublisher;
|
||||
$this->repoWorkerPopulate = $repoWorkerPopulate;
|
||||
}
|
||||
|
||||
public function process(array $payload)
|
||||
{
|
||||
$em = $this->repoWorkerPopulate->getEntityManager();
|
||||
$em->beginTransaction();
|
||||
$date = new \DateTime();
|
||||
|
||||
try {
|
||||
$workerRunningPopulate = new WorkerRunningPopulate();
|
||||
$workerRunningPopulate
|
||||
->setHost($payload['host'])
|
||||
->setPort($payload['port'])
|
||||
->setIndexName($payload['indexName'])
|
||||
->setDataboxId($payload['databoxId'])
|
||||
->setPublished($date->setTimestamp($payload['published']))
|
||||
->setStatus(WorkerRunningPopulate::RUNNING)
|
||||
;
|
||||
|
||||
$em->persist($workerRunningPopulate);
|
||||
|
||||
$em->flush();
|
||||
|
||||
$em->commit();
|
||||
} catch (\Exception $e) {
|
||||
$em->rollback();
|
||||
}
|
||||
|
||||
/** @var ElasticsearchOptions $options */
|
||||
$options = $this->indexer->getIndex()->getOptions();
|
||||
|
||||
$options->setIndexName($payload['indexName']);
|
||||
$options->setHost($payload['host']);
|
||||
$options->setPort($payload['port']);
|
||||
|
||||
$databoxId = $payload['databoxId'];
|
||||
|
||||
$indexExists = $this->indexer->indexExists();
|
||||
|
||||
if (!$indexExists) {
|
||||
$workerMessage = sprintf("Index %s don't exist!", $payload['indexName']);
|
||||
$this->messagePublisher->pushLog($workerMessage);
|
||||
|
||||
$count = isset($payload['count']) ? $payload['count'] + 1 : 2 ;
|
||||
|
||||
// send to retry queue
|
||||
$this->dispatch(WorkerEvents::POPULATE_INDEX_FAILURE, new PopulateIndexFailureEvent(
|
||||
$payload['host'],
|
||||
$payload['port'],
|
||||
$payload['indexName'],
|
||||
$payload['databoxId'],
|
||||
$workerMessage,
|
||||
$count
|
||||
));
|
||||
} else {
|
||||
$databox = $this->findDataboxById($databoxId);
|
||||
|
||||
try {
|
||||
$r = $this->indexer->populateIndex(Indexer::THESAURUS | Indexer::RECORDS, $databox); // , $temporary);
|
||||
|
||||
$this->messagePublisher->pushLog(sprintf(
|
||||
"Indexation of databox \"%s\" finished in %0.2f sec (Mem. %0.2f Mo)",
|
||||
$databox->get_dbname(),
|
||||
$r['duration']/1000,
|
||||
$r['memory']/1048576
|
||||
));
|
||||
} catch(\Exception $e) {
|
||||
if ($workerRunningPopulate != null) {
|
||||
|
||||
$em->remove($workerRunningPopulate);
|
||||
|
||||
$em->flush();
|
||||
}
|
||||
|
||||
$workerMessage = sprintf("Error on indexing : %s ", $e->getMessage());
|
||||
$this->messagePublisher->pushLog($workerMessage);
|
||||
|
||||
$count = isset($payload['count']) ? $payload['count'] + 1 : 2 ;
|
||||
|
||||
// notify to send a retry
|
||||
$this->dispatch(WorkerEvents::POPULATE_INDEX_FAILURE, new PopulateIndexFailureEvent(
|
||||
$payload['host'],
|
||||
$payload['port'],
|
||||
$payload['indexName'],
|
||||
$payload['databoxId'],
|
||||
$workerMessage,
|
||||
$count
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// tell that the populate is finished
|
||||
if ($workerRunningPopulate != null) {
|
||||
$workerRunningPopulate
|
||||
->setStatus(WorkerRunningPopulate::FINISHED)
|
||||
->setFinished(new \DateTime('now'))
|
||||
;
|
||||
|
||||
$em->persist($workerRunningPopulate);
|
||||
|
||||
$em->flush();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
100
lib/Alchemy/Phrasea/WorkerManager/Worker/ProcessPool.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Worker;
|
||||
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Process\Process;
|
||||
use Symfony\Component\Process\ProcessBuilder;
|
||||
|
||||
class ProcessPool implements LoggerAwareInterface
|
||||
{
|
||||
const MAX_PROCESSES = 4;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $maxProcesses = self::MAX_PROCESSES;
|
||||
|
||||
/**
|
||||
* @var Process[]
|
||||
*/
|
||||
private $processes = [];
|
||||
|
||||
/**
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->logger = new NullLogger();
|
||||
}
|
||||
|
||||
public function setMaxProcesses($maxProcesses)
|
||||
{
|
||||
$this->maxProcesses = max(1, $maxProcesses);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a logger instance on the object
|
||||
*
|
||||
* @param LoggerInterface $logger
|
||||
* @return null
|
||||
*/
|
||||
public function setLogger(LoggerInterface $logger)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $processArguments
|
||||
* @param string|null $workingDirectory
|
||||
* @return Process
|
||||
*/
|
||||
public function getWorkerProcess(array $processArguments, $workingDirectory = null)
|
||||
{
|
||||
$this->detachFinishedProcesses();
|
||||
$this->waitForNextSlot();
|
||||
|
||||
$builder = new ProcessBuilder($processArguments);
|
||||
|
||||
$builder->setWorkingDirectory($workingDirectory ?: getcwd());
|
||||
|
||||
return ($this->processes[] = $builder->getProcess());
|
||||
}
|
||||
|
||||
private function detachFinishedProcesses()
|
||||
{
|
||||
$runningProcesses = [];
|
||||
|
||||
foreach ($this->processes as $process) {
|
||||
if ($process->isRunning()) {
|
||||
$runningProcesses[] = $process;
|
||||
} else {
|
||||
$process->stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
$this->processes = $runningProcesses;
|
||||
}
|
||||
|
||||
private function waitForNextSlot()
|
||||
{
|
||||
$this->logger->debug(
|
||||
sprintf('Checking for available process slot: %d processes found.', count($this->processes))
|
||||
);
|
||||
|
||||
$interval = 1;
|
||||
|
||||
while (count($this->processes) >= $this->maxProcesses) {
|
||||
$this->logger->debug(sprintf('%d Max process count reached, will retry in %d second.', $this->maxProcesses, $interval));
|
||||
|
||||
sleep($interval);
|
||||
|
||||
$this->detachFinishedProcesses();
|
||||
$interval = min(10, $interval + 1);
|
||||
}
|
||||
}
|
||||
}
|
155
lib/Alchemy/Phrasea/WorkerManager/Worker/PullAssetsWorker.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Worker;
|
||||
|
||||
use Alchemy\Phrasea\Core\Configuration\PropertyAccess;
|
||||
use Alchemy\Phrasea\Model\Entities\WorkerRunningUploader;
|
||||
use Alchemy\Phrasea\Model\Repositories\WorkerRunningUploaderRepository;
|
||||
use Alchemy\Phrasea\WorkerManager\Queue\MessagePublisher;
|
||||
use GuzzleHttp\Client;
|
||||
|
||||
class PullAssetsWorker implements WorkerInterface
|
||||
{
|
||||
private $messagePublisher;
|
||||
private $conf;
|
||||
|
||||
/** @var WorkerRunningUploaderRepository $repoWorkerUploader */
|
||||
private $repoWorkerUploader;
|
||||
|
||||
public function __construct(MessagePublisher $messagePublisher, PropertyAccess $conf, WorkerRunningUploaderRepository $repoWorkerUploader)
|
||||
{
|
||||
$this->messagePublisher = $messagePublisher;
|
||||
$this->conf = $conf;
|
||||
$this->repoWorkerUploader = $repoWorkerUploader;
|
||||
}
|
||||
|
||||
public function process(array $payload)
|
||||
{
|
||||
$config = $this->conf->get(['workers']);
|
||||
|
||||
if (isset($config['pull_assets'])) {
|
||||
$config = $config['pull_assets'];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
$uploaderClient = new Client();
|
||||
|
||||
// if a token exist , use it
|
||||
if (isset($config['assetToken'])) {
|
||||
$res = $this->getCommits($uploaderClient, $config);
|
||||
if ($res == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if Unauthorized get a new token first
|
||||
if ($res->getStatusCode() == 401) {
|
||||
if (($config = $this->generateToken($uploaderClient, $config)) === null) {
|
||||
return;
|
||||
};
|
||||
$res = $this->getCommits($uploaderClient, $config);
|
||||
}
|
||||
} else { // if there is not a token , get one from the uploader service
|
||||
if (($config = $this->generateToken($uploaderClient, $config)) === null) {
|
||||
return;
|
||||
};
|
||||
if (($res = $this->getCommits($uploaderClient, $config)) === null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$body = $res->getBody()->getContents();
|
||||
$body = json_decode($body,true);
|
||||
$commits = $body['hydra:member'];
|
||||
|
||||
$urlInfo = parse_url($config['endpointCommit']);
|
||||
$baseUrl = $urlInfo['scheme'] . '://' . $urlInfo['host'] .':'.$urlInfo['port'];
|
||||
|
||||
foreach ($commits as $commit) {
|
||||
// send only payload in ingest-queue if the commit is ack false and it is not being creating
|
||||
if (!$commit['acknowledged'] && !$this->isCommitToBeCreating($commit['id'])) {
|
||||
$this->messagePublisher->pushLog("A new commit found in the uploader ! commit_ID : ".$commit['id']);
|
||||
|
||||
// this is an uploader PULL mode
|
||||
$payload = [
|
||||
'message_type' => MessagePublisher::ASSETS_INGEST_TYPE,
|
||||
'payload' => [
|
||||
'assets' => array_map(function($asset) {
|
||||
return str_replace('/assets/', '', $asset);
|
||||
}, $commit['assets']),
|
||||
'publisher' => $commit['userId'],
|
||||
'commit_id' => $commit['id'],
|
||||
'token' => $commit['token'],
|
||||
'base_url' => $baseUrl,
|
||||
'type' => WorkerRunningUploader::TYPE_PULL
|
||||
]
|
||||
];
|
||||
|
||||
$this->messagePublisher->publishMessage($payload, MessagePublisher::ASSETS_INGEST_QUEUE);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Client $uploaderClient
|
||||
* @param array $config
|
||||
* @return \Psr\Http\Message\ResponseInterface|null
|
||||
*/
|
||||
private function getCommits(Client $uploaderClient, array $config)
|
||||
{
|
||||
try {
|
||||
$res = $uploaderClient->get($config['endpointCommit'], [
|
||||
'headers' => [
|
||||
'Authorization' => 'AssetToken '.$config['assetToken']
|
||||
]
|
||||
]);
|
||||
} catch(\Exception $e) {
|
||||
$this->messagePublisher->pushLog("An error occurred when fetching endpointCommit : " . $e->getMessage());
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Client $uploaderClient
|
||||
* @param array $config
|
||||
* @return array|null
|
||||
*/
|
||||
private function generateToken(Client $uploaderClient, array $config)
|
||||
{
|
||||
try {
|
||||
$tokenBody = $uploaderClient->post($config['endpointToken'], [
|
||||
'json' => [
|
||||
'client_id' => $config['clientId'],
|
||||
'client_secret' => $config['clientSecret'],
|
||||
'grant_type' => 'client_credentials',
|
||||
'scope' => 'uploader:commit_list'
|
||||
]
|
||||
])->getBody()->getContents();
|
||||
} catch (\Exception $e) {
|
||||
$this->messagePublisher->pushLog("An error occurred when fetching endpointToken : " . $e->getMessage());
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$tokenBody = json_decode($tokenBody,true);
|
||||
|
||||
$this->conf->set(['workers', 'pull_assets', 'assetToken'], $tokenBody['access_token']);
|
||||
|
||||
return $this->conf->get(['workers', 'pull_assets']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $commitId
|
||||
* @return bool
|
||||
*/
|
||||
private function isCommitToBeCreating($commitId)
|
||||
{
|
||||
$res = $this->repoWorkerUploader->findBy(['commitId' => $commitId]);
|
||||
|
||||
return count($res) != 0;
|
||||
}
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Worker\Resolver;
|
||||
|
||||
|
||||
use Alchemy\Phrasea\WorkerManager\Worker\Factory\WorkerFactoryInterface;
|
||||
use Alchemy\Phrasea\WorkerManager\Worker\WorkerInterface;
|
||||
|
||||
class TypeBasedWorkerResolver implements WorkerResolverInterface
|
||||
{
|
||||
/**
|
||||
* @var WorkerInterface[]
|
||||
*/
|
||||
private $workers = [];
|
||||
|
||||
/**
|
||||
* @var WorkerFactoryInterface[]
|
||||
*/
|
||||
private $factories = [];
|
||||
|
||||
public function addFactory($messageType, WorkerFactoryInterface $workerFactory)
|
||||
{
|
||||
$this->factories[$messageType] = $workerFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return WorkerFactoryInterface[]
|
||||
*/
|
||||
public function getFactories()
|
||||
{
|
||||
return $this->factories;
|
||||
}
|
||||
|
||||
public function getWorker($messageType, array $message)
|
||||
{
|
||||
if (isset($this->workers[$messageType])) {
|
||||
return $this->workers[$messageType];
|
||||
}
|
||||
|
||||
if (isset($this->factories[$messageType])) {
|
||||
return $this->workers[$messageType] = $this->factories[$messageType]->createWorker();
|
||||
}
|
||||
|
||||
throw new \RuntimeException('Invalid worker type requested: ' . $messageType);
|
||||
}
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Worker\Resolver;
|
||||
|
||||
use Alchemy\Phrasea\WorkerManager\Worker\WorkerInterface;
|
||||
|
||||
interface WorkerResolverInterface
|
||||
{
|
||||
/**
|
||||
* @param string $messageType
|
||||
* @param array $message
|
||||
* @return WorkerInterface
|
||||
*/
|
||||
public function getWorker($messageType, array $message);
|
||||
}
|
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Worker;
|
||||
|
||||
use Alchemy\Phrasea\Application\Helper\ApplicationBoxAware;
|
||||
use Alchemy\Phrasea\Application\Helper\EntityManagerAware;
|
||||
use Alchemy\Phrasea\Core\PhraseaTokens;
|
||||
use Alchemy\Phrasea\Filesystem\FilesystemService;
|
||||
use Alchemy\Phrasea\Media\SubdefGenerator;
|
||||
use Alchemy\Phrasea\Model\Entities\WorkerRunningJob;
|
||||
use Alchemy\Phrasea\Model\Repositories\WorkerRunningJobRepository;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Indexer;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\StoryCreateCoverEvent;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\SubdefinitionCreationFailureEvent;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\SubdefinitionWritemetaEvent;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\WorkerEvents;
|
||||
use Alchemy\Phrasea\WorkerManager\Queue\MessagePublisher;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class SubdefCreationWorker implements WorkerInterface
|
||||
{
|
||||
use ApplicationBoxAware;
|
||||
|
||||
private $subdefGenerator;
|
||||
|
||||
/** @var MessagePublisher $messagePublisher */
|
||||
private $messagePublisher;
|
||||
|
||||
private $logger;
|
||||
private $dispatcher;
|
||||
private $filesystem;
|
||||
private $repoWorker;
|
||||
private $indexer;
|
||||
|
||||
public function __construct(
|
||||
SubdefGenerator $subdefGenerator,
|
||||
MessagePublisher $messagePublisher,
|
||||
LoggerInterface $logger,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
FilesystemService $filesystem,
|
||||
WorkerRunningJobRepository $repoWorker,
|
||||
Indexer $indexer
|
||||
)
|
||||
{
|
||||
$this->subdefGenerator = $subdefGenerator;
|
||||
$this->messagePublisher = $messagePublisher;
|
||||
$this->logger = $logger;
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->filesystem = $filesystem;
|
||||
$this->repoWorker = $repoWorker;
|
||||
$this->indexer = $indexer;
|
||||
}
|
||||
|
||||
public function process(array $payload)
|
||||
{
|
||||
if(isset($payload['recordId']) && isset($payload['databoxId'])) {
|
||||
$recordId = $payload['recordId'];
|
||||
$databoxId = $payload['databoxId'];
|
||||
$wantedSubdef = [$payload['subdefName']];
|
||||
|
||||
$databox = $this->findDataboxById($databoxId);
|
||||
$record = $databox->get_record($recordId);
|
||||
|
||||
$oldLogger = $this->subdefGenerator->getLogger();
|
||||
|
||||
if (!$record->isStory()) {
|
||||
// check if there is a write meta running for the record or the same task running
|
||||
$canCreateSubdef = $this->repoWorker->canCreateSubdef($payload['subdefName'], $recordId, $databoxId);
|
||||
|
||||
if (!$canCreateSubdef) {
|
||||
// the file is in used to write meta
|
||||
$payload = [
|
||||
'message_type' => MessagePublisher::SUBDEF_CREATION_TYPE,
|
||||
'payload' => $payload
|
||||
];
|
||||
$this->messagePublisher->publishMessage($payload, MessagePublisher::DELAYED_SUBDEF_QUEUE);
|
||||
|
||||
return ;
|
||||
}
|
||||
|
||||
// tell that a file is in used to create subdef
|
||||
$em = $this->repoWorker->getEntityManager();
|
||||
$em->beginTransaction();
|
||||
|
||||
try {
|
||||
$date = new \DateTime();
|
||||
$workerRunningJob = new WorkerRunningJob();
|
||||
$workerRunningJob
|
||||
->setDataboxId($databoxId)
|
||||
->setRecordId($recordId)
|
||||
->setWork(PhraseaTokens::MAKE_SUBDEF)
|
||||
->setWorkOn($payload['subdefName'])
|
||||
->setPublished($date->setTimestamp($payload['published']))
|
||||
->setStatus(WorkerRunningJob::RUNNING)
|
||||
;
|
||||
|
||||
$em->persist($workerRunningJob);
|
||||
$em->flush();
|
||||
|
||||
$em->commit();
|
||||
} catch (\Exception $e) {
|
||||
$em->rollback();
|
||||
}
|
||||
|
||||
$this->subdefGenerator->setLogger($this->logger);
|
||||
|
||||
try {
|
||||
$this->subdefGenerator->generateSubdefs($record, $wantedSubdef);
|
||||
} catch (\Exception $e) {
|
||||
$em->beginTransaction();
|
||||
try {
|
||||
$em->remove($workerRunningJob);
|
||||
$em->flush();
|
||||
$em->commit();
|
||||
} catch (\Exception $e) {
|
||||
$em->rollback();
|
||||
}
|
||||
}
|
||||
|
||||
// begin to check if the subdef is successfully generated
|
||||
$subdef = $record->getDatabox()->get_subdef_structure()->getSubdefGroup($record->getType())->getSubdef($payload['subdefName']);
|
||||
$filePathToCheck = null;
|
||||
|
||||
if ($record->has_subdef($payload['subdefName']) ) {
|
||||
$filePathToCheck = $record->get_subdef($payload['subdefName'])->getRealPath();
|
||||
}
|
||||
|
||||
$filePathToCheck = $this->filesystem->generateSubdefPathname($record, $subdef, $filePathToCheck);
|
||||
|
||||
if (!$this->filesystem->exists($filePathToCheck)) {
|
||||
|
||||
$count = isset($payload['count']) ? $payload['count'] + 1 : 2 ;
|
||||
|
||||
$this->dispatcher->dispatch(WorkerEvents::SUBDEFINITION_CREATION_FAILURE, new SubdefinitionCreationFailureEvent(
|
||||
$record,
|
||||
$payload['subdefName'],
|
||||
'Subdef generation failed !',
|
||||
$count
|
||||
));
|
||||
|
||||
$this->subdefGenerator->setLogger($oldLogger);
|
||||
return ;
|
||||
}
|
||||
// checking ended
|
||||
|
||||
// order to write meta for the subdef if needed
|
||||
$this->dispatcher->dispatch(WorkerEvents::SUBDEFINITION_WRITE_META, new SubdefinitionWritemetaEvent($record, $payload['subdefName']));
|
||||
|
||||
$this->subdefGenerator->setLogger($oldLogger);
|
||||
|
||||
// update jeton when subdef is created
|
||||
$this->updateJeton($record);
|
||||
|
||||
$parents = $record->get_grouping_parents();
|
||||
|
||||
// create a cover for a story
|
||||
// used when uploaded via uploader-service and grouped as a story
|
||||
if (!$parents->is_empty() && isset($payload['status']) && $payload['status'] == MessagePublisher::NEW_RECORD_MESSAGE && in_array($payload['subdefName'], array('thumbnail', 'preview'))) {
|
||||
foreach ($parents->get_elements() as $story) {
|
||||
if (self::checkIfFirstChild($story, $record)) {
|
||||
$data = implode('_', [$databoxId, $story->getRecordId(), $recordId, $payload['subdefName']]);
|
||||
|
||||
$this->dispatcher->dispatch(WorkerEvents::STORY_CREATE_COVER, new StoryCreateCoverEvent($data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update elastic
|
||||
$this->indexer->flushQueue();
|
||||
|
||||
// tell that we have finished to work on this file
|
||||
$em->beginTransaction();
|
||||
try {
|
||||
$workerRunningJob->setStatus(WorkerRunningJob::FINISHED);
|
||||
$workerRunningJob->setFinished(new \DateTime('now'));
|
||||
$em->persist($workerRunningJob);
|
||||
$em->flush();
|
||||
$em->commit();
|
||||
} catch (\Exception $e) {
|
||||
$em->rollback();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function checkIfFirstChild(\record_adapter $story, \record_adapter $record)
|
||||
{
|
||||
$sql = "SELECT * FROM regroup WHERE rid_parent = :parent_record_id AND rid_child = :children_id and ord = :ord";
|
||||
|
||||
$connection = $record->getDatabox()->get_connection();
|
||||
|
||||
$stmt = $connection->prepare($sql);
|
||||
|
||||
$stmt->execute([
|
||||
':parent_record_id' => $story->getRecordId(),
|
||||
':children_id' => $record->getRecordId(),
|
||||
':ord' => 0,
|
||||
]);
|
||||
|
||||
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
$stmt->closeCursor();
|
||||
|
||||
if ($row) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function updateJeton(\record_adapter $record)
|
||||
{
|
||||
$connection = $record->getDatabox()->get_connection();
|
||||
$connection->beginTransaction();
|
||||
|
||||
// mark subdef created
|
||||
$sql = 'UPDATE record'
|
||||
. ' SET jeton=(jeton & ~(:token)), moddate=NOW()'
|
||||
. ' WHERE record_id=:record_id';
|
||||
|
||||
$stmt = $connection->prepare($sql);
|
||||
|
||||
$stmt->execute([
|
||||
':record_id' => $record->getRecordId(),
|
||||
':token' => PhraseaTokens::MAKE_SUBDEF,
|
||||
]);
|
||||
|
||||
$connection->commit();
|
||||
$stmt->closeCursor();
|
||||
}
|
||||
}
|
206
lib/Alchemy/Phrasea/WorkerManager/Worker/WebhookWorker.php
Normal file
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Worker;
|
||||
|
||||
use Alchemy\Phrasea\Application;
|
||||
use Alchemy\Phrasea\Application\Helper\DispatcherAware;
|
||||
use Alchemy\Phrasea\Core\Version;
|
||||
use Alchemy\Phrasea\Model\Entities\ApiApplication;
|
||||
use Alchemy\Phrasea\Model\Entities\WebhookEvent;
|
||||
use Alchemy\Phrasea\Model\Entities\WebhookEventDelivery;
|
||||
use Alchemy\Phrasea\Webhook\Processor\ProcessorInterface;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\WebhookDeliverFailureEvent;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\WorkerEvents;
|
||||
use Alchemy\Phrasea\WorkerManager\Queue\MessagePublisher;
|
||||
use Guzzle\Batch\BatchBuilder;
|
||||
use Guzzle\Common\Event;
|
||||
use Guzzle\Http\Client as GuzzleClient;
|
||||
use Guzzle\Http\Message\Request;
|
||||
use Guzzle\Plugin\Backoff\BackoffPlugin;
|
||||
use Guzzle\Plugin\Backoff\CallbackBackoffStrategy;
|
||||
use Guzzle\Plugin\Backoff\CurlBackoffStrategy;
|
||||
use Guzzle\Plugin\Backoff\TruncatedBackoffStrategy;
|
||||
use PhpAmqpLib\Wire\AMQPTable;
|
||||
|
||||
class WebhookWorker implements WorkerInterface
|
||||
{
|
||||
use DispatcherAware;
|
||||
|
||||
private $app;
|
||||
|
||||
/** @var MessagePublisher $messagePublisher */
|
||||
private $messagePublisher;
|
||||
|
||||
public function __construct(Application $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
$this->messagePublisher = $app['alchemy_worker.message.publisher'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $payload
|
||||
*/
|
||||
public function process(array $payload)
|
||||
{
|
||||
if (isset($payload['id'])) {
|
||||
$webhookEventId = $payload['id'];
|
||||
$app = $this->app;
|
||||
|
||||
$httpClient = new GuzzleClient();
|
||||
$version = new Version();
|
||||
$httpClient->setUserAgent(sprintf('Phraseanet/%s (%s)', $version->getNumber(), $version->getName()));
|
||||
|
||||
$httpClient->getEventDispatcher()->addListener('request.error', function (Event $event) {
|
||||
// override guzzle default behavior of throwing exceptions
|
||||
// when 4xx & 5xx responses are encountered
|
||||
$event->stopPropagation();
|
||||
}, -254);
|
||||
|
||||
// Set callback which logs success or failure
|
||||
$subscriber = new CallbackBackoffStrategy(function ($retries, Request $request, $response, $e) use ($app, $webhookEventId, $payload) {
|
||||
$retry = true;
|
||||
if ($response && (null !== $deliverId = parse_url($request->getUrl(), PHP_URL_FRAGMENT))) {
|
||||
/** @var WebhookEventDelivery $delivery */
|
||||
$delivery = $app['repo.webhook-delivery']->find($deliverId);
|
||||
|
||||
$logContext = [ 'host' => $request->getHost() ];
|
||||
|
||||
if ($response->isSuccessful()) {
|
||||
$app['manipulator.webhook-delivery']->deliverySuccess($delivery);
|
||||
|
||||
$logType = 'info';
|
||||
$logEntry = sprintf('Deliver success event "%d:%s" for app "%s"',
|
||||
$delivery->getWebhookEvent()->getId(), $delivery->getWebhookEvent()->getName(),
|
||||
$delivery->getThirdPartyApplication()->getName()
|
||||
);
|
||||
|
||||
$retry = false;
|
||||
} else {
|
||||
$app['manipulator.webhook-delivery']->deliveryFailure($delivery);
|
||||
|
||||
$logType = 'error';
|
||||
$logEntry = sprintf('Deliver failure event "%d:%s" for app "%s"',
|
||||
$delivery->getWebhookEvent()->getId(), $delivery->getWebhookEvent()->getName(),
|
||||
$delivery->getThirdPartyApplication()->getName()
|
||||
);
|
||||
|
||||
$count = isset($payload['count']) ? $payload['count'] + 1 : 2 ;
|
||||
|
||||
$this->dispatch(WorkerEvents::WEBHOOK_DELIVER_FAILURE, new WebhookDeliverFailureEvent(
|
||||
$webhookEventId,
|
||||
$logEntry,
|
||||
$count,
|
||||
$deliverId
|
||||
));
|
||||
}
|
||||
|
||||
$app['alchemy_worker.message.publisher']->pushLog($logEntry, $logType, $logContext);
|
||||
|
||||
return $retry;
|
||||
}
|
||||
}, true, new CurlBackoffStrategy());
|
||||
|
||||
// set max retries
|
||||
$subscriber = new TruncatedBackoffStrategy(1, $subscriber);
|
||||
$subscriber = new BackoffPlugin($subscriber);
|
||||
|
||||
$httpClient->addSubscriber($subscriber);
|
||||
|
||||
|
||||
$thirdPartyApplications = $this->app['repo.api-applications']->findWithDefinedWebhookCallback();
|
||||
|
||||
/** @var WebhookEvent|null $webhookevent */
|
||||
$webhookevent = $this->app['repo.webhook-event']->find($webhookEventId);
|
||||
|
||||
if ($webhookevent !== null) {
|
||||
$app['manipulator.webhook-event']->processed($webhookevent);
|
||||
|
||||
$this->messagePublisher->pushLog(sprintf('Processing event "%s" with id %d', $webhookevent->getName(), $webhookevent->getId()));
|
||||
// send requests
|
||||
$this->deliverEvent($httpClient, $thirdPartyApplications, $webhookevent, $payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function deliverEvent(GuzzleClient $httpClient, array $thirdPartyApplications, WebhookEvent $webhookevent, $payload)
|
||||
{
|
||||
if (count($thirdPartyApplications) === 0) {
|
||||
$workerMessage = 'No applications defined to listen for webhook events';
|
||||
$this->messagePublisher->pushLog($workerMessage);
|
||||
|
||||
// count = 0 mean do not retry because no api application defined
|
||||
$this->dispatch(WorkerEvents::WEBHOOK_DELIVER_FAILURE, new WebhookDeliverFailureEvent($webhookevent->getId(), $workerMessage, 0));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// format event data
|
||||
if (!isset($payload['delivery_id'])) {
|
||||
$webhookData = $webhookevent->getData();
|
||||
$webhookData['time'] = $webhookevent->getCreated();
|
||||
$webhookevent->setData($webhookData);
|
||||
}
|
||||
|
||||
/** @var ProcessorInterface $eventProcessor */
|
||||
$eventProcessor = $this->app['webhook.processor_factory']->get($webhookevent);
|
||||
$data = $eventProcessor->process($webhookevent);
|
||||
|
||||
// batch requests
|
||||
$batch = BatchBuilder::factory()
|
||||
->transferRequests(10)
|
||||
->build();
|
||||
|
||||
/** @var ApiApplication $thirdPartyApplication */
|
||||
foreach ($thirdPartyApplications as $thirdPartyApplication) {
|
||||
$creator = $thirdPartyApplication->getCreator();
|
||||
|
||||
if ($creator == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$creatorGrantedBaseIds = array_keys($this->app['acl']->get($creator)->get_granted_base());
|
||||
|
||||
$concernedBaseIds = array_intersect($webhookevent->getCollectionBaseIds(), $creatorGrantedBaseIds);
|
||||
|
||||
if (count($webhookevent->getCollectionBaseIds()) != 0 && count($concernedBaseIds) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($payload['delivery_id']) && $payload['delivery_id'] != null) {
|
||||
/** @var WebhookEventDelivery $delivery */
|
||||
$delivery = $this->app['repo.webhook-delivery']->find($payload['delivery_id']);
|
||||
|
||||
// only the app url to retry
|
||||
if ($delivery->getThirdPartyApplication()->getId() != $thirdPartyApplication->getId()) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
$delivery = $this->app['manipulator.webhook-delivery']->create($thirdPartyApplication, $webhookevent);
|
||||
}
|
||||
|
||||
// append delivery id as url anchor
|
||||
$uniqueUrl = $this->getUrl($thirdPartyApplication, $delivery);
|
||||
|
||||
// create http request with data as request body
|
||||
$batch->add($httpClient->createRequest('POST', $uniqueUrl, [
|
||||
'Content-Type' => 'application/vnd.phraseanet.event+json'
|
||||
], json_encode($data)));
|
||||
}
|
||||
|
||||
try {
|
||||
$batch->flush();
|
||||
} catch (\Exception $e) {
|
||||
$this->messagePublisher->pushLog($e->getMessage());
|
||||
$this->messagePublisher->publishFailedMessage(
|
||||
$payload,
|
||||
new AMQPTable(['worker-message' => $e->getMessage()]),
|
||||
MessagePublisher::FAILED_WEBHOOK_QUEUE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function getUrl(ApiApplication $application, WebhookEventDelivery $delivery)
|
||||
{
|
||||
return sprintf('%s#%s', $application->getWebhookUrl(), $delivery->getId());
|
||||
}
|
||||
}
|
12
lib/Alchemy/Phrasea/WorkerManager/Worker/WorkerInterface.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Worker;
|
||||
|
||||
interface WorkerInterface
|
||||
{
|
||||
/**
|
||||
* @param array $payload
|
||||
* @return mixed
|
||||
*/
|
||||
public function process(array $payload);
|
||||
}
|
144
lib/Alchemy/Phrasea/WorkerManager/Worker/WorkerInvoker.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Worker;
|
||||
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Process\Exception\RuntimeException as ProcessRuntimeException;
|
||||
|
||||
class WorkerInvoker implements LoggerAwareInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $environment;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $command = 'worker:run-service';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $binaryPath;
|
||||
|
||||
/**
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* @var ProcessPool
|
||||
*/
|
||||
private $processPool;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $preservePayloads = false;
|
||||
|
||||
/**
|
||||
* payload file prefix
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $prefix = 'alchemy_wk_';
|
||||
|
||||
/**
|
||||
* WorkerInvoker constructor.
|
||||
*
|
||||
* @param ProcessPool $processPool
|
||||
* @param bool $environment
|
||||
*/
|
||||
public function __construct(ProcessPool $processPool, $environment = false)
|
||||
{
|
||||
$this->binaryPath = $_SERVER['SCRIPT_NAME'];
|
||||
$this->environment = $environment;
|
||||
$this->processPool = $processPool;
|
||||
$this->logger = new NullLogger();
|
||||
}
|
||||
|
||||
public function preservePayloads()
|
||||
{
|
||||
$this->preservePayloads = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a logger instance on the object
|
||||
*
|
||||
* @param LoggerInterface $logger
|
||||
* @return null
|
||||
*/
|
||||
public function setLogger(LoggerInterface $logger)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function setPrefix($prefix)
|
||||
{
|
||||
$this->prefix = $prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $messageType
|
||||
* @param string $payload
|
||||
*/
|
||||
public function invokeWorker($messageType, $payload)
|
||||
{
|
||||
$args = [
|
||||
$this->binaryPath,
|
||||
$this->command,
|
||||
'-vv',
|
||||
$messageType,
|
||||
$this->createPayloadFile($payload)
|
||||
];
|
||||
|
||||
if ($this->environment) {
|
||||
$args[] = sprintf('-e=%s', $this->environment);
|
||||
}
|
||||
|
||||
if ($this->preservePayloads) {
|
||||
$args[] = '--preserve-payload';
|
||||
}
|
||||
|
||||
$process = $this->processPool->getWorkerProcess($args, getcwd());
|
||||
|
||||
$this->logger->debug('Invoking shell command: ' . $process->getCommandLine());
|
||||
|
||||
try {
|
||||
$process->start([$this, 'logWorkerOutput']);
|
||||
} catch (ProcessRuntimeException $e) {
|
||||
$process->stop();
|
||||
|
||||
throw new \RuntimeException(sprintf('Command "%s" failed: %s', $process->getCommandLine(),
|
||||
$e->getMessage()), 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
public function logWorkerOutput($stream, $output)
|
||||
{
|
||||
if ($stream == 'err') {
|
||||
$this->logger->error($output);
|
||||
} else {
|
||||
$this->logger->info($output);
|
||||
}
|
||||
}
|
||||
|
||||
public function setMaxProcessPoolValue($maxProcesses)
|
||||
{
|
||||
$this->processPool->setMaxProcesses($maxProcesses);
|
||||
}
|
||||
|
||||
private function createPayloadFile($payload)
|
||||
{
|
||||
$path = tempnam(sys_get_temp_dir(), $this->prefix);
|
||||
|
||||
if (file_put_contents($path, $payload) === false) {
|
||||
throw new \RuntimeException('Cannot write payload file to path: ' . $path);
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
}
|
@@ -0,0 +1,313 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\WorkerManager\Worker;
|
||||
|
||||
use Alchemy\Phrasea\Application\Helper\ApplicationBoxAware;
|
||||
use Alchemy\Phrasea\Application\Helper\DispatcherAware;
|
||||
use Alchemy\Phrasea\Application\Helper\EntityManagerAware;
|
||||
use Alchemy\Phrasea\Core\PhraseaTokens;
|
||||
use Alchemy\Phrasea\Metadata\TagFactory;
|
||||
use Alchemy\Phrasea\Model\Entities\WorkerRunningJob;
|
||||
use Alchemy\Phrasea\Model\Repositories\WorkerRunningJobRepository;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\SubdefinitionWritemetaEvent;
|
||||
use Alchemy\Phrasea\WorkerManager\Event\WorkerEvents;
|
||||
use Alchemy\Phrasea\WorkerManager\Queue\MessagePublisher;
|
||||
use Monolog\Logger;
|
||||
use PHPExiftool\Driver\Metadata\Metadata;
|
||||
use PHPExiftool\Driver\Metadata\MetadataBag;
|
||||
use PHPExiftool\Driver\Tag;
|
||||
use PHPExiftool\Driver\Value\Mono;
|
||||
use PHPExiftool\Driver\Value\Multi;
|
||||
use PHPExiftool\Exception\ExceptionInterface as PHPExiftoolException;
|
||||
use PHPExiftool\Exception\TagUnknown;
|
||||
use PHPExiftool\Writer;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class WriteMetadatasWorker implements WorkerInterface
|
||||
{
|
||||
use ApplicationBoxAware;
|
||||
use DispatcherAware;
|
||||
use EntityManagerAware;
|
||||
|
||||
/** @var Logger */
|
||||
private $logger;
|
||||
|
||||
/** @var MessagePublisher $messagePublisher */
|
||||
private $messagePublisher;
|
||||
|
||||
/** @var Writer $writer */
|
||||
private $writer;
|
||||
|
||||
private $repoWorker;
|
||||
|
||||
public function __construct(
|
||||
Writer $writer,
|
||||
LoggerInterface $logger,
|
||||
MessagePublisher $messagePublisher,
|
||||
WorkerRunningJobRepository $repoWorker
|
||||
)
|
||||
{
|
||||
$this->writer = $writer;
|
||||
$this->logger = $logger;
|
||||
$this->messagePublisher = $messagePublisher;
|
||||
$this->repoWorker = $repoWorker;
|
||||
}
|
||||
|
||||
public function process(array $payload)
|
||||
{
|
||||
if (isset($payload['recordId']) && isset($payload['databoxId'])) {
|
||||
$recordId = $payload['recordId'];
|
||||
$databoxId = $payload['databoxId'];
|
||||
|
||||
$MWG = isset($payload['MWG']) ? $payload['MWG'] : false;
|
||||
$clearDoc = isset($payload['clearDoc']) ? $payload['clearDoc'] : false;
|
||||
$databox = $this->findDataboxById($databoxId);
|
||||
|
||||
|
||||
$param = ($payload['subdefName'] == "document") ? PhraseaTokens::WRITE_META_DOC : PhraseaTokens::WRITE_META_SUBDEF;
|
||||
|
||||
// check if there is a make subdef running for the record or the same task running
|
||||
$canWriteMeta = $this->repoWorker->canWriteMetadata($payload['subdefName'], $recordId, $databoxId);
|
||||
|
||||
if (!$canWriteMeta) {
|
||||
// the file is in used to generate subdef
|
||||
$payload = [
|
||||
'message_type' => MessagePublisher::WRITE_METADATAS_TYPE,
|
||||
'payload' => $payload
|
||||
];
|
||||
$this->messagePublisher->publishMessage($payload, MessagePublisher::DELAYED_METADATAS_QUEUE);
|
||||
|
||||
return ;
|
||||
}
|
||||
|
||||
// tell that a file is in used to create subdef
|
||||
$em = $this->getEntityManager();
|
||||
$em->beginTransaction();
|
||||
|
||||
try {
|
||||
$date = new \DateTime();
|
||||
$workerRunningJob = new WorkerRunningJob();
|
||||
$workerRunningJob
|
||||
->setDataboxId($databoxId)
|
||||
->setRecordId($recordId)
|
||||
->setWork($param)
|
||||
->setWorkOn($payload['subdefName'])
|
||||
->setPublished($date->setTimestamp($payload['published']))
|
||||
->setStatus(WorkerRunningJob::RUNNING)
|
||||
;
|
||||
|
||||
$em->persist($workerRunningJob);
|
||||
$em->flush();
|
||||
|
||||
$em->commit();
|
||||
} catch (\Exception $e) {
|
||||
$em->rollback();
|
||||
}
|
||||
|
||||
$record = $databox->get_record($recordId);
|
||||
|
||||
$subdef = $record->get_subdef($payload['subdefName']);
|
||||
|
||||
if ($subdef->is_physically_present()) {
|
||||
$metadata = new MetadataBag();
|
||||
|
||||
// add Uuid in metadatabag
|
||||
if ($record->getUuid()) {
|
||||
$metadata->add(
|
||||
new Metadata(
|
||||
new Tag\XMPExif\ImageUniqueID(),
|
||||
new Mono($record->getUuid())
|
||||
)
|
||||
);
|
||||
$metadata->add(
|
||||
new Metadata(
|
||||
new Tag\ExifIFD\ImageUniqueID(),
|
||||
new Mono($record->getUuid())
|
||||
)
|
||||
);
|
||||
$metadata->add(
|
||||
new Metadata(
|
||||
new Tag\IPTC\UniqueDocumentID(),
|
||||
new Mono($record->getUuid())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// read document fields and add to metadatabag
|
||||
$caption = $record->get_caption();
|
||||
foreach ($databox->get_meta_structure() as $fieldStructure) {
|
||||
|
||||
$tagName = $fieldStructure->get_tag()->getTagname();
|
||||
$fieldName = $fieldStructure->get_name();
|
||||
|
||||
// skip fields with no src
|
||||
if ($tagName == '' || $tagName == 'Phraseanet:no-source') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// check exiftool known tags to skip Phraseanet:tf-*
|
||||
try {
|
||||
$tag = TagFactory::getFromRDFTagname($tagName);
|
||||
if(!$tag->isWritable()) {
|
||||
continue;
|
||||
}
|
||||
} catch (TagUnknown $e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$field = $caption->get_field($fieldName);
|
||||
$fieldValues = $field->get_values();
|
||||
|
||||
if ($fieldStructure->is_multi()) {
|
||||
$values = array();
|
||||
foreach ($fieldValues as $value) {
|
||||
$values[] = $this->removeNulChar($value->getValue());
|
||||
}
|
||||
|
||||
$value = new Multi($values);
|
||||
} else {
|
||||
$fieldValue = array_pop($fieldValues);
|
||||
$value = $this->removeNulChar($fieldValue->getValue());
|
||||
|
||||
// fix the dates edited into phraseanet
|
||||
if($fieldStructure->get_type() === $fieldStructure::TYPE_DATE) {
|
||||
try {
|
||||
$value = self::fixDate($value); // will return NULL if the date is not valid
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$value = null; // do NOT write back to iptc
|
||||
}
|
||||
}
|
||||
|
||||
if($value !== null) { // do not write invalid dates
|
||||
$value = new Mono($value);
|
||||
}
|
||||
}
|
||||
} catch(\Exception $e) {
|
||||
// the field is not set in the record, erase it
|
||||
if ($fieldStructure->is_multi()) {
|
||||
$value = new Multi(array(''));
|
||||
}
|
||||
else {
|
||||
$value = new Mono('');
|
||||
}
|
||||
}
|
||||
|
||||
if($value !== null) { // do not write invalid data
|
||||
$metadata->add(
|
||||
new Metadata($fieldStructure->get_tag(), $value)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->writer->reset();
|
||||
|
||||
if ($MWG) {
|
||||
$this->writer->setModule(Writer::MODULE_MWG, true);
|
||||
}
|
||||
|
||||
$this->writer->erase($subdef->get_name() != 'document' || $clearDoc, true);
|
||||
|
||||
// write meta in file
|
||||
try {
|
||||
$this->writer->write($subdef->getRealPath(), $metadata);
|
||||
|
||||
$this->messagePublisher->pushLog(sprintf('meta written for sbasid=%1$d - recordid=%2$d (%3$s)', $databox->get_sbas_id(), $recordId, $subdef->get_name() ));
|
||||
} catch (\Exception $e) {
|
||||
$workerMessage = sprintf('meta NOT written for sbasid=%1$d - recordid=%2$d (%3$s) because "%s"', $databox->get_sbas_id(), $recordId, $subdef->get_name() , $e->getMessage());
|
||||
$this->logger->error($workerMessage);
|
||||
|
||||
$count = isset($payload['count']) ? $payload['count'] + 1 : 2 ;
|
||||
|
||||
$this->dispatch(WorkerEvents::SUBDEFINITION_WRITE_META, new SubdefinitionWritemetaEvent(
|
||||
$record,
|
||||
$payload['subdefName'],
|
||||
SubdefinitionWritemetaEvent::FAILED,
|
||||
$workerMessage,
|
||||
$count
|
||||
));
|
||||
}
|
||||
|
||||
// mark write metas finished
|
||||
$this->updateJeton($record);
|
||||
} else {
|
||||
$count = isset($payload['count']) ? $payload['count'] + 1 : 2 ;
|
||||
|
||||
$this->dispatch(WorkerEvents::SUBDEFINITION_WRITE_META, new SubdefinitionWritemetaEvent(
|
||||
$record,
|
||||
$payload['subdefName'],
|
||||
SubdefinitionWritemetaEvent::FAILED,
|
||||
'Subdef is not physically present!',
|
||||
$count
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
// tell that we have finished to work on this file
|
||||
$em->beginTransaction();
|
||||
try {
|
||||
$workerRunningJob->setStatus(WorkerRunningJob::FINISHED);
|
||||
$workerRunningJob->setFinished(new \DateTime('now'));
|
||||
$em->persist($workerRunningJob);
|
||||
$em->flush();
|
||||
$em->commit();
|
||||
} catch (\Exception $e) {
|
||||
$em->rollback();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private function removeNulChar($value)
|
||||
{
|
||||
return str_replace("\0", "", $value);
|
||||
}
|
||||
|
||||
private function updateJeton(\record_adapter $record)
|
||||
{
|
||||
$connection = $record->getDatabox()->get_connection();
|
||||
|
||||
$connection->beginTransaction();
|
||||
$stmt = $connection->prepare('UPDATE record SET jeton=(jeton & ~(:token)), moddate=NOW() WHERE record_id = :record_id');
|
||||
|
||||
$stmt->execute([
|
||||
':record_id' => $record->getRecordId(),
|
||||
':token' => PhraseaTokens::WRITE_META,
|
||||
]);
|
||||
|
||||
$connection->commit();
|
||||
$stmt->closeCursor();
|
||||
}
|
||||
|
||||
/**
|
||||
* re-format a phraseanet date for iptc writing
|
||||
* return NULL if the date is not valid
|
||||
*
|
||||
* @param string $value
|
||||
* @return string|null
|
||||
*/
|
||||
private static function fixDate($value)
|
||||
{
|
||||
$date = null;
|
||||
try {
|
||||
$a = explode(';', preg_replace('/\D+/', ';', trim($value)));
|
||||
switch (count($a)) {
|
||||
case 3: // yyyy;mm;dd
|
||||
$date = new \DateTime($a[0] . '-' . $a[1] . '-' . $a[2]);
|
||||
$date = $date->format('Y-m-d H:i:s');
|
||||
break;
|
||||
case 6: // yyyy;mm;dd;hh;mm;ss
|
||||
$date = new \DateTime($a[0] . '-' . $a[1] . '-' . $a[2] . ' ' . $a[3] . ':' . $a[4] . ':' . $a[5]);
|
||||
$date = $date->format('Y-m-d H:i:s');
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$date = null;
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
}
|
@@ -157,8 +157,8 @@ class databox_status
|
||||
* compute ((0 M s1) M s2) where M is the "mask" operator
|
||||
* nb : s1,s2 are binary mask strings as "01x0xx1xx0x", no other format (hex) supported
|
||||
*
|
||||
* @param $stat1 a binary mask "010x1xx0.." STRING
|
||||
* @param $stat2 a binary mask "x100x1..." STRING
|
||||
* @param string $stat1 a binary mask "010x1xx0.."
|
||||
* @param string $stat2 a binary mask "x100x1..."
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
|
174
lib/classes/patch/410alpha28a.php
Normal file
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Phraseanet
|
||||
*
|
||||
* (c) 2005-2020 Alchemy
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Alchemy\Phrasea\Application;
|
||||
|
||||
class patch_410alpha28a implements patchInterface
|
||||
{
|
||||
/** @var string */
|
||||
private $release = '4.1.0-alpha.28a';
|
||||
|
||||
/** @var array */
|
||||
private $concern = [base::APPLICATION_BOX];
|
||||
|
||||
/**
|
||||
* Returns the release version.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_release()
|
||||
{
|
||||
return $this->release;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function concern()
|
||||
{
|
||||
return $this->concern;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function require_all_upgrades()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDoctrineMigrations()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function apply(base $appbox, Application $app)
|
||||
{
|
||||
// add geoloc section if not exist
|
||||
if (!$app['conf']->has(['geocoding-providers'])) {
|
||||
$providers[0] = [
|
||||
'map-provider' => 'mapboxWebGL',
|
||||
'enabled' => false,
|
||||
'public-key' => '',
|
||||
'map-layers' => [
|
||||
0 => [
|
||||
'name' => 'Light',
|
||||
'value' => 'mapbox://styles/mapbox/light-v9'
|
||||
],
|
||||
1 => [
|
||||
'name' => 'Streets',
|
||||
'value' => 'mapbox://styles/mapbox/streets-v9'
|
||||
],
|
||||
2 => [
|
||||
'name' => 'Basic',
|
||||
'value' => 'mapbox://styles/mapbox/basic-v9'
|
||||
],
|
||||
3 => [
|
||||
'name' => 'Satellite',
|
||||
'value' => 'mapbox://styles/mapbox/satellite-v9'
|
||||
],
|
||||
4 => [
|
||||
'name' => 'Dark',
|
||||
'value' => 'mapbox://styles/mapbox/dark-v9'
|
||||
]
|
||||
],
|
||||
'transition-mapboxgl' => [
|
||||
0 => [
|
||||
'animate' => true,
|
||||
'speed' => '2.2',
|
||||
'curve' => '1.42'
|
||||
]
|
||||
],
|
||||
'default-position' => [
|
||||
'48.879162',
|
||||
'2.335062'
|
||||
],
|
||||
'default-zoom' => 5,
|
||||
'marker-default-zoom' => 9,
|
||||
'position-fields' => [],
|
||||
'geonames-field-mapping' => true,
|
||||
'cityfields' => 'City, Ville',
|
||||
'provincefields' => 'Province',
|
||||
'countryfields' => 'Country, Pays'
|
||||
];
|
||||
|
||||
$app['conf']->set(['geocoding-providers'], $providers);
|
||||
}
|
||||
|
||||
// add video-editor section if not exist
|
||||
if (!$app['conf']->has(['video-editor'])) {
|
||||
$videoEditor = [
|
||||
'ChapterVttFieldName' => 'VideoTextTrackChapters',
|
||||
'seekBackwardStep' => 500,
|
||||
'seekForwardStep' => 500,
|
||||
'playbackRates' => [
|
||||
1,
|
||||
'1.5',
|
||||
3
|
||||
]
|
||||
];
|
||||
|
||||
$app['conf']->set(['video-editor'], $videoEditor);
|
||||
}
|
||||
|
||||
// add api_token_header if not exist
|
||||
if (!$app['conf']->has(['main', 'api_token_header'])) {
|
||||
$app['conf']->set(['main', 'api_token_header'], false);
|
||||
}
|
||||
|
||||
// insert timeout if not exist
|
||||
if (!$app['conf']->has(['main', 'binaries', 'ffmpeg_timeout'])) {
|
||||
$app['conf']->set(['main', 'binaries', 'ffmpeg_timeout'], 3600);
|
||||
}
|
||||
if (!$app['conf']->has(['main', 'binaries', 'ffprobe_timeout'])) {
|
||||
$app['conf']->set(['main', 'binaries', 'ffprobe_timeout'], 60);
|
||||
}
|
||||
if (!$app['conf']->has(['main', 'binaries', 'gs_timeout'])) {
|
||||
$app['conf']->set(['main', 'binaries', 'gs_timeout'], 60);
|
||||
}
|
||||
if (!$app['conf']->has(['main', 'binaries', 'mp4box_timeout'])) {
|
||||
$app['conf']->set(['main', 'binaries', 'mp4box_timeout'], 60);
|
||||
}
|
||||
if (!$app['conf']->has(['main', 'binaries', 'swftools_timeout'])) {
|
||||
$app['conf']->set(['main', 'binaries', 'swftools_timeout'], 60);
|
||||
}
|
||||
if (!$app['conf']->has(['main', 'binaries', 'unoconv_timeout'])) {
|
||||
$app['conf']->set(['main', 'binaries', 'unoconv_timeout'], 60);
|
||||
}
|
||||
if (!$app['conf']->has(['main', 'binaries', 'exiftool_timeout'])) {
|
||||
$app['conf']->set(['main', 'binaries', 'exiftool_timeout'], 60);
|
||||
}
|
||||
|
||||
// custom-link section, remove default store
|
||||
$app['conf']->remove(['registry', 'custom-links', 0]);
|
||||
$app['conf']->remove(['registry', 'custom-links', 1]);
|
||||
|
||||
$customLinks = [
|
||||
'linkName' => 'Phraseanet store',
|
||||
'linkLanguage' => 'all',
|
||||
'linkUrl' => 'https://store.alchemy.fr',
|
||||
'linkLocation' => 'help-menu',
|
||||
'linkOrder' => 1,
|
||||
'linkBold' => false,
|
||||
'linkColor' => ''
|
||||
];
|
||||
|
||||
$app['conf']->set(['registry', 'custom-links', 0], $customLinks);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -10,6 +10,7 @@ main:
|
||||
maintenance: false
|
||||
key: ''
|
||||
api_require_ssl: true
|
||||
api_token_header: false
|
||||
database:
|
||||
host: 'sql-host'
|
||||
port: 3306
|
||||
|
30
lib/conf.d/json_schema/assets_enqueue.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "New assets",
|
||||
"description": "List of assets to enqueue on Phraseanet",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"assets": {
|
||||
"type": "array"
|
||||
},
|
||||
"publisher": {
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
},
|
||||
"base_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"commit_id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"assets",
|
||||
"publisher",
|
||||
"token",
|
||||
"base_url",
|
||||
"commit_id"
|
||||
]
|
||||
}
|
@@ -65,7 +65,7 @@
|
||||
"normalize-css": "^2.1.0",
|
||||
"npm": "^6.0.0",
|
||||
"npm-modernizr": "^2.8.3",
|
||||
"phraseanet-production-client": "0.34.205-d",
|
||||
"phraseanet-production-client": "0.34.211-d",
|
||||
"requirejs": "^2.3.5",
|
||||
"tinymce": "^4.0.28",
|
||||
"underscore": "^1.8.3",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:jms="urn:jms:translation" version="1.2">
|
||||
<file date="2020-05-15T10:02:54Z" source-language="en" target-language="de" datatype="plaintext" original="not.available">
|
||||
<file date="2020-05-27T06:38:24Z" source-language="en" target-language="de" datatype="plaintext" original="not.available">
|
||||
<header>
|
||||
<tool tool-id="JMSTranslationBundle" tool-name="JMSTranslationBundle" tool-version="1.1.0-DEV"/>
|
||||
<note>The source node in most cases contains the sample message as written by the developer. If it looks like a dot-delimitted string such as "form.label.firstname", then the developer has not provided a default message.</note>
|
||||
@@ -9,9 +9,9 @@
|
||||
<trans-unit id="96f0767cb7ea65a7f86c8c9432e80d16cf9d8680" resname="Please provide the same passwords." approved="yes">
|
||||
<source>Please provide the same passwords.</source>
|
||||
<target state="translated">Bitte geben Sie diesselbe Passwörter ein.</target>
|
||||
<jms:reference-file line="49">Form/Login/PhraseaRegisterForm.php</jms:reference-file>
|
||||
<jms:reference-file line="36">Form/Login/PhraseaRenewPasswordForm.php</jms:reference-file>
|
||||
<jms:reference-file line="44">Form/Login/PhraseaRecoverPasswordForm.php</jms:reference-file>
|
||||
<jms:reference-file line="49">Form/Login/PhraseaRegisterForm.php</jms:reference-file>
|
||||
</trans-unit>
|
||||
<trans-unit id="90b8c9717bb7ed061dbf20fe1986c8b8593d43d4" resname="The token provided is not valid anymore" approved="yes">
|
||||
<source>The token provided is not valid anymore</source>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:jms="urn:jms:translation" version="1.2">
|
||||
<file date="2020-05-15T10:03:18Z" source-language="en" target-language="en" datatype="plaintext" original="not.available">
|
||||
<file date="2020-05-27T06:39:51Z" source-language="en" target-language="en" datatype="plaintext" original="not.available">
|
||||
<header>
|
||||
<tool tool-id="JMSTranslationBundle" tool-name="JMSTranslationBundle" tool-version="1.1.0-DEV"/>
|
||||
<note>The source node in most cases contains the sample message as written by the developer. If it looks like a dot-delimitted string such as "form.label.firstname", then the developer has not provided a default message.</note>
|
||||
@@ -9,9 +9,9 @@
|
||||
<trans-unit id="96f0767cb7ea65a7f86c8c9432e80d16cf9d8680" resname="Please provide the same passwords." approved="yes">
|
||||
<source>Please provide the same passwords.</source>
|
||||
<target state="translated">Please provide the same passwords.</target>
|
||||
<jms:reference-file line="49">Form/Login/PhraseaRegisterForm.php</jms:reference-file>
|
||||
<jms:reference-file line="36">Form/Login/PhraseaRenewPasswordForm.php</jms:reference-file>
|
||||
<jms:reference-file line="44">Form/Login/PhraseaRecoverPasswordForm.php</jms:reference-file>
|
||||
<jms:reference-file line="49">Form/Login/PhraseaRegisterForm.php</jms:reference-file>
|
||||
</trans-unit>
|
||||
<trans-unit id="90b8c9717bb7ed061dbf20fe1986c8b8593d43d4" resname="The token provided is not valid anymore" approved="yes">
|
||||
<source>The token provided is not valid anymore</source>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:jms="urn:jms:translation" version="1.2">
|
||||
<file date="2020-05-15T10:03:43Z" source-language="en" target-language="fr" datatype="plaintext" original="not.available">
|
||||
<file date="2020-05-27T06:41:25Z" source-language="en" target-language="fr" datatype="plaintext" original="not.available">
|
||||
<header>
|
||||
<tool tool-id="JMSTranslationBundle" tool-name="JMSTranslationBundle" tool-version="1.1.0-DEV"/>
|
||||
<note>The source node in most cases contains the sample message as written by the developer. If it looks like a dot-delimitted string such as "form.label.firstname", then the developer has not provided a default message.</note>
|
||||
@@ -9,9 +9,9 @@
|
||||
<trans-unit id="96f0767cb7ea65a7f86c8c9432e80d16cf9d8680" resname="Please provide the same passwords." approved="yes">
|
||||
<source>Please provide the same passwords.</source>
|
||||
<target state="translated">Veuillez indiquer des mots de passe identiques.</target>
|
||||
<jms:reference-file line="49">Form/Login/PhraseaRegisterForm.php</jms:reference-file>
|
||||
<jms:reference-file line="36">Form/Login/PhraseaRenewPasswordForm.php</jms:reference-file>
|
||||
<jms:reference-file line="44">Form/Login/PhraseaRecoverPasswordForm.php</jms:reference-file>
|
||||
<jms:reference-file line="49">Form/Login/PhraseaRegisterForm.php</jms:reference-file>
|
||||
</trans-unit>
|
||||
<trans-unit id="90b8c9717bb7ed061dbf20fe1986c8b8593d43d4" resname="The token provided is not valid anymore" approved="yes">
|
||||
<source>The token provided is not valid anymore</source>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:jms="urn:jms:translation" version="1.2">
|
||||
<file date="2020-05-15T10:04:10Z" source-language="en" target-language="nl" datatype="plaintext" original="not.available">
|
||||
<file date="2020-05-27T06:43:55Z" source-language="en" target-language="nl" datatype="plaintext" original="not.available">
|
||||
<header>
|
||||
<tool tool-id="JMSTranslationBundle" tool-name="JMSTranslationBundle" tool-version="1.1.0-DEV"/>
|
||||
<note>The source node in most cases contains the sample message as written by the developer. If it looks like a dot-delimitted string such as "form.label.firstname", then the developer has not provided a default message.</note>
|
||||
@@ -9,9 +9,9 @@
|
||||
<trans-unit id="96f0767cb7ea65a7f86c8c9432e80d16cf9d8680" resname="Please provide the same passwords.">
|
||||
<source>Please provide the same passwords.</source>
|
||||
<target state="new">Please provide the same passwords.</target>
|
||||
<jms:reference-file line="49">Form/Login/PhraseaRegisterForm.php</jms:reference-file>
|
||||
<jms:reference-file line="36">Form/Login/PhraseaRenewPasswordForm.php</jms:reference-file>
|
||||
<jms:reference-file line="44">Form/Login/PhraseaRecoverPasswordForm.php</jms:reference-file>
|
||||
<jms:reference-file line="49">Form/Login/PhraseaRegisterForm.php</jms:reference-file>
|
||||
</trans-unit>
|
||||
<trans-unit id="90b8c9717bb7ed061dbf20fe1986c8b8593d43d4" resname="The token provided is not valid anymore">
|
||||
<source>The token provided is not valid anymore</source>
|
||||
|
1
resources/www/admin/images/cogs-solid.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg color="#5cb85c" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="cogs" class="svg-inline--fa fa-cogs fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M512.1 191l-8.2 14.3c-3 5.3-9.4 7.5-15.1 5.4-11.8-4.4-22.6-10.7-32.1-18.6-4.6-3.8-5.8-10.5-2.8-15.7l8.2-14.3c-6.9-8-12.3-17.3-15.9-27.4h-16.5c-6 0-11.2-4.3-12.2-10.3-2-12-2.1-24.6 0-37.1 1-6 6.2-10.4 12.2-10.4h16.5c3.6-10.1 9-19.4 15.9-27.4l-8.2-14.3c-3-5.2-1.9-11.9 2.8-15.7 9.5-7.9 20.4-14.2 32.1-18.6 5.7-2.1 12.1.1 15.1 5.4l8.2 14.3c10.5-1.9 21.2-1.9 31.7 0L552 6.3c3-5.3 9.4-7.5 15.1-5.4 11.8 4.4 22.6 10.7 32.1 18.6 4.6 3.8 5.8 10.5 2.8 15.7l-8.2 14.3c6.9 8 12.3 17.3 15.9 27.4h16.5c6 0 11.2 4.3 12.2 10.3 2 12 2.1 24.6 0 37.1-1 6-6.2 10.4-12.2 10.4h-16.5c-3.6 10.1-9 19.4-15.9 27.4l8.2 14.3c3 5.2 1.9 11.9-2.8 15.7-9.5 7.9-20.4 14.2-32.1 18.6-5.7 2.1-12.1-.1-15.1-5.4l-8.2-14.3c-10.4 1.9-21.2 1.9-31.7 0zm-10.5-58.8c38.5 29.6 82.4-14.3 52.8-52.8-38.5-29.7-82.4 14.3-52.8 52.8zM386.3 286.1l33.7 16.8c10.1 5.8 14.5 18.1 10.5 29.1-8.9 24.2-26.4 46.4-42.6 65.8-7.4 8.9-20.2 11.1-30.3 5.3l-29.1-16.8c-16 13.7-34.6 24.6-54.9 31.7v33.6c0 11.6-8.3 21.6-19.7 23.6-24.6 4.2-50.4 4.4-75.9 0-11.5-2-20-11.9-20-23.6V418c-20.3-7.2-38.9-18-54.9-31.7L74 403c-10 5.8-22.9 3.6-30.3-5.3-16.2-19.4-33.3-41.6-42.2-65.7-4-10.9.4-23.2 10.5-29.1l33.3-16.8c-3.9-20.9-3.9-42.4 0-63.4L12 205.8c-10.1-5.8-14.6-18.1-10.5-29 8.9-24.2 26-46.4 42.2-65.8 7.4-8.9 20.2-11.1 30.3-5.3l29.1 16.8c16-13.7 34.6-24.6 54.9-31.7V57.1c0-11.5 8.2-21.5 19.6-23.5 24.6-4.2 50.5-4.4 76-.1 11.5 2 20 11.9 20 23.6v33.6c20.3 7.2 38.9 18 54.9 31.7l29.1-16.8c10-5.8 22.9-3.6 30.3 5.3 16.2 19.4 33.2 41.6 42.1 65.8 4 10.9.1 23.2-10 29.1l-33.7 16.8c3.9 21 3.9 42.5 0 63.5zm-117.6 21.1c59.2-77-28.7-164.9-105.7-105.7-59.2 77 28.7 164.9 105.7 105.7zm243.4 182.7l-8.2 14.3c-3 5.3-9.4 7.5-15.1 5.4-11.8-4.4-22.6-10.7-32.1-18.6-4.6-3.8-5.8-10.5-2.8-15.7l8.2-14.3c-6.9-8-12.3-17.3-15.9-27.4h-16.5c-6 0-11.2-4.3-12.2-10.3-2-12-2.1-24.6 0-37.1 1-6 6.2-10.4 12.2-10.4h16.5c3.6-10.1 9-19.4 15.9-27.4l-8.2-14.3c-3-5.2-1.9-11.9 2.8-15.7 9.5-7.9 20.4-14.2 32.1-18.6 5.7-2.1 12.1.1 15.1 5.4l8.2 14.3c10.5-1.9 21.2-1.9 31.7 0l8.2-14.3c3-5.3 9.4-7.5 15.1-5.4 11.8 4.4 22.6 10.7 32.1 18.6 4.6 3.8 5.8 10.5 2.8 15.7l-8.2 14.3c6.9 8 12.3 17.3 15.9 27.4h16.5c6 0 11.2 4.3 12.2 10.3 2 12 2.1 24.6 0 37.1-1 6-6.2 10.4-12.2 10.4h-16.5c-3.6 10.1-9 19.4-15.9 27.4l8.2 14.3c3 5.2 1.9 11.9-2.8 15.7-9.5 7.9-20.4 14.2-32.1 18.6-5.7 2.1-12.1-.1-15.1-5.4l-8.2-14.3c-10.4 1.9-21.2 1.9-31.7 0zM501.6 431c38.5 29.6 82.4-14.3 52.8-52.8-38.5-29.6-82.4 14.3-52.8 52.8z"></path></svg>
|
After Width: | Height: | Size: 2.6 KiB |
1
resources/www/admin/images/database-solid-blue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg color="#3b99fc" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="database" class="svg-inline--fa fa-database fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M448 73.143v45.714C448 159.143 347.667 192 224 192S0 159.143 0 118.857V73.143C0 32.857 100.333 0 224 0s224 32.857 224 73.143zM448 176v102.857C448 319.143 347.667 352 224 352S0 319.143 0 278.857V176c48.125 33.143 136.208 48.572 224 48.572S399.874 209.143 448 176zm0 160v102.857C448 479.143 347.667 512 224 512S0 479.143 0 438.857V336c48.125 33.143 136.208 48.572 224 48.572S399.874 369.143 448 336z"></path></svg>
|
After Width: | Height: | Size: 649 B |
1
resources/www/admin/images/database-solid.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="database" class="svg-inline--fa fa-database fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M448 73.143v45.714C448 159.143 347.667 192 224 192S0 159.143 0 118.857V73.143C0 32.857 100.333 0 224 0s224 32.857 224 73.143zM448 176v102.857C448 319.143 347.667 352 224 352S0 319.143 0 278.857V176c48.125 33.143 136.208 48.572 224 48.572S399.874 209.143 448 176zm0 160v102.857C448 479.143 347.667 512 224 512S0 479.143 0 438.857V336c48.125 33.143 136.208 48.572 224 48.572S399.874 369.143 448 336z"></path></svg>
|
After Width: | Height: | Size: 633 B |
1
resources/www/admin/images/searchengin.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="searchengin" class="svg-inline--fa fa-searchengin fa-w-15" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 460 512"><path fill="currentColor" d="M220.6 130.3l-67.2 28.2V43.2L98.7 233.5l54.7-24.2v130.3l67.2-209.3zm-83.2-96.7l-1.3 4.7-15.2 52.9C80.6 106.7 52 145.8 52 191.5c0 52.3 34.3 95.9 83.4 105.5v53.6C57.5 340.1 0 272.4 0 191.6c0-80.5 59.8-147.2 137.4-158zm311.4 447.2c-11.2 11.2-23.1 12.3-28.6 10.5-5.4-1.8-27.1-19.9-60.4-44.4-33.3-24.6-33.6-35.7-43-56.7-9.4-20.9-30.4-42.6-57.5-52.4l-9.7-14.7c-24.7 16.9-53 26.9-81.3 28.7l2.1-6.6 15.9-49.5c46.5-11.9 80.9-54 80.9-104.2 0-54.5-38.4-102.1-96-107.1V32.3C254.4 37.4 320 106.8 320 191.6c0 33.6-11.2 64.7-29 90.4l14.6 9.6c9.8 27.1 31.5 48 52.4 57.4s32.2 9.7 56.8 43c24.6 33.2 42.7 54.9 44.5 60.3s.7 17.3-10.5 28.5zm-9.9-17.9c0-4.4-3.6-8-8-8s-8 3.6-8 8 3.6 8 8 8 8-3.6 8-8z"></path></svg>
|
After Width: | Height: | Size: 919 B |
1
resources/www/admin/images/tools-solid.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="tools" class="svg-inline--fa fa-tools fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M501.1 395.7L384 278.6c-23.1-23.1-57.6-27.6-85.4-13.9L192 158.1V96L64 0 0 64l96 128h62.1l106.6 106.6c-13.6 27.8-9.2 62.3 13.9 85.4l117.1 117.1c14.6 14.6 38.2 14.6 52.7 0l52.7-52.7c14.5-14.6 14.5-38.2 0-52.7zM331.7 225c28.3 0 54.9 11 74.9 31l19.4 19.4c15.8-6.9 30.8-16.5 43.8-29.5 37.1-37.1 49.7-89.3 37.9-136.7-2.2-9-13.5-12.1-20.1-5.5l-74.4 74.4-67.9-11.3L334 98.9l74.4-74.4c6.6-6.6 3.4-17.9-5.7-20.2-47.4-11.7-99.6.9-136.6 37.9-28.5 28.5-41.9 66.1-41.2 103.6l82.1 82.1c8.1-1.9 16.5-2.9 24.7-2.9zm-103.9 82l-56.7-56.7L18.7 402.8c-25 25-25 65.5 0 90.5s65.5 25 90.5 0l123.6-123.6c-7.6-19.9-9.9-41.6-5-62.7zM64 472c-13.2 0-24-10.8-24-24 0-13.3 10.7-24 24-24s24 10.7 24 24c0 13.2-10.7 24-24 24z"></path></svg>
|
After Width: | Height: | Size: 921 B |
@@ -189,6 +189,43 @@ $mainMenuLinkBackgroundHoverColor: transparent;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.select-all-line {
|
||||
color: #afafaf;
|
||||
cursor: pointer;
|
||||
}
|
||||
.users_check_line_wrap {
|
||||
position: absolute;
|
||||
div {
|
||||
padding: 5px 10px;
|
||||
background-color: #cccccc;
|
||||
color: #000000;
|
||||
cursor: pointer;
|
||||
line-height: 1.7;
|
||||
|
||||
&:hover {
|
||||
padding: 5px 10px;
|
||||
background-color: #4d4d4d;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fa-right {
|
||||
color: #2475b5;
|
||||
font-size: 16px;
|
||||
.select-all-line-btn & {
|
||||
padding-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.without-border {
|
||||
margin-left: 15px;
|
||||
outline: none;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
/******* EDIT USERS ***********************************************************/
|
||||
div.no_switch {
|
||||
width: 12px;
|
||||
@@ -211,7 +248,7 @@ div.switch_quota.mixed,
|
||||
div.switch_masks.mixed,
|
||||
div.switch_time.mixed,
|
||||
div.switch_right.mixed {
|
||||
background-image: url('#{$iconsPath}ccoch2.gif');
|
||||
background-image: url('#{$iconsPath}square-duotone.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
}
|
||||
@@ -219,7 +256,7 @@ div.switch_quota.checked,
|
||||
div.switch_masks.checked,
|
||||
div.switch_time.checked,
|
||||
div.switch_right.checked {
|
||||
background-image: url('#{$iconsPath}ccoch1.gif');
|
||||
background-image: url('#{$iconsPath}check-square-regular.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
}
|
||||
@@ -227,7 +264,7 @@ div.switch_quota.unchecked,
|
||||
div.switch_masks.unchecked,
|
||||
div.switch_time.unchecked,
|
||||
div.switch_right.unchecked {
|
||||
background-image: url('#{$iconsPath}ccoch0.gif');
|
||||
background-image: url('#{$iconsPath}square-regular.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
}
|
||||
@@ -912,6 +949,12 @@ span.simplecolorpicker.picker {
|
||||
.ui-dialog-titlebar-close {
|
||||
text-indent: -9999999px;
|
||||
}
|
||||
.worker-info-block {
|
||||
.btn-primary {
|
||||
min-width: 161px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
@import './databases';
|
||||
@import './fields';
|
||||
@import './tables';
|
||||
|
@@ -5,3 +5,20 @@
|
||||
@import '../../account/styles/skin';
|
||||
@import '../../../../node_modules/font-awesome/scss/font-awesome.scss';
|
||||
@import '../../../../node_modules/jquery-ui/themes/base/autocomplete'; // not extension for inline import
|
||||
|
||||
|
||||
.authentication-sidebar {
|
||||
.alert {
|
||||
.alert-block-logo {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.fa-2x {
|
||||
font-size: 2em;
|
||||
height: 28px;
|
||||
margin: 0;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="check-square" class="svg-inline--fa fa-check-square fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M400 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V80c0-26.51-21.49-48-48-48zm0 400H48V80h352v352zm-35.864-241.724L191.547 361.48c-4.705 4.667-12.303 4.637-16.97-.068l-90.781-91.516c-4.667-4.705-4.637-12.303.069-16.971l22.719-22.536c4.705-4.667 12.303-4.637 16.97.069l59.792 60.277 141.352-140.216c4.705-4.667 12.303-4.637 16.97.068l22.536 22.718c4.667 4.706 4.637 12.304-.068 16.971z"></path></svg>
|
After Width: | Height: | Size: 664 B |
1
resources/www/common/images/icons/square-duotone.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg color="#aaa8a5" aria-hidden="true" focusable="false" data-prefix="fa" data-icon="square" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" class="svg-inline--fa fa-square fa-w-14 fa-2x"><g class="fa-group"><path fill="currentColor" d="M400 32H48A48 48 0 0 0 0 80v352a48 48 0 0 0 48 48h352a48 48 0 0 0 48-48V80a48 48 0 0 0-48-48zm-16 368a16 16 0 0 1-16 16H80a16 16 0 0 1-16-16V112a16 16 0 0 1 16-16h288a16 16 0 0 1 16 16z" class="fa-secondary"></path><path fill="currentColor" d="M64 400V112a16 16 0 0 1 16-16h288a16 16 0 0 1 16 16v288a16 16 0 0 1-16 16H80a16 16 0 0 1-16-16z" class="fa-primary"></path></g></svg>
|
After Width: | Height: | Size: 635 B |
1
resources/www/common/images/icons/square-regular.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="square" class="svg-inline--fa fa-square fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-6 400H54c-3.3 0-6-2.7-6-6V86c0-3.3 2.7-6 6-6h340c3.3 0 6 2.7 6 6v340c0 3.3-2.7 6-6 6z"></path></svg>
|
After Width: | Height: | Size: 424 B |
@@ -61,24 +61,6 @@
|
||||
div.no_switch{
|
||||
background-image:url(/assets/common/images/icons/ccoch5.gif);
|
||||
}
|
||||
div.switch_quota.mixed,
|
||||
div.switch_masks.mixed,
|
||||
div.switch_time.mixed,
|
||||
div.switch_right.mixed{
|
||||
background-image:url(/assets/common/images/icons/ccoch2.gif);
|
||||
}
|
||||
div.switch_quota.checked,
|
||||
div.switch_masks.checked,
|
||||
div.switch_time.checked,
|
||||
div.switch_right.checked{
|
||||
background-image:url(/assets/common/images/icons/ccoch1.gif);
|
||||
}
|
||||
div.switch_quota.unchecked,
|
||||
div.switch_masks.unchecked,
|
||||
div.switch_time.unchecked,
|
||||
div.switch_right.unchecked{
|
||||
background-image:url(/assets/common/images/icons/ccoch0.gif);
|
||||
}
|
||||
td.users_col{
|
||||
vertical-align:middle;
|
||||
text-align:center;
|
||||
@@ -137,40 +119,20 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table cellspacing="0" cellpadding="0" border="0" style="table-layout: auto;width:820px;height:67px;bottom:auto;top:50px;" class="">
|
||||
<table cellspacing="0" cellpadding="0" border="0" style="table-layout: auto;width:850px;height:67px;bottom:auto;top:50px;" class="">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:122px;">
|
||||
<th style="width:168px;">
|
||||
|
||||
</th>
|
||||
<th colspan="26">
|
||||
<img src="/assets/common/images/lng/inclin-{{app['locale']}}.png" style="width:698px"/>
|
||||
<th colspan="25">
|
||||
<img src="/assets/common/images/lng/inclin-{{app['locale']}}.png" style="width:682px"/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
</td>
|
||||
<td colspan="26">
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="" style="bottom:40px;top:127px;overflow-y:auto;overflow-x:hidden;width:850px;max-height: 450px;">
|
||||
<table class="hoverable" cellspacing="0" cellpadding="0" border="0" style="table-layout: fixed;width:820px;">
|
||||
<!-- <thead>
|
||||
<tr>
|
||||
<th style="width:122px;">
|
||||
|
||||
</th>
|
||||
<th colspan="26">
|
||||
<img src="/assets/common/images/lng/inclin-{{app['locale']}}.gif" >
|
||||
</th>
|
||||
</tr>
|
||||
</thead>-->
|
||||
<div class="" style="bottom:40px;top:127px;overflow-y:auto;overflow-x:hidden;width:870px;max-height: 450px;">
|
||||
<table class="hoverable" cellspacing="0" cellpadding="0" border="0" style="table-layout: fixed;width:840px;">
|
||||
<tbody>
|
||||
{% set sbas = '' %}
|
||||
{% for rights in datas %}
|
||||
@@ -183,54 +145,55 @@
|
||||
{% endif %}
|
||||
|
||||
<tr>
|
||||
<td style="width:122px;overflow:hidden;white-space:nowrap;">
|
||||
<td style="width:140px;overflow:hidden;white-space:nowrap;">
|
||||
{{rights['sbas_id']|sbas_labels(app)}}
|
||||
</td>
|
||||
<td style="width:25px"></td>
|
||||
<td class="users_col options">
|
||||
<div>
|
||||
▼
|
||||
<i class="fa fa-caret-down fa-right"></i>
|
||||
<input type="hidden" name="right" value="right_access"/>
|
||||
<input type="hidden" name="sbas_id" value="{{rights['sbas_id']}}"/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="users_col options">
|
||||
<div>
|
||||
▼
|
||||
<i class="fa fa-caret-down fa-right"></i>
|
||||
<input type="hidden" name="right" value="right_actif"/>
|
||||
<input type="hidden" name="sbas_id" value="{{rights['sbas_id']}}"/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="users_col options">
|
||||
<div>
|
||||
▼
|
||||
<i class="fa fa-caret-down fa-right"></i>
|
||||
<input type="hidden" name="right" value="right_canputinalbum"/>
|
||||
<input type="hidden" name="sbas_id" value="{{rights['sbas_id']}}"/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="users_col options">
|
||||
<div>
|
||||
▼
|
||||
<i class="fa fa-caret-down fa-right"></i>
|
||||
<input type="hidden" name="right" value="right_candwnldpreview"/>
|
||||
<input type="hidden" name="sbas_id" value="{{rights['sbas_id']}}"/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="users_col options">
|
||||
<div>
|
||||
▼
|
||||
<i class="fa fa-caret-down fa-right"></i>
|
||||
<input type="hidden" name="right" value="right_nowatermark"/>
|
||||
<input type="hidden" name="sbas_id" value="{{rights['sbas_id']}}"/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="users_col options">
|
||||
<div>
|
||||
▼
|
||||
<i class="fa fa-caret-down fa-right"></i>
|
||||
<input type="hidden" name="right" value="right_candwnldhd"/>
|
||||
<input type="hidden" name="sbas_id" value="{{rights['sbas_id']}}"/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="users_col options">
|
||||
<div>
|
||||
▼
|
||||
<i class="fa fa-caret-down fa-right"></i>
|
||||
<input type="hidden" name="right" value="right_cancmd"/>
|
||||
<input type="hidden" name="sbas_id" value="{{rights['sbas_id']}}"/>
|
||||
</div>
|
||||
@@ -247,117 +210,127 @@
|
||||
<td class="users_col">
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<td width="97" style="width: 97px;">
|
||||
|
||||
</td>
|
||||
<td class="users_col options">
|
||||
<div>
|
||||
▼
|
||||
<i class="fa fa-caret-down fa-right"></i>
|
||||
<input type="hidden" name="right" value="right_canaddrecord"/>
|
||||
<input type="hidden" name="sbas_id" value="{{rights['sbas_id']}}"/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="users_col options">
|
||||
<div>
|
||||
▼
|
||||
<i class="fa fa-caret-down fa-right"></i>
|
||||
<input type="hidden" name="right" value="right_canmodifrecord"/>
|
||||
<input type="hidden" name="sbas_id" value="{{rights['sbas_id']}}"/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="users_col options">
|
||||
<div>
|
||||
▼
|
||||
<i class="fa fa-caret-down fa-right"></i>
|
||||
<input type="hidden" name="right" value="right_chgstatus"/>
|
||||
<input type="hidden" name="sbas_id" value="{{rights['sbas_id']}}"/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="users_col options">
|
||||
<div>
|
||||
▼
|
||||
<i class="fa fa-caret-down fa-right"></i>
|
||||
<input type="hidden" name="right" value="right_candeleterecord"/>
|
||||
<input type="hidden" name="sbas_id" value="{{rights['sbas_id']}}"/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="users_col options">
|
||||
<div>
|
||||
▼
|
||||
<i class="fa fa-caret-down fa-right"></i>
|
||||
<input type="hidden" name="right" value="right_imgtools"/>
|
||||
<input type="hidden" name="sbas_id" value="{{rights['sbas_id']}}"/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="users_col options">
|
||||
<div>
|
||||
▼
|
||||
<i class="fa fa-caret-down fa-right"></i>
|
||||
<input type="hidden" name="right" value="right_canadmin"/>
|
||||
<input type="hidden" name="sbas_id" value="{{rights['sbas_id']}}"/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="users_col options">
|
||||
<div>
|
||||
▼
|
||||
<i class="fa fa-caret-down fa-right"></i>
|
||||
<input type="hidden" name="right" value="right_canreport"/>
|
||||
<input type="hidden" name="sbas_id" value="{{rights['sbas_id']}}"/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="users_col options">
|
||||
<div>
|
||||
▼
|
||||
<i class="fa fa-caret-down fa-right"></i>
|
||||
<input type="hidden" name="right" value="right_canpush"/>
|
||||
<input type="hidden" name="sbas_id" value="{{rights['sbas_id']}}"/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="users_col options">
|
||||
<div>
|
||||
▼
|
||||
<i class="fa fa-caret-down fa-right"></i>
|
||||
<input type="hidden" name="right" value="right_manage"/>
|
||||
<input type="hidden" name="sbas_id" value="{{rights['sbas_id']}}"/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="users_col options">
|
||||
<div>
|
||||
▼
|
||||
<i class="fa fa-caret-down fa-right"></i>
|
||||
<input type="hidden" name="right" value="right_modify_struct"/>
|
||||
<input type="hidden" name="sbas_id" value="{{rights['sbas_id']}}"/>
|
||||
</div>
|
||||
</td>
|
||||
<td style="text-align:center;width:19px;" title="{{ 'Allowed to publish' | trans }}">
|
||||
<td class="main-right-item" style="text-align:center;width:19px;" title="{{ 'Allowed to publish' | trans }}">
|
||||
{{_self.format_checkbox(app.getAuthenticatedUser(), rights, constant('\\ACL::BAS_CHUPUB'), users, 'sbas')}}
|
||||
</td>
|
||||
<td style="text-align:center;width:19px;" title="{{ 'Manage Thesaurus' | trans }}">
|
||||
<td class="main-right-item" style="text-align:center;width:19px;" title="{{ 'Manage Thesaurus' | trans }}">
|
||||
{{_self.format_checkbox(app.getAuthenticatedUser(), rights, constant('\\ACL::BAS_MODIF_TH'), users, 'sbas')}}
|
||||
</td>
|
||||
<td style="text-align:center;width:19px;" title="{{ 'Manage Database' | trans }}">
|
||||
<td class="main-right-item" style="text-align:center;width:19px;" title="{{ 'Manage Database' | trans }}">
|
||||
{{_self.format_checkbox(app.getAuthenticatedUser(), rights, constant('\\ACL::BAS_MANAGE'), users, 'sbas')}}
|
||||
</td>
|
||||
<td style="text-align:center;width:19px;" title="{{ 'Manage DB fields' | trans }}">
|
||||
<td class="main-right-item" style="text-align:center;width:19px;" title="{{ 'Manage DB fields' | trans }}">
|
||||
{{_self.format_checkbox(app.getAuthenticatedUser(), rights, constant('\\ACL::BAS_MODIFY_STRUCT'), users, 'sbas')}}
|
||||
</td>
|
||||
<td style="text-align:center;width:48px;"></td>
|
||||
<td style="text-align:center;width:48px;">
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td style="overflow:hidden;white-space:nowrap;">
|
||||
<tr class="right-items">
|
||||
<td style="width: 140px;overflow:hidden;white-space:nowrap;" title="{{rights['base_id']|bas_labels(app)}}">
|
||||
{{rights['base_id']|bas_labels(app)}}
|
||||
</td>
|
||||
<td class="users_col case_right_access" title="{{ 'Access' | trans }}">
|
||||
<td class="select-all-line" title="{{'admin::users:edit: Manage inline selection' | trans }}" style="width: 25px">
|
||||
<div class="select-all-line-btn">
|
||||
<i class="fa fa-caret-right fa-right"></i>
|
||||
</div>
|
||||
<div class="users_check_line_wrap hide">
|
||||
<div class="checker check_left_right">{{'admin::users:edit: check read right' | trans }}</div>
|
||||
<div class="unchecker check-all_right">{{'admin::users:edit: check all right' | trans }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="users_col case_right_access left-right" title="{{ 'Access' | trans }}">
|
||||
{{_self.format_checkbox(app.getAuthenticatedUser(), rights, constant('\\ACL::ACCESS'), users, 'base')}}
|
||||
</td>
|
||||
<td class="users_col case_right_actif" title="{{ 'Active' | trans }}">
|
||||
<td class="users_col case_right_actif left-right" title="{{ 'Active' | trans }}">
|
||||
{{_self.format_checkbox(app.getAuthenticatedUser(), rights, constant('\\ACL::ACTIF'), users, 'base')}}
|
||||
</td>
|
||||
<td class="users_col case_right_canputinalbum" title="{{ 'Allowed to add in basket' | trans }}">
|
||||
<td class="users_col case_right_canputinalbum left-right" title="{{ 'Allowed to add in basket' | trans }}">
|
||||
{{_self.format_checkbox(app.getAuthenticatedUser(), rights, constant('\\ACL::CANPUTINALBUM'), users, 'base')}}
|
||||
</td>
|
||||
<td class="users_col case_right_candwnldpreview" title="{{ 'Access to preview' | trans }}">
|
||||
<td class="users_col case_right_candwnldpreview left-right" title="{{ 'Access to preview' | trans }}">
|
||||
{{_self.format_checkbox(app.getAuthenticatedUser(), rights, constant('\\ACL::CANDWNLDPREVIEW'), users, 'base')}}
|
||||
</td>
|
||||
<td class="users_col case_right_nowatermark" title="{{ 'Remove watermark' | trans }}">
|
||||
<td class="users_col case_right_nowatermark left-right" title="{{ 'Remove watermark' | trans }}">
|
||||
{{_self.format_checkbox(app.getAuthenticatedUser(), rights, constant('\\ACL::NOWATERMARK'), users, 'base')}}
|
||||
</td>
|
||||
<td class="users_col case_right_candwnldhd" title="{{ 'Access to HD' | trans }}">
|
||||
<td class="users_col case_right_candwnldhd left-right" title="{{ 'Access to HD' | trans }}">
|
||||
{{_self.format_checkbox(app.getAuthenticatedUser(), rights, constant('\\ACL::CANDWNLDHD'), users, 'base')}}
|
||||
</td>
|
||||
<td class="users_col case_right_cancmd" title="{{ 'Allowed to order' | trans }}">
|
||||
<td class="users_col case_right_cancmd left-right" title="{{ 'Allowed to order' | trans }}">
|
||||
{{_self.format_checkbox(app.getAuthenticatedUser(), rights, constant('\\ACL::CANCMD'), users, 'base')}}
|
||||
</td>
|
||||
<td class="users_col case_right_quota" title="{{ 'Set download quotas' | trans }}">
|
||||
@@ -391,7 +364,7 @@
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td style="text-align:center;width:100px;"></td>
|
||||
<td style="text-align:center;width:97px;"></td>
|
||||
|
||||
<td class="users_col case_right_canaddrecord" title="{{ 'Allowed to add' | trans }}">
|
||||
{{_self.format_checkbox(app.getAuthenticatedUser(), rights, constant('\\ACL::CANADDRECORD'), users, 'base')}}
|
||||
@@ -423,8 +396,8 @@
|
||||
<td class="users_col case_right_modify" title="{{ 'Manage values lists' | trans }}">
|
||||
{{_self.format_checkbox(app.getAuthenticatedUser(), rights, constant('\\ACL::COLL_MODIFY_STRUCT'), users, 'base')}}
|
||||
</td>
|
||||
<td colspan="5">
|
||||
|
||||
<td colspan="5">
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -1099,6 +1072,31 @@
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$('body').click(function() {
|
||||
$('.users_check_line_wrap').addClass('hide');
|
||||
});
|
||||
$('.select-all-line-btn').click(function (event) {
|
||||
event.stopPropagation();
|
||||
var top = $(this).offset().top - 50;
|
||||
var left = $(this).offset().left + 60;
|
||||
$('.users_check_line_wrap').addClass('hide');
|
||||
$(this).closest('.select-all-line').find('.users_check_line_wrap').removeClass('hide').css('top', +top+'px');
|
||||
|
||||
});
|
||||
$('.check_left_right').click(function () {
|
||||
$(this).closest('.right-items').find('.left-right div').show().removeClass('unchecked').addClass('checked');
|
||||
$(this).closest('.right-items').find('.left-right input').val(1);
|
||||
$('.users_check_line_wrap').addClass('hide');
|
||||
});
|
||||
|
||||
$('.check-all_right').click(function () {
|
||||
$(this).closest('.right-items').find('div').show().removeClass('unchecked').addClass('checked');
|
||||
$(this).closest('.right-items').find('input').val(1);
|
||||
$('.users_check_line_wrap').addClass('hide');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
require([
|
||||
|
@@ -42,6 +42,8 @@
|
||||
serverDisconnected: '{{ 'phraseanet::erreur: Votre session est fermee, veuillez vous re-authentifier' | trans | e('js') }}',
|
||||
check_all : '{{ 'Cocher toute la colonne' | trans | e('js') }}',
|
||||
uncheck_all : '{{ 'Decocher toute la colonne' | trans | e('js') }}',
|
||||
check_left_right : '{{ 'admin::users:edit: check left right' | trans | e('js') }}',
|
||||
check_all_right : '{{ 'admin::users:edit: check all right' | trans | e('js') }}',
|
||||
create_template : '{{ 'Creer un model' | trans | e('js') }}',
|
||||
create_user : '{{ 'Creer un utilisateur' | trans | e('js') }}',
|
||||
annuler : '{{ 'boutton::annuler' | trans | e('js') }}',
|
||||
|
@@ -18,12 +18,13 @@
|
||||
{% if app.getAclForUser(app.getAuthenticatedUser()).is_admin() %}
|
||||
<li>
|
||||
<a target="right" href="{{ path('setup_display_globals') }}" class="ajax">
|
||||
<img src="/assets/admin/images/Setup.png" />
|
||||
<img width="16" src="/assets/admin/images/tools-solid.svg" />
|
||||
<span>{% trans %}Setup{% endtrans %}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a target="right" href="{{ path('admin_searchengine_form') }}">
|
||||
<img width="16" src="/assets/admin/images/searchengin.svg" />
|
||||
<span>{{ 'SearchEngine settings' | trans }}</span>
|
||||
</a>
|
||||
</li>
|
||||
@@ -76,10 +77,19 @@
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if app.getAclForUser(app.getAuthenticatedUser()).has_right(constant('\\ACL::TASKMANAGER')) %}
|
||||
<li class="{% if feature == 'workermanager' %}selected{% endif %}">
|
||||
<a target="right" href="{{ path('worker_admin') }}" class="ajax">
|
||||
<img width="16" src="/assets/admin/images/cogs-solid.svg" />
|
||||
<span>{{ 'Worker Manager' | trans }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="open">
|
||||
<div class="{% if feature == 'bases' %}selected{% endif %}" style="padding:0 0 2px 0;">
|
||||
<a id="TREE_DATABASES" target="right" href="{{ path('admin_databases') }}" class="ajax">
|
||||
<img src="/assets/admin/images/DatabasesAvailable.png" />
|
||||
<img width="16" src="/assets/admin/images/database-solid-blue.svg" />
|
||||
<span>{{ 'admin::utilisateurs: bases de donnees' | trans }}</span>
|
||||
</a>
|
||||
</div>
|
||||
@@ -104,7 +114,7 @@
|
||||
<li class="{% if this_is_open %}open{% endif %}">
|
||||
<div style="padding:0 0 2px 0;" class="{% if this_is_selected %}selected{% endif %}">
|
||||
<a target="right" href="{{ path('admin_database', { 'databox_id' : sbas_id }) }}" class="ajax">
|
||||
<img src="/assets/admin/images/Database.png"/>
|
||||
<img width="16" src="/assets/admin/images/database-solid.svg"/>
|
||||
<span>{{ databox.get_label(app['locale']) }}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
@@ -46,7 +46,10 @@
|
||||
<option {% if parm['like_field'] == "company" %}selected="selected"{% endif %} value="company">{{ 'Push::filter on companies' | trans }}</option>
|
||||
<option {% if parm['like_field'] == "email" %}selected="selected"{% endif %} value="email">{{ 'Push::filter on emails' | trans }}</option>
|
||||
</select>
|
||||
<span>{{ 'Push::filter starts' | trans }}</span>
|
||||
<select name="like_type" class="input-medium without-border">
|
||||
<option selected="selected" value="content">{{ 'admin::users:filter: content' | trans }}</option>
|
||||
<option value="start_with">{{ 'admin::users:filter: start with' | trans }}</option>
|
||||
</select>
|
||||
<input type="text" value="{{parm['like_value']}}" name="like_value" class="input-medium">
|
||||
<span>{{ 'Last applied template' | trans }}</span>
|
||||
<select name="last_model" class="input-medium">
|
||||
|
91
templates/web/admin/worker-manager/index.html.twig
Normal file
@@ -0,0 +1,91 @@
|
||||
<h1>Worker</h1>
|
||||
|
||||
{% if isConnected %}
|
||||
<div>
|
||||
<!-- Nav tabs -->
|
||||
<ul class="nav nav-tabs" id="configurationTabs">
|
||||
|
||||
<li class="worker-configuration" role="presentation">
|
||||
<a href="#worker-configuration" aria-controls="worker-configuration" role="tab" data-toggle="tab" data-url="/admin/worker-manager/configuration">
|
||||
{{ "admin::workermanager:tab:configuration: title" | trans }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="worker-info active" role="presentation">
|
||||
<a href="#worker-info" aria-controls="worker-info" role="tab" data-toggle="tab" data-url="/admin/worker-manager/info">
|
||||
{{ 'admin::workermanager:tab:workerinfo: title' |trans }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="worker-searchengine" role="presentation">
|
||||
<a href="#worker-searchengine" aria-controls="worker-searchengine" role="tab" data-toggle="tab" data-url="/admin/worker-manager/searchengine">
|
||||
{{ 'admin::workermanager:tab:searchengine: title' |trans }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="worker-pull-assets" role="presentation">
|
||||
<a href="#worker-pull-assets" aria-controls="worker-pull-assets" role="tab" data-toggle="tab" data-url="/admin/worker-manager/pull-assets">
|
||||
{{ 'admin::workermanager:tab:pullassets: title' |trans }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="worker-subview" role="presentation">
|
||||
<a href="#worker-subview" aria-controls="worker-subview" role="tab" data-toggle="tab" data-url="/admin/worker-manager/subview">
|
||||
{{ 'admin::workermanager:tab:subview: title' |trans }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="worker-metadata" role="presentation">
|
||||
<a href="#worker-metadata" aria-controls="worker-metadata" role="tab" data-toggle="tab" data-url="/admin/worker-manager/metadata">
|
||||
{{ 'admin::workermanager:tab:metadata: title' |trans }}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<!-- Tab panes -->
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane fade" id="worker-configuration"></div>
|
||||
<div role="tabpanel" class="tab-pane fade in active" id="worker-info">
|
||||
{% include "admin/worker-manager/worker_info.html.twig" %}
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane fade" id="worker-searchengine"></div>
|
||||
<div role="tabpanel" class="tab-pane fade" id="worker-pull-assets"></div>
|
||||
<div role="tabpanel" class="tab-pane fade" id="worker-subview"></div>
|
||||
<div role="tabpanel" class="tab-pane fade" id="worker-metadata"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
var contentsDownloaded = {};
|
||||
var remoteContent = function(url) {
|
||||
return $.get(url);
|
||||
};
|
||||
|
||||
var tabs = $('#configurationTabs a[data-toggle="tab"]');
|
||||
|
||||
tabs.on('click', function(){
|
||||
$(this).tab('show');
|
||||
});
|
||||
|
||||
$('.nav-tabs li').on('show.bs.tab', function (e) {
|
||||
if (contentsDownloaded[e.target.hash] === undefined) {
|
||||
$(e.target.hash).empty().html('<img src="/assets/common/images/icons/main-loader.gif" alt="loading"/>');
|
||||
}
|
||||
});
|
||||
|
||||
$('.nav-tabs').on('shown.bs.tab', function (e) {
|
||||
|
||||
if (contentsDownloaded[e.target.hash] === undefined) {
|
||||
var targetDiv = $(e.target.hash);
|
||||
|
||||
remoteContent($(e.target).attr('data-url')).then(function(response) {
|
||||
targetDiv.empty().html(response);
|
||||
contentsDownloaded[e.target.hash] = true;
|
||||
}, function(error) {
|
||||
console.log(error);
|
||||
targetDiv.empty().html('<i class="icon-fire">{{ 'admin:worker Retrieve configuration error'|trans }}</i>');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% else %}
|
||||
<h1 class="alert alert-danger">
|
||||
{{ 'admin::workermanager: Rabbit config error' |trans }}
|
||||
</h1>
|
||||
{% endif %}
|