* 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
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);
+ }
}
?>
* 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
*/
/**
- * 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
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;
}
}
}
--- /dev/null
+<?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;
+ }
+ }
+}
+?>
<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/" />
]
}
},
+ "/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"],
* 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
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 {
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.';
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 {
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);
}