foldershare-8.x-1.2/src/Entity/FolderShareTraits/OperationArchiveTrait.php

src/Entity/FolderShareTraits/OperationArchiveTrait.php
<?php

namespace Drupal\foldershare\Entity\FolderShareTraits;

use Drupal\foldershare\Settings;
use Drupal\foldershare\ManageFilenameExtensions;
use Drupal\foldershare\ManageFileSystem;
use Drupal\foldershare\Utilities\FileUtilities;
use Drupal\foldershare\Utilities\FormatUtilities;
use Drupal\foldershare\Entity\Exception\LockException;
use Drupal\foldershare\Entity\Exception\SystemException;
use Drupal\foldershare\Entity\Exception\ValidationException;

/**
 * Archive FolderShare entities to a FolderShare ZIP archive.
 *
 * This trait includes methods to archive one or more FolderShare
 * entities into a ZIP archive saved into a new FolderShare entity.
 *
 * <B>Internal trait</B>
 * This trait is internal to the FolderShare module and used to define
 * features of the FolderShare entity class. It is a mechanism to group
 * functionality to improve code management.
 *
 * @ingroup foldershare
 */
trait OperationArchiveTrait {

  /*---------------------------------------------------------------------
   *
   * Archive to FolderShare entity.
   *
   *---------------------------------------------------------------------*/

  /**
   * Archives the given root items to a new archive in the root list.
   *
   * A new ZIP archive is created in the root list and all of the given
   * items, and their children, recursively, are added to the archive.
   *
   * All items must be root items.
   *
   * <B>Process locks</B>
   * The root folder trees above the items added to the archive are locked
   * for exclusive use during the operation.
   *
   * <B>Post-operation hooks</B>
   * This method calls the "hook_foldershare_post_operation_add_files" hook.
   *
   * <B>Activity log</B>
   * This method posts a log message after the archive is created.
   *
   * @param \Drupal\foldershare\FolderShareInterface[] $items
   *   An array of root items that are to be included in a new archive
   *   added to the root list.
   *
   * @return \Drupal\foldershare\FolderShareInterface
   *   Returns the FolderShare entity for the new archive.
   *
   * @throws \Drupal\foldershare\Entity\Exception\LockException
   *   Thrown if an access lock on any item could not be acquired.
   * @throws \Drupal\foldershare\Entity\Exception\ValidationException
   *   Thrown if any item in the array are not root items.
   * @throws \Drupal\foldershare\Entity\Exception\SystemException
   *   Thrown if the archive file could not be created, such as if the
   *   temporary directory does not exist or is not writable, if a temporary
   *   file for the archive could not be created, or if any of the file
   *   children of this item could not be read and saved into the archive.
   */
  public static function archiveToRoot(array $items) {
    //
    // Validate.
    // ---------
    // ZIP extensions must be supported and all items must be root items.
    if (empty($items) === TRUE) {
      return NULL;
    }

    if (ManageFilenameExtensions::isZipExtensionAllowed() === FALSE) {
      throw new ValidationException(FormatUtilities::createFormattedMessage(
        t(
          '@method was called to create an archive type the site does not support.',
          [
            '@method' => __METHOD__,
          ]),
        t('The ZIP file type required for new archives is not an allowed file type for the site. Archive creation is therefore not supported.')));
    }

    $itemsToArchive = [];
    foreach ($items as $item) {
      if ($item !== NULL) {
        if ($item->isRootItem() === FALSE) {
          throw new ValidationException(FormatUtilities::createFormattedMessage(
            t(
              '@method was called with an item that is not a root item.',
              [
                '@method' => __METHOD__,
              ])));
        }

        $itemsToArchive[] = $item;
      }
    }

    if (empty($itemsToArchive) === TRUE) {
      return NULL;
    }

    //
    // Create archive in local temp storage.
    // -------------------------------------
    // Create a ZIP archive containing the given items.
    //
    // On completion, a ZIP archive exists on the local file system in a
    // temporary file. This throws an exception and deletes the archive on
    // an error.
    $archiveUri = self::createZipArchive($itemsToArchive);

    //
    // Create File entity.
    // -------------------
    // Create a new File entity from the local file. This also moves the
    // file into FolderShare's directory tree and gives it a numeric name.
    // The MIME type is also set, and the File is marked as permanent.
    // This throws an exception if the File could not be created.
    try {
      if (count($itemsToArchive) === 1) {
        // Add '.zip' to the item name.
        $archiveName = $itemsToArchive[0]->getName() . '.zip';
      }
      else {
        // Use a generic name.
        $archiveName = Settings::getNewZipArchiveName();
      }

      $archiveFile = self::createFileEntityFromLocalFile(
        $archiveUri,
        $archiveName);
    }
    catch (\Exception $e) {
      FileUtilities::unlink($archiveUri);
      throw $e;
    }

    //
    // Add the archive to the root list.
    // ---------------------------------
    // Add the file, checking for and correcting name collisions.
    //
    // The user's root list is locked as this file is added.
    try {
      $ary = self::addFilesInternal(
        NULL,
        [$archiveFile],
        (-1),
        TRUE,
        TRUE,
        FALSE);
    }
    catch (\Exception $e) {
      // On any failure, the archive wasn't added and we need to delete it.
      $archiveFile->delete();
      throw $e;
    }

    if (empty($ary) === TRUE) {
      return NULL;
    }

    return $ary[0];
  }

  /**
   * {@inheritdoc}
   */
  public function archiveToFolder(array $items) {
    //
    // Validate.
    // ---------
    // ZIP extensions must be supported and all items must be children
    // of this folder.
    if (empty($items) === TRUE) {
      return NULL;
    }

    if (ManageFilenameExtensions::isZipExtensionAllowed() === FALSE) {
      throw new ValidationException(FormatUtilities::createFormattedMessage(
        t(
          '@method was called to create an archive type the site does not support.',
          [
            '@method' => __METHOD__,
          ]),
        t('The ZIP file type required for new archives is not an allowed file type for the site. Archive creation is therefore not supported.')));
    }

    if ($this->isFolder() === FALSE) {
      throw new ValidationException(FormatUtilities::createFormattedMessage(
        t(
          '@method was called with an item that is not a folder.',
          [
            '@method' => __METHOD__,
          ])));
    }

    $parentId = (int) $this->id();
    $itemsToArchive = [];
    foreach ($items as $item) {
      if ($item !== NULL) {
        if ($item->getParentFolderId() !== $parentId) {
          throw new ValidationException(FormatUtilities::createFormattedMessage(
            t(
              '@method was called with an item that is not a folder.',
              [
                '@method' => __METHOD__,
              ])));
        }
        $itemsToArchive[] = $item;
      }
    }

    if (empty($itemsToArchive) === TRUE) {
      return NULL;
    }

    //
    // Create archive in local temp storage.
    // -------------------------------------
    // Create a ZIP archive containing the given items.
    //
    // On completion, a ZIP archive exists on the local file system in a
    // temporary file. This throws an exception and deletes the archive on
    // an error.
    $archiveUri = self::createZipArchive($itemsToArchive);

    //
    // Create File entity.
    // -------------------
    // Create a new File entity from the local file. This also moves the
    // file into FolderShare's directory tree and gives it a numeric name.
    // The MIME type is also set, and the File is marked as permanent.
    // This throws an exception if the File could not be created.
    try {
      if (count($itemsToArchive) === 1) {
        // Add '.zip' to the item name.
        $archiveName = $itemsToArchive[0]->getName() . '.zip';
      }
      else {
        // Use a generic name.
        $archiveName = Settings::getNewZipArchiveName();
      }

      $archiveFile = self::createFileEntityFromLocalFile(
        $archiveUri,
        $archiveName);
    }
    catch (\Exception $e) {
      FileUtilities::unlink($archiveUri);
      throw $e;
    }

    //
    // Add the archive to this folder.
    // -------------------------------
    // Add the file, checking for and correcting name collisions.
    //
    // The root folder tree is locked as this file is added.
    try {
      $ary = self::addFilesInternal(
        $this,
        [$archiveFile],
        (-1),
        TRUE,
        TRUE,
        FALSE);
    }
    catch (\Exception $e) {
      // On any failure, the archive wasn't added and we need to delete it.
      $archiveFile->delete();
      throw $e;
    }

    if (empty($ary) === TRUE) {
      return NULL;
    }

    return $ary[0];
  }

  /*---------------------------------------------------------------------
   *
   * Archive to file.
   *
   *---------------------------------------------------------------------*/

  /**
   * Creates and adds a list of children to a local ZIP archive.
   *
   * A new ZIP archive is created in the site's temporary directory
   * on the local file system. The given list of children are then
   * added to the archive and the file path of the archive is returned.
   *
   * If an error occurs, an exception is thrown and the archive file is
   * deleted.
   *
   * If a URI for the new archive is not provided, a temporary file is
   * created in the site's temporary directory, which is normally cleaned
   * out regularly. This limits the lifetime of the file, though
   * callers should delete the file when it is no longer needed, or move
   * it out of the temporary directory.
   *
   * <B>Process locks</B>
   * Each child file or folder and all of their subfolders and files are
   * locked for exclusive editing access by this function for the duration of
   * the archiving.
   *
   * @param \Drupal\foldershare\FolderShareInterface[] $items
   *   An array of FolderShare files and/or folders that are to be included
   *   in a new ZIP archive. They should all be children of the same parent
   *   folder.
   * @param string $archiveUri
   *   (optional, default = '' = create temp name) The URI for a local file
   *   to be overwritten with the new ZIP archive. If the URI is an empty
   *   string, a temporary file with a randomized name will be created in
   *   the site's temporary directory. The name will not have a filename
   *   extension.
   *
   * @return string
   *   Returns a URI to the new ZIP archive. The URI refers to a new file
   *   in the module's temporary files directory, which is cleaned out
   *   periodically. Callers should move the file to a new destination if
   *   they intend to keep the file.
   *
   * @throws \Drupal\foldershare\Entity\Exception\LockException
   *   Thrown if an access lock on any child could not be acquired.
   *
   * @throws \Drupal\foldershare\Entity\Exception\SystemException
   *   Thrown if the archive file could not be created, such as if the
   *   temporary directory does not exist or is not writable, if a temporary
   *   file for the archive could not be created, or if any of the file
   *   children of this item could not be read and saved into the archive.
   */
  public static function createZipArchive(
    array $items,
    string $archiveUri = '') {

    // Implementation note: How PHP's ZipArchive class works.
    //
    // Creating a ZipArchive selects a file name for the output.  Adding files
    // to the archive just adds those files to a list in memory of files
    // TO BE ADDED, but doesn't add them immediately or do any file I/O. The
    // actual file I/O occurs entirely when close() is called.
    //
    // This impacts when we lock files and folders. We cannot just lock
    // them before and after the archive addFile() call because that call
    // doesn't do the file I/O. Our locks would not guarantee that the file
    // continues to exist until the file I/O really happens on close().
    //
    // Instead, we need to lock everything FIRST, before adding anything to
    // the archive. Then we need to add the files and close the
    // archive to trigger the file I/O. And then when the file I/O is done
    // we can safely unlock everything LAST.
    //
    // Setup
    // -----
    // Sweep through the list of items to create a list of unique roots
    // above them.
    $rootIds = [];
    foreach ($items as $item) {
      if ($item === NULL) {
        // Item does not exist.
        continue;
      }

      $rootIds[] = $item->getRootItemId();
    }

    //
    // Lock roots.
    // -----------
    // Lock each of the root folder trees containing items to add to the
    // ZIP archive. Typically, all of the items are in the same folder
    // tree, so there will be just one root.
    //
    // Locking the root folder trees keeps them from changing out from
    // under this operation.
    $rootIds = array_unique($rootIds);
    foreach ($rootIds as $index => $rootId) {
      // LOCK ITEM ROOT FOLDER TREES.
      if (self::acquireRootOperationLock($rootId) === FALSE) {
        // Failed to get lock. Back out and unlock everything locked so far.
        //
        // UNLOCK ITEM ROOT FOLDER TREES.
        foreach ($rootIds as $i => $rid) {
          if ($i === $index) {
            break;
          }
          self::releaseRootOperationLock($rid);
        }

        throw new LockException(
          self::getStandardLockExceptionMessage(t('compressed'), NULL));
      }
    }

    //
    // Create ZIP file.
    // ----------------
    // Create a new empty ZIP file and URI, unless one was given.
    if (empty($archiveUri) === TRUE) {
      // Create an empty temporary file. The file will have a randomized
      // name guaranteed not to collide with anything.
      $archiveUri = FileUtilities::tempnam(
        ManageFileSystem::getTempDirectoryUri(),
        'zip');

      if ($archiveUri === FALSE) {
        // This can fail if file system permissions are messed up, the
        // file system is full, or some other system error has occurred.
        //
        // UNLOCK ITEM ROOT FOLDER TREES.
        foreach ($rootIds as $rootId) {
          self::releaseRootOperationLock($rootId);
        }

        throw new SystemException(t(
          "System error. A file at '@path' could not be created.\nThere may be a problem with directories or permissions. Please report this to the site administrator.",
          [
            '@path' => $archiveUri,
          ]));
      }
    }

    //
    // Add to ZIP.
    // -----------
    // Set up the ZIP archive, add a comment, then loop over all of the
    // items to add to the ZIP archive. Each time a folder is added,
    // recurse through that folder's descendants and add them too.
    // Finally, close the archive.
    $archive = NULL;
    $failException = NULL;
    try {
      //
      // Create the ZipArchive object.
      // -----------------------------
      // Create the archiver and assign it the output file.
      $archive = new \ZipArchive();
      $archivePath = FileUtilities::realpath($archiveUri);
      if ($archive->open($archivePath, \ZipArchive::OVERWRITE) !== TRUE) {
        // All errors that could be returned are very unlikely. The
        // archive's path is known to be good since we just created a
        // temp file using it.  This means permissions are right, the
        // directory and file name are fine, and the file system is up
        // and working. Something catestrophic now has happened.
        $failException = new SystemException(t(
          "System error. A file at '@path' could not be created.\nThere may be a problem with directories or permissions. Please report this to the site administrator.",
          [
            '@path' => $archivePath,
          ]));
      }
      else {
        $archive->setArchiveComment(Settings::getNewZipArchiveComment());

        //
        // Recursively add to archive.
        // ---------------------------
        // For each of the given items, add them to the archive.  An exception
        // is thrown if any child, or its children, cannot be added. That
        // causes us to abort.
        foreach ($items as $item) {
          if ($item !== NULL) {
            $item->addToZipArchiveInternal($archive, '');
          }
        }

        // Close the file and trigger I/O to build the archive.
        if ($archive->close() === FALSE) {
          // Something went wrong with the file I/O. The ZIP
          // library delays all of its I/O until the close, so we actually
          // don't know which specific operation failed.
          $failException = new SystemException(t(
            "System error. A file at '@path' could not be written.\nThere may be a problem with permissions. Please report this to the site administrator.",
            [
              '@path' => $archivePath,
            ]));
        }
      }
    }
    catch (\Exception $e) {
      $failException = $e;
    }

    //
    // Unlock roots.
    // -------------
    // All of the items have been added to the ZIP archive now. Unlock
    // all of the root folder trees above them.
    //
    // UNLOCK ITEM ROOT FOLDER TREES.
    foreach ($rootIds as $rootId) {
      self::releaseRootOperationLock($rootId);
    }

    if ($failException !== NULL) {
      // On failure, clean out the archive, delete the output file,
      // unlock everyting.
      if ($archive !== NULL) {
        $archive->unchangeAll();
        unset($archive);
      }

      // Delete the archive file as it exists so far.
      FileUtilities::unlink($archiveUri);

      throw $failException;
    }

    // Change the permissions to be suitable for web serving.
    FileUtilities::chmod($archiveUri);

    return $archiveUri;
  }

  /*---------------------------------------------------------------------
   *
   * Archive implementation.
   *
   *---------------------------------------------------------------------*/

  /**
   * Adds this item to the archive and recurses.
   *
   * This item is added to the archive, and then all its children are
   * added.
   *
   * If this item is a folder, all of the folder's children are added to
   * the archive. If the folder is empty, an empty directory is added to
   * the archive.
   *
   * If this item is a file, the file on the file system is copied into
   * the archive.
   *
   * To insure that the archive has the user-visible file and folder names,
   * an archive path is created during recursion. On the first call, a base
   * path is passed in as $baseZipPath. This item's name is then appended.
   * If this item is a file, the path with appended name is used as the
   * name for the file when in the archive. If this item is a folder, this
   * path with appended name is passed as the base path for adding the
   * folder's children, and so on.
   *
   * <B>Process locks</B>
   * This method does not lock access. The caller should lock around changes
   * to the entity.
   *
   * @param \ZipArchive $archive
   *   The archive to add to.
   * @param string $baseZipPath
   *   The folder path to be used within the ZIP archive to lead to the
   *   parent of this item.
   *
   * @throws \Drupal\foldershare\Entity\Exception\LockException
   *   Thrown if an access lock on this folder could not be acquired.
   *   This exception is never thrown if $lock is FALSE.
   *
   * @throws \Drupal\foldershare\Entity\Exception\SystemException
   *   Thrown if a file could not be added to the archive.
   */
  private function addToZipArchiveInternal(
    \ZipArchive &$archive,
    string $baseZipPath) {
    //
    // Implementation note:
    //
    // The file path used within a ZIP file is recommended to always use
    // the '/' directory separator, regardless of the local OS conventions.
    // Since we are building a ZIP path, we therefore use '/'.
    //
    // Use the incoming folder path and append the user-visible item name
    // to create the name for the item as it should appear in the ZIP archive.
    if (empty($baseZipPath) === TRUE) {
      $currentZipPath = $this->getName();
    }
    else {
      $currentZipPath = $baseZipPath . '/' . $this->getName();
    }

    // Add the item to the archive.
    switch ($this->getKind()) {
      case self::FOLDER_KIND:
        // For folders, create an empty directory entry in the ZIP archive.
        // Then recurse to add all of the folder's children.
        if ($archive->addEmptyDir($currentZipPath) === FALSE) {
          throw new SystemException(t(
            "System error. Cannot add '@path' to archive '@archive'.\nThere may be a problem with the file system (such as out of storage space), with file permissions, or with the ZIP archive library. Please report this to the site administrator.",
            [
              '@path'    => 'empty directory',
              '@archive' => $currentZipPath,
            ]));
        }

        foreach ($this->findChildren() as $child) {
          $child->addToZipArchiveInternal($archive, $currentZipPath);
        }
        break;

      case self::FILE_KIND:
        // For files, get the path to the underlying stored file and add
        // the file to the archive.
        $file     = $this->getFile();
        $fileUri  = $file->getFileUri();
        $filePath = FileUtilities::realpath($fileUri);

        if ($archive->addFile($filePath, $currentZipPath) === FALSE) {
          throw new SystemException(t(
            "System error. Cannot add '@path' to archive '@archive'.\nThere may be a problem with the file system (such as out of storage space), with file permissions, or with the ZIP archive library. Please report this to the site administrator.",
            [
              '@path'    => $filePath,
              '@archive' => $currentZipPath,
            ]));
        }
        break;

      case self::IMAGE_KIND:
        // For images, get the path to the underlying stored file and add
        // the file to the archive.
        $file     = $this->getImage();
        $fileUri  = $file->getFileUri();
        $filePath = FileUtilities::realpath($fileUri);

        if ($archive->addFile($filePath, $currentZipPath) === FALSE) {
          throw new SystemException(t(
            "System error. Cannot add '@path' to archive '@archive'.\nThere may be a problem with the file system (such as out of storage space), with file permissions, or with the ZIP archive library. Please report this to the site administrator.",
            [
              '@path'    => $filePath,
              '@archive' => $currentZipPath,
            ]));
        }
        break;

      case self::MEDIA_KIND:
      default:
        // For any other kind, we don't know what it is or it does not have
        // a stored file, so we cannot add it to the archive. Silently
        // ignore it.
        break;
    }
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc