tapis_job-1.4.1-alpha1/src/Controller/JobOutputController.php
src/Controller/JobOutputController.php
<?php
namespace Drupal\tapis_job\Controller;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Url;
use Drupal\tapis_job\TapisJobInterface;
use Drupal\tapis_job\TapisProvider\TapisJobProviderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\StreamedResponse;
/**
* Class JobOutputController.
*
* This class is used to create a controller that
* allows a user to download a job's output file.
*
* @package Drupal\tapis_job\Controller
*/
class JobOutputController extends ControllerBase {
/**
* The Tapis Job provider.
*
* @var \Drupal\tapis_job\TapisProvider\TapisJobProviderInterface
*/
protected TapisJobProviderInterface $tapisJobProvider;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected RequestStack $requestStack;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a JobOutputController object.
*
* @param \Drupal\tapis_job\TapisProvider\TapisJobProviderInterface $tapisJobProvider
* The Tapis Job provider.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The configuration interface.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler interface.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
*/
public function __construct(TapisJobProviderInterface $tapisJobProvider,
RequestStack $request_stack,
ConfigFactoryInterface $configFactory,
ModuleHandlerInterface $module_handler,
EntityTypeManagerInterface $entityTypeManager) {
$this->tapisJobProvider = $tapisJobProvider;
$this->requestStack = $request_stack;
$this->configFactory = $configFactory;
$this->moduleHandler = $module_handler;
$this->entityTypeManager = $entityTypeManager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('tapis_job.tapis_job_provider'),
$container->get('request_stack'),
$container->get('config.factory'),
$container->get('module_handler'),
$container->get('entity_type.manager'),
);
}
/**
* Download a job's output file.
*
* @param \Drupal\tapis_job\TapisJobInterface|null $tapis_job
* The Tapis Job.
*
* @return \Symfony\Component\HttpFoundation\Response|\Symfony\Component\HttpFoundation\StreamedResponse|void
* return the job output file.
*/
public function getJobOutputFile(TapisJobInterface $tapis_job = NULL) {
if (!$this->currentUser()->hasPermission("create job")) {
return new RedirectResponse('/system/403');
}
if ($tapis_job) {
$tenantId = $tapis_job->getTenantId();
$request = $this->requestStack->getCurrentRequest();
$outputPath = $request->query->get('outputPath');
$mimeType = $request->query->get('mimeType');
$type = $request->query->get('type');
$execSystemId = $request->query->get('execSystemId');
$attachmentFilename = basename($outputPath);
$zip_param = FALSE;
if ($type === "dir") {
$mimeType = "application/zip";
$attachmentFilename = "$attachmentFilename.zip";
$zip_param = TRUE;
}
$jobUuid = $tapis_job->getTapisUUID();
$jobOwnerId = $tapis_job->getOwnerId();
// @todo Need to update this function to follow some other better approach
// Current approach ends up storing file
// in memory and then sending it to user.
$body = $this->tapisJobProvider->getJobOutputFileDownload($tenantId, $jobUuid, $execSystemId,
$outputPath, $zip_param, $jobOwnerId);
$content = (string) $body;
// Check if the request prefers a download.
$preferDownload = boolval($request->query->get('download', 1));
if ($preferDownload) {
$response = new StreamedResponse(function () use ($content) {
echo $content;
});
$response->headers->set('Content-Type', "application/octet-stream");
$response->headers->set('Content-Disposition', 'attachment; filename="' . $attachmentFilename . '";');
return $response;
}
else {
// Load the supported image mimes from configuration.
$config = $this->configFactory->get('tapis_job.config');
$supported_image_mimes = explode(" ", $config->get('supported_image_mimes'));
// Always serve image types as binary streams.
if (in_array($mimeType, $supported_image_mimes)) {
$response = new StreamedResponse(function () use ($content) {
echo $content;
});
$response->headers->set('Content-Type', $mimeType);
return $response;
}
else {
// Format the file content using the text file viewer formatter.
$formatted_output = $this->formatRawContent($content, $attachmentFilename, $mimeType);
// Load the Prism CSS/JS URLs from configuration.
$prism_css = $config->get('prism_css');
$prism_js = $config->get('prism_js');
// Prepare the assets array.
$assets = [
'css' => $prism_css,
'js' => $prism_js,
];
return new JsonResponse([
'content' => $formatted_output,
'assets' => $assets,
]);
}
}
}
}
/**
* Apply Prism.js syntax highlighting to the code.
*/
public function formatRawContent($fileContent, $filename, $mimetype) {
$config = $this->configFactory->get('tapis_job.config');
$supported_extensions = explode(" ", $config->get('supported_extensions'));
$supported_mimes = explode(" ", $config->get('supported_mimes'));
$markup = '';
// Get the file name extension and see if it is supported
// by any of the known file formats.
$ext = pathinfo($filename, PATHINFO_EXTENSION);
if ($ext && (in_array($ext, $supported_extensions) || in_array($mimetype, $supported_mimes))) {
$markup .= '<h3>File name: ' . $filename . '</h3>';
$code = htmlentities($fileContent, ENT_QUOTES | ENT_HTML5, 'UTF-8');
if (in_array($ext, $supported_extensions)) {
$languageClass = $this->convertExtensionToPrismClassName($ext);
$markup .= '<pre class="line-numbers"><code class="language-' . $languageClass . '">' . $code . '</code></pre>';
}
else {
$markup .= '<pre class="line-numbers"><code class="language-' . $this->convertExtensionToPrismClassName("txt") . '">' . $code . '</code></pre>';
}
}
else {
$markup = '<h3>The file extension, "' . $ext . '" is not supported for viewing.</h3>';
}
return $markup;
}
/**
* Map the extension to prime class name.
*/
private function convertExtensionToPrismClassName($ext): string {
$hTable = [
"htm" => "markup",
"html" => "markup",
"xml" => "markup",
"svg" => "markup",
"md" => "markup",
"mathml" => "markup",
"css" => "css",
"clike" => "clike",
"pl" => "perl",
"cpp" => "cpp",
"hpp" => "cpp",
"m" => "matlab",
"mat" => "matlab",
"sh" => "bash",
"csh" => "bash",
"ps1" => "powershell",
"bat" => "powershell",
"txt" => "textile",
"csv" => "textile",
"tsv" => "textile",
"cs" => "csharp",
"conf" => "apacheconf",
];
return array_key_exists($ext, $hTable) ? $hTable[$ext] : $ext;
}
/**
* Retrieves the directory contents for a specific tapis job.
*
* This method fetches a list of files and folders
* within the specified directory in the execution system
* associated with the tapis job. It formats the output
* as a file tree for use in the front-end.
*
* @param \Drupal\tapis_job\TapisJobInterface|null $tapis_job
* The Tapis job entity.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* A JSON response containing the formatted file tree data.
*/
public function getDirectoryContents(TapisJobInterface $tapis_job = NULL): JsonResponse {
$request = $this->requestStack->getCurrentRequest();
$recurse = "false";
// Fetch tapis job details.
$tenantId = $tapis_job->getTenantId();
$jobOwnerId = $tapis_job->getOwnerId();
$tapisJobDefinition = $this->tapisJobProvider->getJob($tenantId, $tapis_job->getTapisUUID(), $jobOwnerId);
// Retrieve the execution system and directory information.
$execSystemId = $tapisJobDefinition['execSystemId'];
$execSystemExecDir = $tapisJobDefinition['execSystemExecDir'];
$outputPath = $request->query->get('path', $execSystemExecDir);
// Fetch the job outputs from the tapis API.
$outputList = $this->tapisJobProvider->getJobOutputs($tenantId, $execSystemId, $outputPath, $recurse, $jobOwnerId);
// Prepare the file tree data.
$fileTreeData = [];
if ($outputList['status_code'] === 200 && !empty($outputList['result'])) {
foreach ($outputList['result'] as $outputItem) {
// Determine the relative path.
$path = substr($outputItem['path'], strpos('/' . $outputItem['path'], $execSystemExecDir) + strlen($execSystemExecDir));
$id = $outputItem['path'];
// Generate human-readable file size (if applicable).
$fileSize = $outputItem['size'] ? " (" . $this->humanizeNumBytes($outputItem['size']) . ")" : '';
// Generate URL for file or directory actions.
$jobProxyURL = Url::fromRoute('entity.tapis_job.output_file', [
'tapis_job' => $tapis_job->id(),
'outputPath' => $outputItem['path'],
'mimeType' => $outputItem['mimeType'],
'type' => $outputItem['type'],
'execSystemId' => $execSystemId,
]);
// Add file or directory to the file tree data.
$fileTreeData[] = [
"id" => $id,
'type' => $outputItem['type'] === "dir" ? 'folder' : 'file',
"text" => basename($path) . " " . $fileSize,
'icon' => 'fa fa-download',
"children" => $outputItem['type'] === "dir",
"a_attr" => [
"href" => $jobProxyURL->toString(),
'class' => $outputItem['type'] === "dir" ? 'dir-link' : 'file-link',
'dataaction' => $outputItem['type'] === "dir" ? 'download-dir' : '',
'filename' => basename($path),
],
];
}
}
// Return the file tree data as a JSON response.
return new JsonResponse($fileTreeData);
}
/**
* {@inheritdoc}
*/
private function humanizeNumBytes($bytes) {
$units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
for ($i = 0; $bytes > 1024; $i++) {
$bytes /= 1024;
}
return round($bytes, 2) . ' ' . $units[$i];
}
}
