translator->trans('task::archive:Archivage'); } /** * {@inheritdoc} */ public function getJobId() { return 'Archive'; } /** * {@inheritdoc} */ public function getDescription() { return $this->translator->trans("task::archive:Archiving files found into a 'hotfolder'"); } /** * {@inheritdoc} */ public function getEditor() { return new ArchiveEditor($this->translator); } /** * {@inheritdoc} */ protected function doJob(JobData $data) { $app = $data->getApplication(); // quick fix to reconnect if mysql is lost $app->getApplicationBox()->get_connection(); $task = $data->getTask(); $settings = simplexml_load_string($task->getSettings()); $baseId = (string) $settings->base_id; $sbasId = \phrasea::sbasFromBas($app, $baseId); if (!$sbasId) { throw new RuntimeException('Invalid collection base_id.'); } $databox = $app->findDataboxById($sbasId); // quick fix to reconnect if mysql is lost $databox->get_connection(); $TColls = []; $collection = null; foreach ($databox->get_collections() as $coll) { $TColls['c' . $coll->get_coll_id()] = $coll->get_coll_id(); if ($baseId == $coll->get_base_id()) { $collection = $coll; } } $server_coll_id = $collection->get_coll_id(); // mask(s) of accepted files $tmask = []; $tmaskgrp = []; $cold = min(max((int) $settings->cold, self::MINCOLD), self::MAXCOLD); $stat0 = $stat1 = "0"; if (false !== $sxBasePrefs = simplexml_load_string($collection->get_prefs())) { if ($sxBasePrefs->status) { $stat0 = (string) $sxBasePrefs->status; } } if ($settings->status) { $stat1 = (string) $settings->status; } if (!$stat0) { $stat0 = '0'; } if (!$stat1) { $stat1 = '0'; } $filesystem = $app['filesystem']; // load masks if ($settings->files && $settings->files->file) { foreach ($settings->files->file as $ft) { $tmask[] = [ "mask" => (string)$ft["mask"], "caption" => (string)$ft["caption"], "accept" => (string)$ft["accept"], ]; } } if ($settings->files && $settings->files->grouping) { foreach ($settings->files->grouping as $ft) { $tmaskgrp[] = [ "mask" => (string)$ft["mask"], "caption" => (string)$ft["caption"], "representation" => (string)$ft["representation"], "accept" => (string)$ft["accept"], ]; } } if (count($tmask) == 0) { // no mask defined : accept all kind of files $tmask[] = ["mask" => ".*", "caption" => "", "accept" => ""]; } while ($this->isStarted()) { $path_in = rtrim((string) $settings->hotfolder, '\\/'); $path_in = $filesystem->exists($path_in) ? realpath($path_in) : $path_in; if (!@is_dir($path_in)) { throw new RuntimeException(sprintf('Error : missing hotfolder \'%s\', stopping.', $path_in)); } // copy settings to task, so it's easier to get later $moveArchived = \p4field::isyes($settings->move_archived); $moveError = \p4field::isyes($settings->move_error); clearstatcache(); if (false === $filesystem->exists($path_in . "/.phrasea.xml")) { throw new RuntimeException(sprintf('NO .phrasea.xml AT ROOT \'%s\' !', $path_in)); } $path_archived = $path_error = null; if ($moveArchived) { $path_archived = $path_in . '_archived'; $filesystem->mkdir($path_archived, 0755); } if ($moveError) { $path_error = $path_in . '_error'; $filesystem->mkdir($path_error, 0755); } $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()); } if (!$this->isStarted()) { return; } // wait for files to be cold 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()); } if (!$this->isStarted()) { return; } $this->makePairs($dom, $root, $path_in, $path_archived, $path_error, false, 0, $tmask, $tmaskgrp); if ($app['debug']) { $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()); } $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()); } } } private function listFilesPhase1(Application $app, \DOMDocument $dom, \DomElement $node, $path, $server_coll_id, $depth, &$TColls) { $nnew = 0; $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' && ($app['filesystem']->exists($path . '/' . $magicfile) === true)) { return 0; } elseif ($magicmethod == 'UNLOCK' && ($app['filesystem']->exists($path . '/' . $magicfile) === false)) { return 0; } } // change collection ? if (($new_cid = $sxDotPhrasea['collection']) != '') { if (isset($TColls['c' . $new_cid])) { $server_coll_id = $new_cid; } else { $this->log('debug', sprintf('Unknown coll_id (%1$d) in "%2$s"', (int) $new_cid, $path . '/.phrasea.xml')); $server_coll_id = -1; } } $node->setAttribute('pxml', '1'); } foreach ($this->listFolder($path) as $file) { if (!$this->isStarted()) { break; } usleep(10); if ($this->isIgnoredFile($file)) { continue; } /** @var \DOMElement $n */ if (is_dir($path . '/' . $file)) { $n = $node->appendChild($dom->createElement('file')); $n->setAttribute('isdir', '1'); $n->setAttribute('name', $file); $nnew += $this->listFilesPhase1($app, $dom, $n, $path . '/' . $file, $server_coll_id, $depth + 1, $TColls); if (!$this->isStarted()) { break; } } else { $n = $node->appendChild($dom->createElement('file')); $n->setAttribute('name', $file); $stat = stat($path . '/' . $file); 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); $n->setAttribute('temperature', 'hot'); } return $nnew; } private function listFilesPhase2(Application $app, \DOMDocument $dom, \DOMElement $node, $path, $depth) { $nnew = 0; $xp = new \DOMXPath($dom); if (false !== $sxDotPhrasea = @simplexml_load_file($path . '/.phrasea.xml')) { // test magicfile if ('' !== $magicfile = trim((string) ($sxDotPhrasea->magicfile))) { $magicmethod = strtoupper($sxDotPhrasea->magicfile['method']); if ($magicmethod == 'LOCK' && true === $app['filesystem']->exists($path . '/' . $magicfile)) { return 0; } if ($magicmethod == 'UNLOCK' && false === $app['filesystem']->exists($path . '/' . $magicfile)) { return 0; } } } foreach ($this->listFolder($path) as $file) { if (!$this->isStarted()) { break; } if ($this->isIgnoredFile($file)) { continue; } usleep(10); $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($n); } elseif ($dnl && $dnl->length == 1) { /** @var \DOMElement $n */ $n = $dnl->item(0); $n->setAttribute('temperature', 'cold'); if (is_dir($path . '/' . $file)) { $this->listFilesPhase2($app, $dom, $n, $path . '/' . $file, $depth + 1); } else { $stat = stat($path . '/' . $file); foreach (["size", "ctime", "mtime"] as $k) { if ($n->getAttribute($k) != $stat[$k]) { $this->setBranchHot($n); break; } } } } } return $nnew; } private function makePairs(\DOMDocument $dom, \DOMElement $node, $path, $path_archived, $path_error, $inGrp, $depth, &$tmask, $tmaskgrp) { if ($depth == 0 && ($node->getAttribute('temperature') == 'hot' || $node->getAttribute('cid') == '-1')) { return; } $xpath = new \DOMXPath($dom); for ($n = $node->firstChild; $n; $n = $n->nextSibling) { usleep(10); if (!$this->isStarted()) { break; } // make xml lighter (free ram) foreach (["size", "ctime", "mtime"] as $k) { $n->removeAttribute($k); } if ($n->getAttribute('temperature') == 'hot' || $n->getAttribute('cid') == '-1') { continue; } $name = $n->getAttribute('name'); if ($n->getAttribute('isdir') == '1') { // get 'caption', 'representation' if (($grpSettings = $this->getGrpSettings($name, $tmaskgrp)) !== false) { // this is a grp folder, we check it $dnl = $xpath->query('./file[@name=".grouping.xml"]', $n); if ($dnl->length == 1) { // this group is old (don't care about any linked files), just flag it $n->setAttribute('grp', 'tocomplete'); /** @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) { if (isset($grpSettings[$linkName]) && $grpSettings[$linkName] != '') { // we need this linked file, calc his real name $f = preg_replace('/' . $grpSettings['mask'] . '/i', $grpSettings[$linkName], $name); $dnl = $xpath->query('./file[@name="' . $f . '"]', $node); if ($dnl->length == 1) { // it's here $flink[$linkName] = $dnl->item(0); } else { $this->log('debug', sprintf('missing linked file \'%1$s\' to group \'%2$s\'', $f, $name)); // missing -> error $err = true; } } } if (!$err) { // the group is ok, flag it ... $n->setAttribute('grp', 'tocreate'); // ... as the existing linked file(s) ... foreach ($flink as $linkName => $v) { // this linked file exists if ($v) { $v->setAttribute('match', '*'); $n->setAttribute('grp_' . $linkName, $v->getAttribute('name')); } } // recurse only if group is ok $this->makePairs($dom, $n, $path . '/' . $name , $path_archived . '/' . $name , $path_error . '/' . $name , true, $depth + 1, $tmask, $tmaskgrp); } else { // something is missing, the whole group goes error, ... $n->setAttribute('grp', 'todelete'); $this->setAllChildren($dom, $n, ['error' => '1']); // bubble to the top for ($nn = $n; $nn && $nn->nodeType == XML_ELEMENT_NODE; $nn = $nn->parentNode) { $nn->setAttribute('error', '1'); } // ... as the existing linked file(s) ... foreach ($flink as $linkName => $v) { // this linked file exists, it goes error also if ($v) { $v->setAttribute('error', '1'); } } } } } else { // not a grp folder, recurse $this->makePairs($dom, $n, $path . '/' . $name , $path_archived . '/' . $name , $path_error . '/' . $name , $inGrp, $depth + 1, $tmask, $tmaskgrp); } } else { // 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 { $this->checkMatch($dom, $n, $tmask); // } } } } // scan again for unmatched files for ($n = $node->firstChild; $n; $n = $n->nextSibling) { if (!$this->isStarted()) { break; } if (!$n->getAttribute('isdir') == '1' && !$n->getAttribute('match')) { // still no match, now it's an error (bubble to the top) for ($nn = $n; $nn && $nn->nodeType == XML_ELEMENT_NODE; $nn = $nn->parentNode) { $nn->setAttribute('error', '1'); } } } } private function removeBadGroups(Application $app, \DOMDocument $dom, \DOMElement $node, $path, $path_archived, $path_error, $depth, $moveError) { $ret = false; // if root of hotfolder if hot, die... if ($depth == 0 && $node->getAttribute('temperature') == 'hot') { return; } $nodesToDel = []; for ($n = $node->firstChild; $n; $n = $n->nextSibling) { usleep(10); if (!$this->isStarted()) { break; } if ($n->getAttribute('temperature') == 'hot') { // do not move hotfiles continue; } $name = $n->getAttribute('name'); if ($n->getAttribute('isdir')) { $this->removeBadGroups($app, $dom, $n, $path . '/' . $name , $path_archived . '/' . $name , $path_error . '/' . $name , $depth + 1, $moveError); if ($n->getAttribute('grp') == 'todelete') { $nodesToDel[] = $n; try { $app['filesystem']->remove($path . '/' . $name); } catch (IOException $e) { $this->log('error', $e->getMessage()); } } } else { if ($n->getAttribute('error')) { if ($moveError) { $this->log('debug', sprintf('copy \'%s\' to \'error\'', $path . '/' . $name)); try { $app['filesystem']->mkdir($path_error, 0755); } catch (IOException $e) { $this->log('error', $e->getMessage()); } try { $app['filesystem']->copy($path . '/' . $name, $path_error . '/' . $name, true); } catch (IOException $e) { $this->log('error', $e->getMessage()); } } $nodesToDel[] = $n; try { $app['filesystem']->remove($path . '/' . $name); } catch (IOException $e) { $this->log('error', $e->getMessage()); } } } } foreach ($nodesToDel as $n) { $n->parentNode->removeChild($n); } } private function archive(Application $app, \databox $databox, \DOMDOcument $dom, \DOMElement $node, $path, $path_archived, $path_error, $depth, $moveError, $moveArchived, $stat0, $stat1) { // quick fix to reconnect if mysql is lost $app->getApplicationBox()->get_connection(); $databox->get_connection(); if ($node->getAttribute('temperature') == 'hot') { return; } $nodesToDel = []; for ($n = $node->firstChild; $n; $n = $n->nextSibling) { usleep(10); if (!$this->isStarted()) { break; } if ($n->getAttribute('temperature') == 'hot') { continue; } if ($n->getAttribute('cid') == '-1') { $n->setAttribute('error', '1'); continue; } if ($n->getAttribute('isdir') == '1') { if ($n->getAttribute('grp')) { // a grp folder : special work $this->archiveGrp($app, $databox, $dom, $n, $path, $path_archived, $path_error, $nodesToDel, $moveError, $moveArchived, $stat0, $stat1); } else { // ...normal subfolder : recurse $name = $n->getAttribute('name'); $this->archive($app, $databox, $dom, $n, $path . '/' . $name , $path_archived . '/' . $name , $path_error . '/' . $name , $depth + 1, $moveError, $moveArchived, $stat0, $stat1); } } else { // a file, 0 = no grp $this->archiveFile($app, $databox, $dom, $n, $path, $path_archived, $path_error, $nodesToDel, 0, $stat0, $stat1, $moveError, $moveArchived); } } foreach ($nodesToDel as $n) { $n->parentNode->removeChild($n); } // at the end of recursion, restore the magic file (create or delete it) if (($magicfile = $node->getAttribute('magicfile')) != '') { $magicmethod = $node->getAttribute('magicmethod'); if ($magicmethod == 'LOCK') { file_put_contents($path . '/' . $magicfile, ''); } elseif ($magicmethod == 'UNLOCK') { try { $app['filesystem']->remove($path . '/' . $magicfile); } catch (IOException $e) { $this->log('debug', $e->getMessage()); } } } return; } private function bubbleResults(\DOMDocument $dom, \DOMElement $node, $path, $depth, $copySpe) { static $iloop = 0; if ($depth == 0) { $iloop = 0; } if ($node->getAttribute('temperature') == 'hot') { return 0; } $ret = 0; for ($n = $node->firstChild; $n; $n = $n->nextSibling) { if (($iloop++ % 20) == 0) { usleep(1000); } if ($n->getAttribute('name') == '.phrasea.xml' || $n->getAttribute('name') == '.grouping.xml') { // special files stay in place AND are copied into 'archived' $n->setAttribute('keep', '1'); if ($copySpe) { $n->setAttribute('archived', '1'); } } if ($n->getAttribute('keep') == '1') { $ret |= 1; } if ($n->getAttribute('archived') == '1') { $ret |= 2; } if ($n->getAttribute('error') == '1') { $ret |= 4; } if ($n->getAttribute('isdir') == '1') { $ret |= $this->bubbleResults($dom, $n, $path . '/' . $n->getAttribute('name'), $depth + 1, $copySpe); } } if ($ret & 1) { $node->setAttribute('keep', '1'); } if ($ret & 2) { $node->setAttribute('archived', '1'); } if ($ret & 4) { $node->setAttribute('error', '1'); } return $ret; } private function moveFiles(Application $app, \DOMDocument $dom, \DOMElement $node, $path, $path_archived, $path_error, $depth, $moveArchived, $moveError) { $ret = false; // if root of hotfolder if hot, die... if ($depth == 0 && $node->getAttribute('temperature') == 'hot') { return $ret; } $nodesToDel = []; for ($n = $node->firstChild; $n; $n = $n->nextSibling) { usleep(10); if ($n->getAttribute('temperature') == 'hot') { // do not move hotfiles continue; } $name = $n->getAttribute('name'); if ($n->getAttribute('isdir')) { $ret |= $this->moveFiles($app, $dom, $n, $path . '/' . $name , $path_archived . '/' . $name , $path_error . '/' . $name , $depth + 1, $moveArchived, $moveError); if (!$n->firstChild) { $nodesToDel[] = $n; } /** * Do not remove empty folders yet */ } else { if ($n->getAttribute('archived') && $moveArchived) { $this->log('debug', sprintf('copy \'%s\' to \'archived\'', $path . '/' . $name)); try { $app['filesystem']->mkdir($path_archived); } catch (IOException $e) { $this->log('debug', $e->getMessage()); } try { $app['filesystem']->copy($path . '/' . $name, $path_archived . '/' . $name, true); } catch (IOException $e) { $this->log('debug', $e->getMessage()); } if (!$n->getAttribute('keep')) { // do not count copy of special files as a real event $nodesToDel[] = $n; $ret = true; } } if ($n->getAttribute('error') && $moveError) { $this->log('debug', sprintf('copy \'%s\' to \'error\'', $path . '/' . $name)); try { $app['filesystem']->mkdir($path_error); } catch (IOException $e) { $this->log('debug', $e->getMessage()); } try { $app['filesystem']->copy($path . '/' . $name, $path_error . '/' . $name, true); } catch (IOException $e) { $this->log('debug', $e->getMessage()); } if (!$n->getAttribute('keep')) { // do not count copy of special files as a real event $nodesToDel[] = $n; $ret = true; } } if (!$n->getAttribute('keep') && !$n->getAttribute('match')) { $this->log('debug', sprintf('delete \'%s\'', $path . '/' . $name)); try { $app['filesystem']->remove($path . '/' . $name); } catch (IOException $e) { $this->log('debug', $e->getMessage()); } } } } foreach ($nodesToDel as $n) { $n->parentNode->removeChild($n); } return $ret; } protected function checkMatch(\DOMDocument $dom, \DOMElement $node, &$tmask) { $file = $node->getAttribute('name'); foreach ($tmask as $mask) { $preg_mask = '/' . $mask['mask'] . '/'; if (preg_match($preg_mask, $file)) { if ($mask['caption']) { // caption in a linked file ? $captionFileName = @preg_replace($preg_mask, $mask['caption'], $file); $xpath = new \DOMXPath($dom); $dnl = $xpath->query('./file[@name="' . $captionFileName . '"]', $node->parentNode); if ($dnl->length == 1) { // the caption file exists $node->setAttribute('match', $captionFileName); /** @var \DOMElement $n */ $n = $dnl->item(0); $n->setAttribute('match', '*'); } else { // the caption file is missing $node->setAttribute('match', '?'); } } else { // self-described $node->setAttribute('match', '.'); } // first match is ok break; } } return; } protected function isIgnoredFile($f) { $f = strtolower($f); return ($f[0] == '.' && $f != '.phrasea.xml' && $f != '.grouping.xml') || $f == 'thumbs.db' || $f == 'par-system'; } private function setBranchHot(\DOMElement $node) { for ($n = $node; $n; $n = $n->parentNode) { if ($n->nodeType == XML_ELEMENT_NODE) { $n->setAttribute('temperature', 'hot'); if ($n->hasAttribute('pxml')) { break; } } } return; } private function archiveGrp(Application $app, \databox $databox, \DOMDocument $dom, \DOMElement $node, $path, $path_archived, $path_error, array &$nodesToDel, $moveError, $moveArchived, $stat0, $stat1) { // quick fix to reconnect if mysql is lost $app->getApplicationBox()->get_connection(); $databox->get_connection(); $xpath = new \DOMXPath($dom); // grp folders stay in place $node->setAttribute('keep', '1'); $grpFolder = $node->getAttribute('name'); $groupingFile = $path . '/' . $grpFolder . '/.grouping.xml'; 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; $this->log('debug', sprintf('created story \'%s\'', $path . '/' . $grpFolder)); // if the .grp does not have a representative doc, let's use a generic file if (!($rep = $node->getAttribute('grp_representation'))) { try { $app['filesystem']->copy($app['root.path'] . '/www/assets/common/images/icons/substitution/regroup_doc.png', $genericdoc = ($path . '/group.jpg'), true); } catch (IOException $e) { $this->log('debug', $e->getMessage()); } $representationFileName = 'group.jpg'; $this->log('debug', ' (no representation file)'); } else { $dnl = $xpath->query('./file[@name="' . $rep . '"]', $node->parentNode); $representationFileNode = $dnl->item(0); $representationFileName = $rep; $node->removeAttribute('grp_representation'); $this->log('debug', sprintf('representation from \'%s\'', $representationFileName)); } if (($cap = $node->getAttribute('grp_caption')) != '') { $dnl = $xpath->query('./file[@name="' . $cap . '"]', $node->parentNode); $captionFileNode = $dnl->item(0); $captionFileName = $cap; $node->removeAttribute('grp_caption'); $this->log('debug', sprintf('caption from \'%s\'', $captionFileName)); } try { $collection = \collection::getByCollectionId($app, $databox, (int) $cid); if ($captionFileName === null) { $story = $this->createStory($app, $collection, $path . '/' . $representationFileName, null, $stat0, $stat1); } else { $story = $this->createStory($app, $collection, $path . '/' . $representationFileName, $path . '/' . $captionFileName, $stat0, $stat1); } $rid = $story->getRecordId(); $this->log('info', sprintf('story %s created', $rid)); if ($genericdoc) { try { $app['filesystem']->remove($genericdoc); } catch (IOException $e) { $this->log('debug', $e->getMessage()); } } file_put_contents($groupingFile, ''); /** @var \DOMElement $n */ $n = $node->appendChild($dom->createElement('file')); $n->setAttribute('name', '.grouping.xml'); $n->setAttribute('temperature', 'cold'); $n->setAttribute('grp', '1'); $n->setAttribute('match', '*'); if ($moveArchived) { $this->log('debug', sprintf('copy \'%s\' to \'archived\'', $path . '/' . $grpFolder . '/.grouping.xml')); try { $app['filesystem']->mkdir($path_archived . '/' . $grpFolder, 0755); } catch (IOException $e) { $this->log('debug', $e->getMessage()); } try { $app['filesystem']->copy($path . '/' . $grpFolder . '/.grouping.xml', $path_archived . '/' . $grpFolder . '/.grouping.xml', true); } catch (IOException $e) { $this->log('debug', $e->getMessage()); } } if ($captionFileNode) { $captionFileNode->setAttribute('archived', '1'); if ($moveArchived) { $this->log('debug', sprintf('copy \'%s\' to \'archived\'', $path . '/' . $captionFileName)); try { $app['filesystem']->mkdir($path_archived, 0755); } catch (IOException $e) { $this->log('debug', $e->getMessage()); } try { $app['filesystem']->copy($path . '/' . $captionFileName, $path_archived . '/' . $captionFileName, true); } catch (IOException $e) { $this->log('debug', $e->getMessage()); } } try { $app['filesystem']->remove($path . '/' . $captionFileName); } catch (IOException $e) { $this->log('debug', $e->getMessage()); } $nodesToDel[] = $captionFileNode; } if ($representationFileNode) { $representationFileNode->setAttribute('archived', '1'); if ($moveArchived) { $this->log('debug', sprintf('copy \'%s\' to \'archived\'', $path . '/' . $representationFileName)); try { $app['filesystem']->mkdir($path_archived, 0755); } catch (IOException $e) { $this->log('debug', $e->getMessage()); } try { $app['filesystem']->copy($path . '/' . $representationFileName, $path_archived . '/' . $representationFileName, true); } catch (IOException $e) { $this->log('debug', $e->getMessage()); } } try { $app['filesystem']->remove($path . '/' . $representationFileName); } catch (IOException $e) { $this->log('debug', $e->getMessage()); } $nodesToDel[] = $representationFileNode; } $node->setAttribute('grp', 'tocomplete'); } catch (\Exception $e) { $this->log('debug', $e->getMessage()); } } // here the .grouping.xml should exists if ($app['filesystem']->exists($groupingFile)) { // a .grouping.xml must stay in place // -- don't do, done in phase4 $sxGrouping = @simplexml_load_file($groupingFile); $grp_rid = $sxGrouping['grouping']; $this->archiveFilesToGrp($app, $databox, $dom, $node, $path . '/' . $grpFolder , $path_archived . '/' . $grpFolder , $path_error . '/' . $grpFolder , $grp_rid, $stat0, $stat1, $moveError, $moveArchived); } return; } public function createStory(Application $app, \collection $collection, $pathfile, $captionFile, $stat0, $stat1) { // quick fix to reconnect if mysql is lost $app->getApplicationBox()->get_connection(); $collection->get_connection(); $status = \databox_status::operation_or($stat0, $stat1); $media = $app->getMediaFromUri($pathfile); $databox = $collection->get_databox(); $metadatasStructure = $databox->get_meta_structure(); $metadatas = $this->getIndexByFieldName($metadatasStructure, $media->getMetadatas()); $metaFields = null; if ($captionFile !== null && true === $app['filesystem']->exists($captionFile)) { $metaFields = $this->readXMLForDatabox($app, $metadatasStructure, $captionFile); $captionStatus = $this->parseStatusBit(@simplexml_load_file($captionFile)); if ($captionStatus) { $status = \databox_status::operation_mask($status, $captionStatus); } } $story = \record_adapter::createStory($app, $collection); $story->setStatus($status); $app['subdef.substituer']->substituteDocument($story, $media); $story->set_metadatas($metadatas->toMetadataArray($metadatasStructure), true); if ($metaFields) { $story->set_metadatas($metaFields->toMetadataArray($metadatasStructure), true); } // order to write meta in file $this->dispatcher->dispatch(WorkerEvents::RECORDS_WRITE_META, new RecordsWriteMetaEvent([$story->getRecordId()], $story->getDataboxId())); $story->setStatus(\databox_status::operation_or($stat0, $stat1)); $app['dispatcher']->dispatch(RecordEvents::SUBDEFINITION_CREATE, new SubdefinitionCreateEvent($story)); unset($media); return $story; } /** * Creates a record * * @param \collection $collection The destination collection * @param string $pathfile The file to archive * @param string|null $captionFile The Phrasea XML caption file or null if no caption file * @param integer $grp_rid Add the record to a story * @param integer $force Force lazaret or record ; use \Alchemy\Phrasea\Border\Manager::FORCE_* constants * @return \record_adapter */ public function createRecord(Application $app, \collection $collection, $pathfile, $captionFile, $grp_rid, $force, $stat0, $stat1) { // quick fix to reconnect if mysql is lost $app->getApplicationBox()->get_connection(); $collection->get_connection(); $status = \databox_status::operation_or($stat0, $stat1); $media = $app->getMediaFromUri($pathfile); $databox = $collection->get_databox(); $metadatasStructure = $databox->get_meta_structure(); $metadatas = $this->getIndexByFieldName($metadatasStructure, $media->getMetadatas()); $metaFields = []; if ($captionFile !== null && true === $app['filesystem']->exists($captionFile)) { $metaFields = $this->readXMLForDatabox($app, $metadatasStructure, $captionFile); $captionStatus = $this->parseStatusBit(@simplexml_load_file($captionFile)); if ($captionStatus) { $status = \databox_status::operation_mask($status, $captionStatus); } } $file = new File($app, $media, $collection); $file->addAttribute(new BorderAttribute\Status($app, $status)); /** @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($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)); } foreach ($metaFields as $metaField) { $file->addAttribute($metaField); } if ($grp_rid) { $file->addAttribute(new BorderAttribute\Story(new \record_adapter($app, $databox->get_sbas_id(), $grp_rid))); } $record = null; $postProcess = function ($element, $visa, $code) use (&$record) { $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']; $borderManager->process($this->getLazaretSession($app), $file, $postProcess, $force); return $record; } /** * * @param \DOMDocument $dom * @param \DOMElement $node * @param string $path * @param string $path_archived * @param string $path_error * @param integer $grp_rid */ private function archiveFilesToGrp(Application $app, \databox $databox, \DOMDocument $dom, \DOMElement $node, $path, $path_archived, $path_error, $grp_rid, $stat0, $stat1, $moveError, $moveArchived) { // quick fix to reconnect if mysql is lost $app->getApplicationBox()->get_connection(); $databox->get_connection(); $nodesToDel = []; for ($n = $node->firstChild; $n; $n = $n->nextSibling) { if (!$this->isStarted()) { break; } if ($n->getAttribute('isdir') == '1') { // in a grp, all levels goes in the same grp $node->setAttribute('archived', '1'); // the main grp folder is 'keep'ed, but not subfolders $this->archiveFilesToGrp($app, $databox, $dom, $n, $path . '/' . $n->getAttribute('name') , $path_archived . '/' . $n->getAttribute('name') , $path_error . '/' . $n->getAttribute('name') , $grp_rid, $stat0, $stat1, $moveError, $moveArchived); } else { // a file $this->archiveFile($app, $databox, $dom, $n, $path, $path_archived, $path_error, $nodesToDel, $grp_rid, $stat0, $stat1, $moveError, $moveArchived); } } foreach ($nodesToDel as $n) { $n->parentNode->removeChild($n); } return; } /** * Archive File * * @param \DOMDocument $dom * @param \DOMElement $node * @param string $path * @param string $path_archived * @param string $path_error * @param array $nodesToDel out, filled with files to delete * @param integer $grp_rid */ private function archiveFile(Application $app, \databox $databox, \DOMDocument $dom, \DOMElement $node, $path, $path_archived, $path_error, array &$nodesToDel, $grp_rid, $stat0, $stat1, $moveError, $moveArchived) { // quick fix to reconnect if mysql is lost $app->getApplicationBox()->get_connection(); $databox->get_connection(); $match = $node->getAttribute('match'); if ($match == '*') { return; } $file = $node->getAttribute('name'); $captionFileNode = null; if (!$match) { // the file does not match on any mask $this->log('debug', sprintf("File '%s' does not match any mask", $path . '/' . $file)); $node->setAttribute('error', '1'); return; } if ($match == '?') { // the caption file is missing $this->log('debug', sprintf("Caption of file '%s' is missing", $path . '/' . $file)); $node->setAttribute('error', '1'); return; } if ($match != '.') { // match='.' : the file does not have a separate caption $xpath = new \DOMXPath($dom); $dnl = $xpath->query('./file[@name="' . $match . '"]', $node->parentNode); // in fact, xquery has been done in checkMatch, setting match='?' if caption does not exists... if ($dnl->length == 1) { // ...so we ALWAYS come here $captionFileNode = $dnl->item(0); } else { // ...so we should NEVER come here $node->setAttribute('error', '1'); return; } } $this->archiveFileAndCaption($app, $databox, $node, $captionFileNode, $path, $path_archived, $path_error, $grp_rid, $nodesToDel, $stat0, $stat1, $moveError, $moveArchived); } /** * * @param Application $app * @param \databox $databox * @param \DOMElement $node * @param \DOMElement $captionFileNode * @param string $path * @param string $path_archived * @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, \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(); $databox->get_connection(); $file = $node->getAttribute('name'); $cid = $node->getAttribute('cid'); $captionFileName = $captionFileNode ? $captionFileNode->getAttribute('name') : null; $this->log('info', sprintf("Archiving file '%s'", $path . '/' . $file)); if ($captionFileName !== null) { $this->log('info', sprintf(' ' . (" (caption in '%s')"), $captionFileName)); } if ($grp_rid !== 0) { $this->log('info', sprintf(' ' . (" into GRP rid=%s"), $grp_rid)); } try { $collection = \collection::getByCollectionId($app, $databox, (int) $cid); if ($captionFileName === null) { $this->createRecord($app, $collection, $path . '/' . $file, null, $grp_rid, null, $stat0, $stat1); } else { $this->createRecord($app, $collection, $path . '/' . $file, $path . '/' . $captionFileName, $grp_rid, null, $stat0, $stat1); } $node->setAttribute('archived', '1'); if ($captionFileNode) { $captionFileNode->setAttribute('archived', '1'); } } catch (\Exception $e) { $this->log('debug', "Error : can't insert record : " . $e->getMessage()); $node->setAttribute('error', '1'); if ($captionFileNode) { $captionFileNode->setAttribute('error', '1'); } } if ($node->getAttribute('archived') && $moveArchived) { $this->log('debug', sprintf('copy \'%s\' to \'archived\'', $path . '/' . $file)); try { $app['filesystem']->mkdir($path_archived); } catch (IOException $e) { $this->log('debug', $e->getMessage()); } try { $app['filesystem']->copy($path . '/' . $file, $path_archived . '/' . $file, true); } catch (IOException $e) { $this->log('debug', $e->getMessage()); } if ($captionFileName != $file && $captionFileName) { $this->log('debug', sprintf('copy \'%s\' to \'archived\'', $path . '/' . $captionFileName)); try { $app['filesystem']->copy($path . '/' . $captionFileName, $path_archived . '/' . $captionFileName, true); } catch (IOException $e) { $this->log('debug', $e->getMessage()); } } } if ($node->getAttribute('error') && $moveError) { $this->log('debug', sprintf('copy \'%s\' to \'error\'', $path . '/' . $file)); try { $app['filesystem']->mkdir($path_error); } catch (IOException $e) { $this->log('debug', $e->getMessage()); } try { $app['filesystem']->copy($path . '/' . $file, $path_error . '/' . $file, true); } catch (IOException $e) { $this->log('debug', $e->getMessage()); } if ($captionFileName != $file && $captionFileName) { $this->log('debug', sprintf('copy \'%s\' to \'error\'', $path . '/' . $captionFileName)); try { $app['filesystem']->copy($path . '/' . $captionFileName, $path_error . '/' . $captionFileName, true); } catch (IOException $e) { $this->log('debug', $e->getMessage()); } } } if (!$node->getAttribute('keep')) { $file = $node->getAttribute('name'); try { $app['filesystem']->remove($path . '/' . $file); } catch (IOException $e) { $this->log('debug', $e->getMessage()); } $nodesToDel[] = $node; } if ($captionFileNode && !$captionFileNode->getAttribute('keep')) { $file = $captionFileNode->getAttribute('name'); try { $app['filesystem']->remove($path . '/' . $file); } catch (IOException $e) { $this->log('debug', $e->getMessage()); } $nodesToDel[] = $captionFileNode; } return; } /** * xml facility : set attributes to a node and all children * * @staticvar integer $iloop * @param \DOMDocument $dom * @param \DOMElement $node * @param array $attributes An associative array of attributes * @param integer $depth */ private function setAllChildren(\DOMDocument $dom, \DOMElement $node, array $attributes, $depth = 0) { static $iloop = 0; if ($depth == 0) { $iloop = 0; } foreach ($attributes as $a => $v) { $node->setAttribute($a, $v); } if (($iloop++ % 100) == 0) { usleep(1000); } for ($n = $node->firstChild; $n; $n = $n->nextSibling) { $this->setAllChildren($dom, $n, $attributes, $depth + 1); } } /** * Return the story settings * * @param string $file * @return array */ private function getGrpSettings($file, $tmaskgrp) { $matched = false; foreach ($tmaskgrp as $maskgrp) { $preg_maskgrp = "/" . $maskgrp["mask"] . "/"; if (preg_match($preg_maskgrp, $file)) { $matched = $maskgrp; } if ($matched) { break; } } return $matched; } /** * Return a LazaretSession * * @return LazaretSession */ protected function getLazaretSession(Application $app) { // quick fix to reconnect if mysql is lost $app->getApplicationBox()->get_connection(); $lazaretSession = new LazaretSession(); $app['orm.em']->persist($lazaretSession); $app['orm.em']->flush(); return $lazaretSession; } /** * Map a Bag of metadatas indexed by **Tagnames** to a bag of metadatas * indexed by **FieldNames** * * @param \databox_descriptionStructure $metadatasStructure The databox structure related * @param ExiftoolMetadataBag $bag The metadata bag * @return MetadataBag */ protected function getIndexByFieldName(\databox_descriptionStructure $metadatasStructure, ExiftoolMetadataBag $bag) { $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())); } } return $ret; } protected function readXMLForDatabox(Application $app, \databox_descriptionStructure $metadatasStructure, $pathfile) { if (false === $app['filesystem']->exists($pathfile)) { throw new \InvalidArgumentException(sprintf('file %s does not exists', $pathfile)); } if (false === $sxcaption = @simplexml_load_file($pathfile)) { throw new \InvalidArgumentException(sprintf('Invalid XML file %s', $pathfile)); } $metadataBag = new MetaFieldsBag(); foreach ($sxcaption->description->children() as $tagname => $value) { $value = trim($value); $meta = $metadatasStructure->get_element_by_name(trim($tagname)); if (!$meta) { continue; } if ($meta->is_multi()) { $fields = \caption_field::get_multi_values($value, $meta->get_separator()); if (!$metadataBag->containsKey($meta->get_name())) { $values = $fields; } else { $values = array_merge($metadataBag->get($meta->get_name())->getValue(), $fields); } $metadataBag->set($meta->get_name(), new BorderAttribute\MetaField($meta, $values)); } else { $metadataBag->set($meta->get_name(), new BorderAttribute\MetaField($meta, [$value])); } } return $metadataBag; } /** * Parse a Phrasea XML to find status tag * * @param \SimpleXMLElement $sxcaption The SimpleXML related to the XML * @return string */ protected function parseStatusBit($sxcaption) { if (!$sxcaption instanceof \SimpleXMLElement) { return null; } if ('' !== $inStatus = (string) $sxcaption->status) { return $inStatus; } return null; } private function listFolder($path) { $list = []; if ($hdir = opendir($path)) { while (false !== $file = readdir($hdir)) { $list[] = $file; } closedir($hdir); natcasesort($list); } return $list; } }