Merge branch 'master' into PHRAS-3049_more_setup_in_docker-compose

This commit is contained in:
Nicolas Maillat
2020-05-29 00:46:01 +02:00
committed by GitHub
116 changed files with 10072 additions and 2554 deletions

View File

@@ -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

View File

@@ -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();

View File

@@ -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
View File

@@ -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",

View File

@@ -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]

View File

@@ -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) {

View File

@@ -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,

View File

@@ -368,6 +368,7 @@ class RootController extends Controller
'collection',
'user',
'users',
'workermanager'
];
$feature = 'connected';

View File

@@ -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 = [

View File

@@ -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;
}
}

View File

@@ -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 => [],

View File

@@ -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'
];
/**

View File

@@ -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' => ''
]
]
];
}

View File

@@ -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',
];
}
}

View File

@@ -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;

View File

@@ -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();

View File

@@ -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"> &nbsp;</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"> &nbsp;</a>$7'
, $string
);
}, ['needs_environment' => true, 'is_safe' => ['html']]));

View File

@@ -17,7 +17,7 @@ class Version
* @var string
*/
private $number = '4.1.0-alpha.26a';
private $number = '4.1.0-alpha.29a';
/**
* @var string

View 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;
}
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -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();
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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()));

View File

@@ -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();
}
}

View File

@@ -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'));
}
}
}

View File

@@ -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), '']);
}
}

View File

@@ -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'], []);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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';
}

View File

@@ -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';
}
}

View File

@@ -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';
}
}

View File

@@ -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';
}
}

View File

@@ -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();
}
}

View File

@@ -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'];
}
}

View File

@@ -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();
}
}

View 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;
}
}

View 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);
}
}
}
}

View 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);
}
}

View 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);
}
}

View File

@@ -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'
];
}
}

View File

@@ -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'
];
}
}

View File

@@ -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'];
}
}

View File

@@ -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'
];
}
}

View File

@@ -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',
];
}
}

View 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();
}
}
}

View 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'];
}
}

View File

@@ -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();
}
}

View 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
)
);
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Alchemy\Phrasea\WorkerManager\Worker\Factory;
use Alchemy\Phrasea\WorkerManager\Worker\WorkerInterface;
interface WorkerFactoryInterface
{
/**
* @return WorkerInterface
*/
public function createWorker();
}

View 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();
}
}
}

View 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);
}
}
}

View 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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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();
}
}

View 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());
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Alchemy\Phrasea\WorkerManager\Worker;
interface WorkerInterface
{
/**
* @param array $payload
* @return mixed
*/
public function process(array $payload);
}

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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
*/

View 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;
}
}

View File

@@ -10,6 +10,7 @@ main:
maintenance: false
key: ''
api_require_ssl: true
api_token_header: false
database:
host: 'sql-host'
port: 3306

View 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"
]
}

View File

@@ -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",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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

View 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

View 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

View 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

View 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

View File

@@ -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';

View File

@@ -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;
}
}
}

View File

@@ -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

View 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

View 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

View File

@@ -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>
&#x25bc;
<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>
&#x25bc;
<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>
&#x25bc;
<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>
&#x25bc;
<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>
&#x25bc;
<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>
&#x25bc;
<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>
&#x25bc;
<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>
&#x25bc;
<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>
&#x25bc;
<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>
&#x25bc;
<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>
&#x25bc;
<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>
&#x25bc;
<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>
&#x25bc;
<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>
&#x25bc;
<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>
&#x25bc;
<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>
&#x25bc;
<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>
&#x25bc;
<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([

View File

@@ -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') }}',

View File

@@ -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>

View File

@@ -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">

View 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 %}

Some files were not shown because too many files have changed in this diff Show More