Skip to content

Instantly share code, notes, and snippets.

@theking2
Last active June 22, 2024 08:38
Show Gist options
  • Save theking2/3c5d8f7accf72e2b08349e9084da84d0 to your computer and use it in GitHub Desktop.
Save theking2/3c5d8f7accf72e2b08349e9084da84d0 to your computer and use it in GitHub Desktop.
<?php declare(strict_types=1);
/**
* ZipStepResponse
*
* This class creates a ZIP archive of the specified directory.
* With the name of the archive, it creates a new archive name if the current one is too large.
* in small steps to avoid PHP timeouts
*/
class ZipStepResponse
{
private int $archiveCount;
private int $refreshRate = 10;
private int $maxFileSizeMB = 250;
private int $batchSize = 10;
private array $files;
private int $fileCount;
private int $fileIndex;
private int $step;
private ?string $message = null;
public function __construct(
readonly string $directory,
private string $archiveName)
{
session_start();
if( !array_key_exists( 'files', $_SESSION ) ) {
$_SESSION['archiveName'] = $archiveName;
$_SESSION['archiveCount'] = 0;
$_SESSION['files'] = $this->getFilesRecursively( $directory );
}
// Read the request
$this->archiveName = $_SESSION['archiveName'];
$this->archiveCount = $_SESSION['archiveCount'];
$this->files = $_SESSION['files'];
$request = json_decode( file_get_contents( 'php://input' ) );
$this->fileIndex = $request->fileIndex;
$this->registerFatal();
}
public function setBatchSize( int $batchSize ): self
{
$this->batchSize = $batchSize;
return $this;
}
public function setRefreshRate( int $refreshRate ): self
{
$this->refreshRate = $refreshRate;
return $this;
}
public function setMaxFileSize( int $maxFileSizeMB ): self
{
$this->maxFileSizeMB = $maxFileSizeMB;
return $this;
}
public function processStep(): array
{
$basepathLength = strlen( $this->directory );
$fileCount = count( $this->files );
$secondsElapsed = 0;
try {
// (Re)open the ZIP file
for(
$fileIndex = $this->fileIndex, $start = hrtime( true );
$fileIndex < $fileCount and $secondsElapsed < $this->refreshRate;
) {
$zip = $this->openArchive();
// add a batch of #batchSize files
for( $i = 0; $fileIndex < $fileCount and $i < $this->batchSize; $i++, $fileIndex++ ) {
$currentfile = $this->files[ $fileIndex ];
if( file_exists( $currentfile ) ) {
$zip->addFile( filepath: $currentfile, entryname: str_replace('\\', '/', substr( $currentfile, $basepathLength )) );
}
}
// Close wil add the batch of (#batchSize) files.
$zip->close();
$secondsElapsed = ( hrtime( true ) - $start ) / 1e+9;
}
if( $fileIndex === $fileCount ) {
$fileIndex = null; // Done
session_unset();
session_destroy();
}
} catch ( Exception $e ) {
$message = $e->getMessage();
session_unset();
session_destroy();
}
return [
'fileIndex' => $fileIndex,
'total' => count( $this->files ),
'secondsElapsed' => number_format( $secondsElapsed, 1 ),
'message' => $message ?? null
];
}
/**
* currentAcrhiveName
* create a new archive name if the current one
*/
private function currentAcrhiveName(): string
{
$archiveName = $this->archiveName . '-' . $this->archiveCount . '.zip';
if( file_exists( $archiveName ) and ( (filesize( $archiveName )>>6) > $this->maxFileSizeMB ) ) {
// Current archive is too large, create a new one
$this->archiveCount++;
$_SESSION['archiveCount'] = $this->archiveCount;
$archiveName = $this->currentAcrhiveName();
}
return $archiveName;
}
/**
* openArchive
*/
private function openArchive(): ZipArchive
{
$archiveName = $this->currentAcrhiveName();
$zip = new ZipArchive();
$result = $zip->open( $archiveName, ZipArchive::CREATE );
if( $result !== TRUE ) {
throw new Exception( "Unable to open {$archiveName}\n" );
}
return $zip;
}
/**
* getFilesRecursively
* create a list of files in a directory, recursively
* @param $directory
*/
private function getFilesRecursively( string $directory ): array
{
$files = [];
try {
// Create a RecursiveDirectoryIterator
$dirIterator = new RecursiveDirectoryIterator( $directory );
// Create a RecursiveIteratorIterator
$iterator = new RecursiveIteratorIterator( $dirIterator );
// Loop through the iterator
foreach( $iterator as $fileInfo ) {
// Skip directories (you can remove this condition if you want to include directories)
if( $fileInfo->isDir() )
continue;
if( $fileInfo->getBasename() === basename( __FILE__ ) )
continue; // skip self
if( $fileInfo->getExtension() === 'zip' )
continue; // skip ZIP files
// Get the full path of the file
$files[] = $fileInfo->getPathname();
}
} catch ( Exception $e ) {
echo "Error: " . $e->getMessage();
}
return $files;
}
private function registerFatal(): void
{
register_shutdown_function( function() {
$error = error_get_last();
if( $error !== null ) {
echo json_encode( [ 'error' => $error ] );
}
});
}
}
@theking2
Copy link
Author

theking2 commented Jun 19, 2024

Call with

<?php declare(strict_types=1);

require_once __DIR__ . '/ZipStepArchiv.php';

$host = str_replace( '.', '_', $_SERVER['HTTP_HOST'] );
$date = date( 'Ymd_Hi' );
echo json_encode( ( new ZipStepResponse(
  __DIR__ . '/../wp-content',
  __DIR__ . "/archiv-$host-$date"
) )->setBatchSize( 250 )->setRefreshRate( 10 )->setMaxFileSize( 6000 )->processStep() );

and a js

const dialogStatus = document.getElementById("status");
const elementFiles = document.getElementById("files");
const elementTime = document.getElementById("time");
let totalElasped = 0;
async function createZipStep(fileIndex) {
  elementFiles.textContent = '0 / 0%';
  elementTime.textContent = '0';

  try {
    const response = await fetch(
      'zip-step.php',
      {
        method: 'POST',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ fileIndex: fileIndex })
      });
    if (!response.ok) {
      throw new Error('Network response was not ok ' + response.statusText);
    }
    const result = await response.json();
    if (result.fileIndex) {
      createZipStep(result.fileIndex);
    }
    const percent = (100.0 * result.fileIndex / result.total).toFixed(2);
    totalElasped += parseFloat(result.secondsElapsed);
    elementFiles.textContent = `${result.fileIndex} / ${percent}%`;
    elementTime.textContent = totalElasped.toFixed(2) + 's';
  } catch (error) {
    console.error('There has been a problem with your fetch operation:', error);
  }
}

dialogStatus.show();
createZipStep(0);

with this html

<?php declare(strict_types=1);
session_start();
session_destroy();
?><script type="module" src="create-zip-step.js"></script>
<style>#status{width:24ex;text-align:center;}</style>
<dialog id="status" style="width:24ex">
  <p>Processed <span id="files"></span></p>
  <p>Time <span id="time"></span></p>
</dialog>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment