From: Marcin Haba Date: Sun, 25 Oct 2020 06:39:05 +0000 (+0100) Subject: baculum: Add job files API endpoint X-Git-Tag: Release-9.6.7~42 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=16b1cca5c0f83b95a98cdc650ccb1ae10a6a24df;p=thirdparty%2Fbacula.git baculum: Add job files API endpoint --- diff --git a/gui/baculum/protected/API/Class/JobManager.php b/gui/baculum/protected/API/Class/JobManager.php index 4880aa354..1ecf60a32 100644 --- a/gui/baculum/protected/API/Class/JobManager.php +++ b/gui/baculum/protected/API/Class/JobManager.php @@ -3,7 +3,7 @@ * Bacula(R) - The Network Backup Solution * Baculum - Bacula web interface * - * Copyright (C) 2013-2019 Kern Sibbald + * Copyright (C) 2013-2020 Kern Sibbald * * The main author of Baculum is Marcin Haba. * The original author of Bacula is Kern Sibbald, with contributions @@ -243,5 +243,60 @@ LEFT JOIN FileSet USING (FilesetId) WHERE Client.ClientId='$clientid' $jobs_criteria"; return JobRecord::finder()->findAllBySql($sql); } + + /** + * Get jobs where specific filename is stored + * + * @param string $clientid client identifier + * @param string $filename filename without path + * @param boolean $strict_mode if true then it maches exact filename, otherwise with % around filename + * @param array $allowed_jobs jobs allowed to show + * @return array jobs for specific client and filename + */ + public function getJobsByFilename($clientid, $filename, $strict_mode = false, $allowed_jobs = array()) { + $jobs_criteria = ''; + if (count($allowed_jobs) > 0) { + $jobs_sql = implode("', '", $allowed_jobs); + $jobs_criteria = " AND Job.Name IN ('" . $jobs_sql . "')"; + } + + if ($strict_mode === false) { + $filename = '%' . $filename . '%'; + } + + $fname_col = 'Path.Path || Filename.Name'; + $db_params = $this->getModule('api_config')->getConfig('db'); + if ($db_params['type'] === Database::MYSQL_TYPE) { + $fname_col = 'CONCAT(Path.Path, Filename.Name)'; + } + + $sql = "SELECT Job.JobId AS JobId, + Job.Name AS name, + $fname_col AS file, + Job.StartTime AS starttime, + Job.EndTime AS endtime, + Job.Type AS type, + Job.Level AS level, + Job.JobStatus AS jobstatus, + Job.JobFiles AS jobfiles, + Job.JobBytes AS jobbytes + FROM Client, Job, File, Filename,Path + WHERE Client.ClientId='$clientid' + AND Client.ClientId=Job.ClientId + AND Job.JobId=File.JobId + AND File.FileIndex > 0 + AND Path.PathId=File.PathId + AND Filename.FilenameId=File.FilenameId + AND Filename.Name LIKE :filename + $jobs_criteria + ORDER BY starttime DESC"; + $connection = JobRecord::finder()->getDbConnection(); + $connection->setActive(true); + $pdo = $connection->getPdoInstance(); + $sth = $pdo->prepare($sql); + $sth->bindParam(':filename', $filename, PDO::PARAM_STR, 200); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_ASSOC); + } } ?> diff --git a/gui/baculum/protected/API/Pages/API/JobFiles.php b/gui/baculum/protected/API/Pages/API/JobFiles.php index 1f024c5a1..9a0ff932e 100644 --- a/gui/baculum/protected/API/Pages/API/JobFiles.php +++ b/gui/baculum/protected/API/Pages/API/JobFiles.php @@ -3,7 +3,7 @@ * Bacula(R) - The Network Backup Solution * Baculum - Bacula web interface * - * Copyright (C) 2013-2019 Kern Sibbald + * Copyright (C) 2013-2020 Kern Sibbald * * The main author of Baculum is Marcin Haba. * The original author of Bacula is Kern Sibbald, with contributions @@ -21,7 +21,8 @@ */ /** - * List files from 'list files jobid=xx' bconsole command. + * Job files endpoint. + * It finds job by file criteria. * * @author Marcin Haba * @category API @@ -31,62 +32,43 @@ class JobFiles extends BaculumAPIServer { public function get() { $misc = $this->getModule('misc'); - $jobid = $this->Request->contains('id') ? intval($this->Request['id']) : 0; - $type = $this->Request->contains('type') && $misc->isValidListFilesType($this->Request['type']) ? $this->Request['type'] : null; - $offset = $this->Request->contains('offset') ? intval($this->Request['offset']) : 0; - $limit = $this->Request->contains('limit') ? intval($this->Request['limit']) : 0; - $search = $this->Request->contains('search') && $misc->isValidPath($this->Request['search']) ? $this->Request['search'] : null; + $filename = $this->Request->contains('filename') && $misc->isValidFilename($this->Request['filename']) ? $this->Request['filename'] : null; + $strict_mode = ($this->Request->contains('strict') && $misc->isValidBooleanTrue($this->Request['strict'])); + + $clientid = null; + if ($this->Request->contains('clientid')) { + $clientid = intval($this->Request['clientid']); + } elseif ($this->Request->contains('client') && $this->getModule('misc')->isValidName($this->Request['client'])) { + $client_row = $this->getModule('client')->getClientByName($this->Request['client']); + $clientid = is_object($client_row) ? intval($client_row->clientid) : null; + } + + if (is_null($clientid)) { + $this->output = JobError::MSG_ERROR_CLIENT_DOES_NOT_EXISTS; + $this->error = JobError::ERROR_CLIENT_DOES_NOT_EXISTS; + return; + } + + if (is_null($filename)) { + $this->output = JobError::MSG_ERROR_INVALID_FILENAME; + $this->error = JobError::ERROR_INVALID_FILENAME; + return; + } $result = $this->getModule('bconsole')->bconsoleCommand( $this->director, array('.jobs') ); + if ($result->exitcode === 0) { array_shift($result->output); - $job = $this->getModule('job')->getJobById($jobid); - if (is_object($job) && in_array($job->name, $result->output)) { - $cmd = array('list', 'files'); - if (is_string($type)) { - /** - * NOTE: type param has to be used BEFORE jobid=xx, otherwise it doesn't work. - * This behavior is also described in Bacula source code (src/dird/ua_output.c). - */ - $cmd[] = 'type="' . $type . '"'; - } - $cmd[] = 'jobid="' . $jobid . '"'; - $result = $this->getModule('bconsole')->bconsoleCommand( - $this->director, - $cmd - ); - if ($result->exitcode === 0) { - $file_list = $this->getModule('list')->parseListFilesOutput($result->output); - if (is_string($search)) { - // Find items - $file_list = $this->getModule('list')->findFileListItems($file_list, $search); - } - $total_items = count($file_list); - if ($offset > 0) { - if ($limit > 0) { - $file_list = array_slice($file_list, $offset, $limit); - } else { - $file_list = array_slice($file_list, $offset); - } - } elseif ($limit > 0) { - $file_list = array_slice($file_list, 0, $limit); - } - $this->output = array('items' => $file_list, 'total' => $total_items); - $this->error = GenericError::ERROR_NO_ERRORS; - } else { - $this->output = $result->output; - $this->error = $result->exitcode; - } - } else { - $this->output = JobError::MSG_ERROR_JOB_DOES_NOT_EXISTS; - $this->error = JobError::ERROR_JOB_DOES_NOT_EXISTS; - } + $job = $this->getModule('job')->getJobsByFilename($clientid, $filename, $strict_mode, $result->output); + $this->output = $job; + $this->error = JobError::ERROR_NO_ERRORS; } else { - $this->output = $result->output; - $this->error = $result->exitcode; + $result = is_array($result->output) ? implode('', $result->output) : $result->output; + $this->output = JobError::MSG_ERROR_WRONG_EXITCODE . $result; + $this->error = JobError::ERROR_WRONG_EXITCODE; } } } diff --git a/gui/baculum/protected/API/Pages/API/JobListFiles.php b/gui/baculum/protected/API/Pages/API/JobListFiles.php new file mode 100644 index 000000000..3ad1d0021 --- /dev/null +++ b/gui/baculum/protected/API/Pages/API/JobListFiles.php @@ -0,0 +1,93 @@ + + * @category API + * @package Baculum API + */ +class JobListFiles extends BaculumAPIServer { + + public function get() { + $misc = $this->getModule('misc'); + $jobid = $this->Request->contains('id') ? intval($this->Request['id']) : 0; + $type = $this->Request->contains('type') && $misc->isValidListFilesType($this->Request['type']) ? $this->Request['type'] : null; + $offset = $this->Request->contains('offset') ? intval($this->Request['offset']) : 0; + $limit = $this->Request->contains('limit') ? intval($this->Request['limit']) : 0; + $search = $this->Request->contains('search') && $misc->isValidPath($this->Request['search']) ? $this->Request['search'] : null; + + $result = $this->getModule('bconsole')->bconsoleCommand( + $this->director, + array('.jobs') + ); + if ($result->exitcode === 0) { + array_shift($result->output); + $job = $this->getModule('job')->getJobById($jobid); + if (is_object($job) && in_array($job->name, $result->output)) { + $cmd = array('list', 'files'); + if (is_string($type)) { + /** + * NOTE: type param has to be used BEFORE jobid=xx, otherwise it doesn't work. + * This behavior is also described in Bacula source code (src/dird/ua_output.c). + */ + $cmd[] = 'type="' . $type . '"'; + } + $cmd[] = 'jobid="' . $jobid . '"'; + $result = $this->getModule('bconsole')->bconsoleCommand( + $this->director, + $cmd + ); + if ($result->exitcode === 0) { + $file_list = $this->getModule('list')->parseListFilesOutput($result->output); + if (is_string($search)) { + // Find items + $file_list = $this->getModule('list')->findFileListItems($file_list, $search); + } + $total_items = count($file_list); + if ($offset > 0) { + if ($limit > 0) { + $file_list = array_slice($file_list, $offset, $limit); + } else { + $file_list = array_slice($file_list, $offset); + } + } elseif ($limit > 0) { + $file_list = array_slice($file_list, 0, $limit); + } + $this->output = array('items' => $file_list, 'total' => $total_items); + $this->error = GenericError::ERROR_NO_ERRORS; + } else { + $this->output = $result->output; + $this->error = $result->exitcode; + } + } else { + $this->output = JobError::MSG_ERROR_JOB_DOES_NOT_EXISTS; + $this->error = JobError::ERROR_JOB_DOES_NOT_EXISTS; + } + } else { + $this->output = $result->output; + $this->error = $result->exitcode; + } + } +} +?> diff --git a/gui/baculum/protected/API/Pages/API/endpoints.xml b/gui/baculum/protected/API/Pages/API/endpoints.xml index 2dfb161b9..404180496 100644 --- a/gui/baculum/protected/API/Pages/API/endpoints.xml +++ b/gui/baculum/protected/API/Pages/API/endpoints.xml @@ -64,7 +64,8 @@ - + + diff --git a/gui/baculum/protected/API/openapi_baculum.json b/gui/baculum/protected/API/openapi_baculum.json index 7100f1b97..35acd1b4b 100644 --- a/gui/baculum/protected/API/openapi_baculum.json +++ b/gui/baculum/protected/API/openapi_baculum.json @@ -1088,6 +1088,123 @@ ] } }, + "/api/v1/jobs/files": { + "get": { + "tags": ["jobs"], + "summary": "Search jobs by file criteria", + "description": "Get job list by file criteria.", + "responses": { + "200": { + "description": "Show job list by file criteria", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "output": { + "type": "array", + "items": { + "type": "object", + "properties": { + "jobid": { + "type": "integer", + "description": "Job identifier" + }, + "name": { + "type": "string", + "description": "Job name" + }, + "file": { + "type": "string", + "description": "Filename with full path" + }, + "starttime": { + "type": "string", + "description": "Job start time" + }, + "endtime": { + "type": "string", + "description": "Job end time" + }, + "type": { + "type": "string", + "description": "Job type", + "enum": ["B", "M", "V", "R", "I", "D", "A", "C", "c", "g"] + }, + "level": { + "type": "string", + "description": "Job level", + "enum": ["F","I", "D"] + }, + "jobstatus": { + "type": "string", + "description": "Job status. Note, some statuses can be not visible outside (used internally by Bacula)", + "enum": ["C", "R", "B", "T", "W", "E", "e", "f", "D", "A", "I", "F", "S", "m", "M", "s", "j", "c", "d", "t", "p", "i", "a", "l", "L"] + }, + "jobfiles": { + "type": "integer", + "description": "Job files" + }, + "jobbytes": { + "type": "integer", + "description": "Job bytes" + } + } + } + }, + "error": { + "type": "integer", + "description": "Error code", + "enum": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 53, 59, 1000] + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "clientid", + "in": "query", + "description": "Client identifier (used instead of 'client' parameter)", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "client", + "in": "query", + "description": "Client name (used instead of 'clientid' parameter)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "filename", + "in": "query", + "description": "Filename to find jobs containing the file. Normally it searches for files which have given 'filename' in name, like \\*filename\\*. If strict mode is used then is done equal matching filename == name.", + "required": true, + "schema": { + "type": "string", + "maxLength": 200 + } + }, + { + "name": "strict", + "in": "query", + "description": "Enables strict file matching filename == name", + "required": false, + "default": false, + "schema": { + "type": "boolean" + } + } + ] + } + }, "/api/v1/jobs/resnames": { "get": { "tags": ["jobs"], diff --git a/gui/baculum/protected/Common/Class/Errors.php b/gui/baculum/protected/Common/Class/Errors.php index e08a29143..828daa6c9 100644 --- a/gui/baculum/protected/Common/Class/Errors.php +++ b/gui/baculum/protected/Common/Class/Errors.php @@ -3,7 +3,7 @@ * Bacula(R) - The Network Backup Solution * Baculum - Bacula web interface * - * Copyright (C) 2013-2019 Kern Sibbald + * Copyright (C) 2013-2020 Kern Sibbald * * The main author of Baculum is Marcin Haba. * The original author of Bacula is Kern Sibbald, with contributions @@ -32,11 +32,13 @@ class GenericError { const ERROR_INVALID_COMMAND = 1; const ERROR_INTERNAL_ERROR = 1000; const ERROR_INVALID_PATH = 8; + const ERROR_WRONG_EXITCODE = 9; const MSG_ERROR_NO_ERRORS = ''; const MSG_ERROR_INVALID_COMMAND = 'Invalid command.'; const MSG_ERROR_INTERNAL_ERROR = 'Internal error.'; const MSG_ERROR_INVALID_PATH = 'Invalid path.'; + const MSG_ERROR_WRONG_EXITCODE = 'Wrong exitcode.'; } class DatabaseError extends GenericError { @@ -114,6 +116,7 @@ class JobError extends GenericError { const ERROR_INVALID_RPATH = 56; const ERROR_INVALID_WHERE_OPTION = 57; const ERROR_INVALID_REPLACE_OPTION = 58; + const ERROR_INVALID_FILENAME = 59; const MSG_ERROR_JOB_DOES_NOT_EXISTS = 'Job does not exist.'; const MSG_ERROR_INVALID_JOBLEVEL = 'Inputted job level is invalid.'; @@ -124,6 +127,7 @@ class JobError extends GenericError { const MSG_ERROR_INVALID_RPATH = 'Inputted rpath for restore is invalid. Proper format is b2[0-9]+.'; const MSG_ERROR_INVALID_WHERE_OPTION = 'Inputted "where" option is invalid.'; const MSG_ERROR_INVALID_REPLACE_OPTION = 'Inputted "replace" option is invalid.'; + const MSG_ERROR_INVALID_FILENAME = 'Inputted "filename" option is invalid.'; } class FileSetError extends GenericError { diff --git a/gui/baculum/protected/Common/Class/Miscellaneous.php b/gui/baculum/protected/Common/Class/Miscellaneous.php index ad2fccbab..dd77d9277 100644 --- a/gui/baculum/protected/Common/Class/Miscellaneous.php +++ b/gui/baculum/protected/Common/Class/Miscellaneous.php @@ -247,6 +247,10 @@ class Miscellaneous extends TModule { return (preg_match('/^[\p{L}\p{N}\p{Z}\p{Sc}\p{Pd}\[\]\-\'\/\\(){}:.#~_,+!$]{0,10000}$/u', $path) === 1); } + public function isValidFilename($path) { + return (preg_match('/^[\p{L}\p{N}\p{Z}\p{Sc}\p{Pd}\[\]\-\'\\(){}:.#~_,+!$]{0,1000}$/u', $path) === 1); + } + public function isValidReplace($replace) { return in_array($replace, $this->replace_opts); }