Merge pull request 'Rework album/tag downloads' (#19) from tag-download into master
This commit is contained in:
		
						commit
						909d50efa8
					
				| @ -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; | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user