Rework album/tag downloads #19
@ -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()
|
||||||
{
|
{
|
||||||
@ -16,63 +16,46 @@ class Download extends HTMLController
|
|||||||
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.
|
|
||||||
$lock_file = join('/', [sys_get_temp_dir(), 'pics-export.lock']);
|
|
||||||
if(!file_exists($lock_file))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
$fp = fopen($lock_file, 'x');
|
|
||||||
|
|
||||||
if(!$fp)
|
|
||||||
throw new UnexpectedValueException('Could not open lock-file');
|
|
||||||
|
|
||||||
|
// So far so good?
|
||||||
$this->exportAlbum($album);
|
$this->exportAlbum($album);
|
||||||
}
|
exit;
|
||||||
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');
|
||||||
|
|
||||||
|
// Write filenames to include to STDIN, separated by null bytes.
|
||||||
foreach ($files as $file)
|
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;
|
||||||
|
Loading…
Reference in New Issue
Block a user
Ik zie deze check, maar ik zie niet waar deze session variable geset word? Of mis ik iets.
Oef, terecht. Ik vermoed dat ik die ben verloren in een overijverige rebase. Gaan we fixen.