isLoggedIn()) throw new NotAllowedException(); if (!isset($_GET['tag'])) throw new UserFacingException('No album or tag has been specified for download.'); $tag = (int)$_GET['tag']; $album = Tag::fromId($tag); if (isset($_GET['by']) && ($user = Member::fromSlug($_GET['by'])) !== false) $id_user_uploaded = $user->getUserId(); else $id_user_uploaded = null; if (isset($_SESSION['current_export'])) throw new UserFacingException('You can only export one album at the same time. Please wait until the other download finishes, or try again later.'); // So far so good? $this->exportAlbum($album, $id_user_uploaded); exit; } private function exportAlbum(Tag $album, $id_user_uploaded) { $files = []; $album_ids = array_merge([$album->id_tag], $this->getChildAlbumIds($album->id_tag)); foreach ($album_ids as $album_id) { $iterator = AssetIterator::getByOptions([ 'id_tag' => $album_id, 'id_user_uploaded' => $id_user_uploaded, ]); while ($asset = $iterator->next()) $files[] = join(DIRECTORY_SEPARATOR, [$asset->getSubdir(), $asset->getFilename()]); } $descriptorspec = [ 0 => ['pipe', 'r'], // STDIN 1 => ['pipe', 'w'], // STDOUT ]; // Prevent simultaneous exports. $_SESSION['current_export'] = $album->id_tag; // Allow new exports if the connection is terminated unexpectedly (e.g. when a user aborts a download). register_shutdown_function(function() { if (isset($_SESSION['current_export'])) unset($_SESSION['current_export']); }); $command = 'tar -cf - -C ' . escapeshellarg(ASSETSDIR) . ' --null -T -'; $proc = proc_open($command, $descriptorspec, $pipes, ASSETSDIR); if(!$proc) throw new UnexpectedValueException('Could not execute TAR command'); if(!$pipes[0]) throw new UnexpectedValueException('Could not open pipe for STDIN'); if(!$pipes[1]) throw new UnexpectedValueException('Could not open pipe for STDOUT'); // STDOUT should not block. stream_set_blocking($pipes[1], 0); // Allow this the download to take its time... set_time_limit(0); header('Pragma: no-cache'); header('Content-Description: File Download'); header('Content-disposition: attachment; filename="' . $album->tag . '.tar"'); header('Content-Type: application/octet-stream'); header('Content-Transfer-Encoding: binary'); // Write filenames to include to STDIN, separated by null bytes. foreach ($files as $file) fwrite($pipes[0], $file . "\0"); // Close STDIN pipe to start archiving. fclose($pipes[0]); // At this point, end output buffering so we can enjoy more than ~62MB of photos. ob_end_flush(); do { // Read STDOUT as `tar` is doing its work. echo stream_get_contents($pipes[1], 4096); // Are we still running? $status = proc_get_status($proc); } while (!empty($status) && $status['running']); // Close STDOUT pipe and clean up process. fclose($pipes[1]); proc_close($proc); // Allow new exports from this point onward. unset($_SESSION['current_export']); } private function getChildAlbumIds($parent_id) { $ids = []; $albums = Tag::getAlbums($parent_id, 0, PHP_INT_MAX); foreach ($albums as $album) { $ids[] = $album['id_tag']; $ids = array_merge($ids, $this->getChildAlbumIds($album['id_tag'])); } return $ids; } }