diff --git a/controllers/Download.php b/controllers/Download.php new file mode 100644 index 00000000..b1bcb957 --- /dev/null +++ b/controllers/Download.php @@ -0,0 +1,122 @@ +isLoggedIn()) + throw new NotAllowedException(); + + if(!isset($_GET['tag'])) + throw new UnexpectedValueException('Must specify an album to download'); + + $tag = (int)$_GET['tag']; + $album = Tag::fromId($tag); + + if($album->kind !== 'Album') + throw new UnexpectedValueException('Specified tag does not correspond to an album'); + + //Yes TOCTOU but it does not need to be perfect. + $lock_file = join('/', array(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'); + + $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) + { + $files = array(); + + $album_ids = array_merge(array($album->id_tag), $this->getChildAlbumIds($album->id_tag)); + foreach($album_ids as $album_id) + { + $iterator = AssetIterator::getByOptions( + array( + 'id_tag' => $album_id + ) + ); + while($asset = $iterator->Next()) + { + $files[] = join(DIRECTORY_SEPARATOR, array('.', $asset->getSubdir(), $asset->getFilename())); + } + } + + $descriptorspec = array( + 0 => array('pipe', 'r'), + 1 => array('pipe', 'w'), + ); + + $command = 'tar --null -cf - -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'); + + $album_name = $album->tag; + + header('Pragma: no-cache'); + header('Content-Description: File Download'); + header('Content-disposition: attachment; filename="' . $album_name . '.tar"'); + header('Content-Type: application/octet-stream'); + header('Content-Transfer-Encoding: binary'); + + foreach($files as $file) + fwrite($pipes[0], $file . "\0"); + + fclose($pipes[0]); + + while($chunk = stream_get_contents($pipes[1], 4096)) + echo $chunk; + + fclose($pipes[1]); + + proc_close($proc); + } + + private function getChildAlbumIds($parent_id) + { + $ids = array(); + + $albums = Tag::getAlbums($parent_id, 0, PHP_INT_MAX, 'object'); + foreach($albums as $album) + { + $ids[] = $album->id_tag; + $ids = array_merge($ids, $this->getChildAlbumIds($album->id_tag)); + } + + return $ids; + } +} diff --git a/controllers/ViewPhotoAlbum.php b/controllers/ViewPhotoAlbum.php index 8a4131b8..56debc39 100644 --- a/controllers/ViewPhotoAlbum.php +++ b/controllers/ViewPhotoAlbum.php @@ -61,7 +61,13 @@ class ViewPhotoAlbum extends HTMLController // Can we do fancy things here? // !!! TODO: permission system? $buttons = []; + if (Registry::get('user')->isLoggedIn()) + $buttons[] = [ + 'url' => BASEURL . '/download/?tag=' . $id_tag, + 'caption' => 'Download this album', + ]; + $buttons[] = [ 'url' => BASEURL . '/uploadmedia/?tag=' . $id_tag, 'caption' => 'Upload new photos here', diff --git a/models/Dispatcher.php b/models/Dispatcher.php index f9cc9307..9292788c 100644 --- a/models/Dispatcher.php +++ b/models/Dispatcher.php @@ -28,6 +28,7 @@ class Dispatcher 'suggest' => 'ProvideAutoSuggest', 'timeline' => 'ViewTimeline', 'uploadmedia' => 'UploadMedia', + 'download' => 'Download', ]; // Work around PHP's FPM not always providing PATH_INFO.