Merge pull request 'Rework album/tag downloads' (#19) from tag-download into master

This commit is contained in:
Roflin 2020-03-11 20:04:26 +01:00
commit 909d50efa8

View File

@ -6,7 +6,7 @@
* Kabuki CMS (C) 2013-2019, Aaron van Geffen * Kabuki CMS (C) 2013-2019, Aaron van Geffen
*****************************************************************************/ *****************************************************************************/
class Download extends HTMLController class Download
{ {
public function __construct() public function __construct()
{ {
@ -15,64 +15,47 @@ class Download extends HTMLController
if (!$user->isLoggedIn()) if (!$user->isLoggedIn())
throw new NotAllowedException(); throw new NotAllowedException();
if(!isset($_GET['tag'])) if (!isset($_GET['tag']))
throw new UnexpectedValueException('Must specify an album to download'); throw new UserFacingException('No album or tag has been specified for download.');
$tag = (int)$_GET['tag']; $tag = (int)$_GET['tag'];
$album = Tag::fromId($tag); $album = Tag::fromId($tag);
if($album->kind !== 'Album') if (isset($_SESSION['current_export']))
throw new UnexpectedValueException('Specified tag does not correspond to an album'); throw new UserFacingException('You can only export one album at the same time. Please wait until the other download finishes, or try again later.');
//Yes TOCTOU but it does not need to be perfect. // So far so good?
$lock_file = join('/', [sys_get_temp_dir(), 'pics-export.lock']); $this->exportAlbum($album);
if(!file_exists($lock_file)) exit;
{
try
{
$fp = fopen($lock_file, 'x');
if(!$fp)
throw new UnexpectedValueException('Could not open lock-file');
$this->exportAlbum($album);
}
finally
{
fclose($fp);
unlink($lock_file);
}
}
else
throw new UnexpectedValueException('Another export is busy, please try again later');
exit();
} }
private function exportAlbum($album) private function exportAlbum(Tag $album)
{ {
$files = []; $files = [];
$album_ids = array_merge([$album->id_tag], $this->getChildAlbumIds($album->id_tag)); $album_ids = array_merge([$album->id_tag], $this->getChildAlbumIds($album->id_tag));
foreach($album_ids as $album_id) foreach ($album_ids as $album_id)
{ {
$iterator = AssetIterator::getByOptions( $iterator = AssetIterator::getByOptions(['id_tag' => $album_id]);
[ while ($asset = $iterator->next())
'id_tag' => $album_id $files[] = join(DIRECTORY_SEPARATOR, [$asset->getSubdir(), $asset->getFilename()]);
]
);
while($asset = $iterator->Next())
{
$files[] = join(DIRECTORY_SEPARATOR, ['.', $asset->getSubdir(), $asset->getFilename()]);
}
} }
$descriptorspec = [ $descriptorspec = [
0 => ['pipe', 'r'], 0 => ['pipe', 'r'], // STDIN
1 => ['pipe', 'w'], 1 => ['pipe', 'w'], // STDOUT
]; ];
$command = 'tar --null -cf - -T -'; // 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); $proc = proc_open($command, $descriptorspec, $pipes, ASSETSDIR);
@ -85,36 +68,53 @@ class Download extends HTMLController
if(!$pipes[1]) if(!$pipes[1])
throw new UnexpectedValueException('Could not open pipe for STDOUT'); throw new UnexpectedValueException('Could not open pipe for STDOUT');
$album_name = $album->tag; // STDOUT should not block.
stream_set_blocking($pipes[1], 0);
header('Pragma: no-cache'); header('Pragma: no-cache');
header('Content-Description: File Download'); header('Content-Description: File Download');
header('Content-disposition: attachment; filename="' . $album_name . '.tar"'); header('Content-disposition: attachment; filename="' . $album->tag . '.tar"');
header('Content-Type: application/octet-stream'); header('Content-Type: application/octet-stream');
header('Content-Transfer-Encoding: binary'); header('Content-Transfer-Encoding: binary');
foreach($files as $file) // Write filenames to include to STDIN, separated by null bytes.
foreach ($files as $file)
fwrite($pipes[0], $file . "\0"); fwrite($pipes[0], $file . "\0");
// Close STDIN pipe to start archiving.
fclose($pipes[0]); fclose($pipes[0]);
while($chunk = stream_get_contents($pipes[1], 4096)) // At this point, end output buffering so we can enjoy more than ~62MB of photos.
echo $chunk; 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]); fclose($pipes[1]);
proc_close($proc); proc_close($proc);
// Allow new exports from this point onward.
unset($_SESSION['current_export']);
} }
private function getChildAlbumIds($parent_id) private function getChildAlbumIds($parent_id)
{ {
$ids = []; $ids = [];
$albums = Tag::getAlbums($parent_id, 0, PHP_INT_MAX, 'object'); $albums = Tag::getAlbums($parent_id, 0, PHP_INT_MAX);
foreach($albums as $album) foreach ($albums as $album)
{ {
$ids[] = $album->id_tag; $ids[] = $album['id_tag'];
$ids = array_merge($ids, $this->getChildAlbumIds($album->id_tag)); $ids = array_merge($ids, $this->getChildAlbumIds($album['id_tag']));
} }
return $ids; return $ids;