]> git.ipfire.org Git - thirdparty/bacula.git/commitdiff
baculum: Add job files API endpoint
authorMarcin Haba <marcin.haba@bacula.pl>
Sun, 25 Oct 2020 06:39:05 +0000 (07:39 +0100)
committerMarcin Haba <marcin.haba@bacula.pl>
Sun, 25 Oct 2020 06:39:05 +0000 (07:39 +0100)
gui/baculum/protected/API/Class/JobManager.php
gui/baculum/protected/API/Pages/API/JobFiles.php
gui/baculum/protected/API/Pages/API/JobListFiles.php [new file with mode: 0644]
gui/baculum/protected/API/Pages/API/endpoints.xml
gui/baculum/protected/API/openapi_baculum.json
gui/baculum/protected/Common/Class/Errors.php
gui/baculum/protected/Common/Class/Miscellaneous.php

index 4880aa3547f434494253e6b1ad60982b0bf5096d..1ecf60a324941e9590deb0d96835ce9d019436ba 100644 (file)
@@ -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);
+       }
 }
 ?>
index 1f024c5a180761f56cc6e5004a1ad9bace1ed1ea..9a0ff932e058328b80b54693fb37702734d9d80e 100644 (file)
@@ -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 <marcin.haba@bacula.pl>
  * @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 (file)
index 0000000..3ad1d00
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+/*
+ * Bacula(R) - The Network Backup Solution
+ * Baculum   - Bacula web interface
+ *
+ * Copyright (C) 2013-2019 Kern Sibbald
+ *
+ * The main author of Baculum is Marcin Haba.
+ * The original author of Bacula is Kern Sibbald, with contributions
+ * from many others, a complete list can be found in the file AUTHORS.
+ *
+ * You may use this file and others of this release according to the
+ * license defined in the LICENSE file, which includes the Affero General
+ * Public License, v3.0 ("AGPLv3") and some additional permissions and
+ * terms pursuant to its AGPLv3 Section 7.
+ *
+ * This notice must be preserved when any source code is
+ * conveyed and/or propagated.
+ *
+ * Bacula(R) is a registered trademark of Kern Sibbald.
+ */
+
+/**
+ * List files from 'list files jobid=xx' bconsole command.
+ *
+ * @author Marcin Haba <marcin.haba@bacula.pl>
+ * @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;
+               }
+       }
+}
+?>
index 2dfb161b9e74b32bdfa383766a4eef5b4d9e90f5..404180496bbc3ff7823ecf219f56387f89b4d7c2 100644 (file)
@@ -64,7 +64,8 @@
        <url ServiceParameter="JobRun" pattern="api/v1/jobs/run/" />
        <url ServiceParameter="JobCancel" pattern="api/v1/jobs/{id}/cancel/" parameters.id="\d+"/>
        <url ServiceParameter="JobTotals" pattern="api/v1/jobs/totals/" />
-       <url ServiceParameter="JobFiles" pattern="api/v1/jobs/{id}/files/" parameters.id="\d+" />
+       <url ServiceParameter="JobListFiles" pattern="api/v1/jobs/{id}/files/" parameters.id="\d+" />
+       <url ServiceParameter="JobFiles" pattern="api/v1/jobs/files/" />
        <url ServiceParameter="RestoreRun" pattern="api/v1/jobs/restore/" />
        <!-- bvfs endpoints-->
        <url ServiceParameter="BVFSUpdate" pattern="api/v1/bvfs/update/" />
index 7100f1b97a828a561af198bb01c6751d2fd8d08f..35acd1b4b563b945cff567243471926ea1283edc 100644 (file)
                                ]
                        }
                },
+               "/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"],
index e08a29143a68eaa7318ec548548186f478a30a8f..828daa6c97a238fb3f86054eeb982abbbdbd4070 100644 (file)
@@ -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 {
index ad2fccbab17dde5e8cc6b62cd78bfcb8c5fb823b..dd77d92775757cb45369162ab961ec326f539034 100644 (file)
@@ -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);
        }