Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?phpdeclare(strict_types=1);namespace OpenSpout\Writer\ODS\Helper;use DateTimeImmutable;use OpenSpout\Common\Exception\IOException;use OpenSpout\Common\Helper\FileSystemHelper as CommonFileSystemHelper;use OpenSpout\Writer\Common\Entity\Worksheet;use OpenSpout\Writer\Common\Helper\FileSystemWithRootFolderHelperInterface;use OpenSpout\Writer\Common\Helper\ZipHelper;use OpenSpout\Writer\ODS\Manager\Style\StyleManager;use OpenSpout\Writer\ODS\Manager\WorksheetManager;/*** @internal*/final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface{public const MIMETYPE = 'application/vnd.oasis.opendocument.spreadsheet';public const META_INF_FOLDER_NAME = 'META-INF';public const MANIFEST_XML_FILE_NAME = 'manifest.xml';public const CONTENT_XML_FILE_NAME = 'content.xml';public const META_XML_FILE_NAME = 'meta.xml';public const MIMETYPE_FILE_NAME = 'mimetype';public const STYLES_XML_FILE_NAME = 'styles.xml';private readonly string $baseFolderRealPath;/** @var string document creator */private readonly string $creator;private readonly CommonFileSystemHelper $baseFileSystemHelper;/** @var string Path to the root folder inside the temp folder where the files to create the ODS will be stored */private string $rootFolder;/** @var string Path to the "META-INF" folder inside the root folder */private string $metaInfFolder;/** @var string Path to the temp folder, inside the root folder, where specific sheets content will be written to */private string $sheetsContentTempFolder;/** @var ZipHelper Helper to perform tasks with Zip archive */private readonly ZipHelper $zipHelper;/*** @param string $baseFolderPath The path of the base folder where all the I/O can occur* @param ZipHelper $zipHelper Helper to perform tasks with Zip archive* @param string $creator document creator*/public function __construct(string $baseFolderPath, ZipHelper $zipHelper, string $creator){$this->baseFileSystemHelper = new CommonFileSystemHelper($baseFolderPath);$this->baseFolderRealPath = $this->baseFileSystemHelper->getBaseFolderRealPath();$this->zipHelper = $zipHelper;$this->creator = $creator;}public function createFolder(string $parentFolderPath, string $folderName): string{return $this->baseFileSystemHelper->createFolder($parentFolderPath, $folderName);}public function createFileWithContents(string $parentFolderPath, string $fileName, string $fileContents): string{return $this->baseFileSystemHelper->createFileWithContents($parentFolderPath, $fileName, $fileContents);}public function deleteFile(string $filePath): void{$this->baseFileSystemHelper->deleteFile($filePath);}public function deleteFolderRecursively(string $folderPath): void{$this->baseFileSystemHelper->deleteFolderRecursively($folderPath);}public function getRootFolder(): string{return $this->rootFolder;}public function getSheetsContentTempFolder(): string{return $this->sheetsContentTempFolder;}/*** Creates all the folders needed to create a ODS file, as well as the files that won't change.** @throws IOException If unable to create at least one of the base folders*/public function createBaseFilesAndFolders(): void{$this->createRootFolder()->createMetaInfoFolderAndFile()->createSheetsContentTempFolder()->createMetaFile()->createMimetypeFile();}/*** Creates the "content.xml" file under the root folder.** @param Worksheet[] $worksheets*/public function createContentFile(WorksheetManager $worksheetManager, StyleManager $styleManager, array $worksheets): self{$contentXmlFileContents = <<<'EOD'<?xml version="1.0" encoding="UTF-8" standalone="yes"?><office:document-content office:version="1.2" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:calcext="urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:msoxl="http://schemas.microsoft.com/office/excel/formula" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:xlink="http://www.w3.org/1999/xlink">EOD;$contentXmlFileContents .= $styleManager->getContentXmlFontFaceSectionContent();$contentXmlFileContents .= $styleManager->getContentXmlAutomaticStylesSectionContent($worksheets);$contentXmlFileContents .= '<office:body><office:spreadsheet>';$topContentTempFile = uniqid(self::CONTENT_XML_FILE_NAME);$this->createFileWithContents($this->rootFolder, $topContentTempFile, $contentXmlFileContents);// Append sheets content to "content.xml"$contentXmlFilePath = $this->rootFolder.\DIRECTORY_SEPARATOR.self::CONTENT_XML_FILE_NAME;$contentXmlHandle = fopen($contentXmlFilePath, 'w');\assert(false !== $contentXmlHandle);$topContentTempPathname = $this->rootFolder.\DIRECTORY_SEPARATOR.$topContentTempFile;$topContentTempHandle = fopen($topContentTempPathname, 'r');\assert(false !== $topContentTempHandle);stream_copy_to_stream($topContentTempHandle, $contentXmlHandle);fclose($topContentTempHandle);unlink($topContentTempPathname);foreach ($worksheets as $worksheet) {// write the "<table:table>" node, with the final sheet's namefwrite($contentXmlHandle, $worksheetManager->getTableElementStartAsString($worksheet));$worksheetFilePath = $worksheet->getFilePath();$this->copyFileContentsToTarget($worksheetFilePath, $contentXmlHandle);fwrite($contentXmlHandle, '</table:table>');}// add AutoFilter$databaseRanges = '';foreach ($worksheets as $worksheet) {$databaseRanges .= $worksheetManager->getTableDatabaseRangeElementAsString($worksheet);}if ('' !== $databaseRanges) {fwrite($contentXmlHandle, '<table:database-ranges>');fwrite($contentXmlHandle, $databaseRanges);fwrite($contentXmlHandle, '</table:database-ranges>');}$contentXmlFileContents = '</office:spreadsheet></office:body></office:document-content>';fwrite($contentXmlHandle, $contentXmlFileContents);fclose($contentXmlHandle);return $this;}/*** Deletes the temporary folder where sheets content was stored.*/public function deleteWorksheetTempFolder(): self{$this->deleteFolderRecursively($this->sheetsContentTempFolder);return $this;}/*** Creates the "styles.xml" file under the root folder.** @param int $numWorksheets Number of created worksheets*/public function createStylesFile(StyleManager $styleManager, int $numWorksheets): self{$stylesXmlFileContents = $styleManager->getStylesXMLFileContent($numWorksheets);$this->createFileWithContents($this->rootFolder, self::STYLES_XML_FILE_NAME, $stylesXmlFileContents);return $this;}/*** Zips the root folder and streams the contents of the zip into the given stream.** @param resource $streamPointer Pointer to the stream to copy the zip*/public function zipRootFolderAndCopyToStream($streamPointer): void{$zip = $this->zipHelper->createZip($this->rootFolder);$zipFilePath = $this->zipHelper->getZipFilePath($zip);// In order to have the file's mime type detected properly, files need to be added// to the zip file in a particular order.// @see http://www.jejik.com/articles/2010/03/how_to_correctly_create_odf_documents_using_zip/$this->zipHelper->addUncompressedFileToArchive($zip, $this->rootFolder, self::MIMETYPE_FILE_NAME);$this->zipHelper->addFolderToArchive($zip, $this->rootFolder, ZipHelper::EXISTING_FILES_SKIP);$this->zipHelper->closeArchiveAndCopyToStream($zip, $streamPointer);// once the zip is copied, remove it$this->deleteFile($zipFilePath);}/*** Creates the folder that will be used as root.** @throws IOException If unable to create the folder*/private function createRootFolder(): self{$this->rootFolder = $this->createFolder($this->baseFolderRealPath, uniqid('ods'));return $this;}/*** Creates the "META-INF" folder under the root folder as well as the "manifest.xml" file in it.** @throws IOException If unable to create the folder or the "manifest.xml" file*/private function createMetaInfoFolderAndFile(): self{$this->metaInfFolder = $this->createFolder($this->rootFolder, self::META_INF_FOLDER_NAME);$this->createManifestFile();return $this;}/*** Creates the "manifest.xml" file under the "META-INF" folder (under root).** @throws IOException If unable to create the file*/private function createManifestFile(): self{$manifestXmlFileContents = <<<'EOD'<?xml version="1.0" encoding="UTF-8"?><manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" manifest:version="1.2"><manifest:file-entry manifest:full-path="/" manifest:media-type="application/vnd.oasis.opendocument.spreadsheet"/><manifest:file-entry manifest:full-path="styles.xml" manifest:media-type="text/xml"/><manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/><manifest:file-entry manifest:full-path="meta.xml" manifest:media-type="text/xml"/></manifest:manifest>EOD;$this->createFileWithContents($this->metaInfFolder, self::MANIFEST_XML_FILE_NAME, $manifestXmlFileContents);return $this;}/*** Creates the temp folder where specific sheets content will be written to.* This folder is not part of the final ODS file and is only used to be able to jump between sheets.** @throws IOException If unable to create the folder*/private function createSheetsContentTempFolder(): self{$this->sheetsContentTempFolder = $this->createFolder($this->rootFolder, 'worksheets-temp');return $this;}/*** Creates the "meta.xml" file under the root folder.** @throws IOException If unable to create the file*/private function createMetaFile(): self{$createdDate = (new DateTimeImmutable())->format(DateTimeImmutable::W3C);$metaXmlFileContents = <<<EOD<?xml version="1.0" encoding="UTF-8" standalone="yes"?><office:document-meta office:version="1.2" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:xlink="http://www.w3.org/1999/xlink"><office:meta><dc:creator>{$this->creator}</dc:creator><meta:creation-date>{$createdDate}</meta:creation-date><dc:date>{$createdDate}</dc:date></office:meta></office:document-meta>EOD;$this->createFileWithContents($this->rootFolder, self::META_XML_FILE_NAME, $metaXmlFileContents);return $this;}/*** Creates the "mimetype" file under the root folder.** @throws IOException If unable to create the file*/private function createMimetypeFile(): self{$this->createFileWithContents($this->rootFolder, self::MIMETYPE_FILE_NAME, self::MIMETYPE);return $this;}/*** Streams the content of the file at the given path into the target resource.* Depending on which mode the target resource was created with, it will truncate then copy* or append the content to the target file.** @param string $sourceFilePath Path of the file whose content will be copied* @param resource $targetResource Target resource that will receive the content*/private function copyFileContentsToTarget(string $sourceFilePath, $targetResource): void{$sourceHandle = fopen($sourceFilePath, 'r');\assert(false !== $sourceHandle);stream_copy_to_stream($sourceHandle, $targetResource);fclose($sourceHandle);}}