const { AppSetting } = require('./app_setting');
const { BagItProfile } = require('../bagit/bagit_profile');
const { Context } = require('./context');
const dateFormat = require('dateformat');
const fs = require('fs');
const { PackageOperation } = require('./package_operation');
const path = require('path');
const { PersistentObject } = require('./persistent_object');
const { ValidationOperation } = require('./validation_operation');
const { UploadOperation } = require('./upload_operation');
const { Util } = require('./util');
/**
* This is a list of BagItProfile tags to check to try to find a
* meaningful title for this job. DART checks them in order and
* returns the first one that has a non-empty user-defined value.
*/
const titleTags = [
"Title",
"Internal-Sender-Identifier",
"External-Identifier",
"Internal-Sender-Description",
"External-Description",
"Description"
];
/**
* Job describes a series of related actions for DART to perform.
* This typically includes packaging a number of files and sending
* them across a network to one or more remote repositories, though
* it does not have to include those actions. A job may consist of
* a single action, such as validating a bag or uploading a file
* to an S3 bucket.
*/
class Job extends PersistentObject {
/**
* @param {string} opts.id - A UUID in hex-string format. This is
* the object's unique identifier.
*
* @param {boolean} opts.userCanDelete - Indicates whether user is
* allowed to delete this record.
*
* @param {BagItProfile} opts.bagItProfile - A BagItProfile object.
* This is required only for bagging and validation jobs.
*
* @param {PackageOperation} opts.packageOp - An object describing
* what this job is supposed to package. The is relevant only to
* jobs that involving bagging or other forms of packaging.
*
* @param {ValidationOperation} opts.validationOp - An object
* describing what is to be validated. This is relevant only if the
* job includes a validation step.
*
* @param {Array<UploadOperation>} opts.uploadOps - A list of objects
* describing what should be uploaded, and to where. This is relevant
* only for jobs that will be uploading materials.
*/
constructor(opts = {}) {
super(opts);
this.bagItProfile = opts.bagItProfile || null;
this.packageOp = opts.packageOp || new PackageOperation();
this.validationOp = opts.validationOp || null;
this.uploadOps = opts.uploadOps || [];
this.createdAt = opts.createdAt || new Date();
/**
* This hash will contain descriptions of job validation errors
* after you call job.validate(). This does NOT contain information
* about errors that occurred while running the job. For those, see
* the result propertype (type {@link OperationResult}) of the
* job's {@link PackageOperation}, {@link ValidationOperation},
* or {@link UploadOperation}.
*/
this.errors = {};
/**
* The total number of files to be packaged and/or uploaded
* by this job. This value is set when building a Job through
* the DART UI, but it may not be set by the command-line tools.
* If DART does not set this value, it remains at the default
* of -1.
*
* @type {number}
* @default -1
*/
this.fileCount = -1;
/**
* The total number of directories to be packaged and/or uploaded
* by this job. This value is set when building a Job through
* the DART UI, but it may not be set by the command-line tools.
* If DART does not set this value, it remains at the default
* of -1.
*
* @type {number}
* @default -1
*/
this.dirCount = -1;
/**
* The total number of bytes in the payload to be packaged
* and/or uploaded by this job. This number may not match
* to total size of the bag the job produces because the bag
* may include additiaonal manifests and tag files, and it may
* be serialized to a format like tar that makes it slightly
* larger, or to a format like zip or gzip that makes it
* smaller.
*
* This value is set when building a Job through the DART UI,
* but it may not be set by the command-line tools.
* If DART does not set this value, it remains at the default
* of -1.
*
* @type {number}
* @default -1
*/
this.byteCount = -1;
/**
* The id of the workflow from which this job was created.
* If the job was not created from a {@link Workflow}, this
* will be null.
*
* This attribute is significant only to some portions of the
* UI, which may hide some job configution options since the
* workflow already describes what those options should be.
*
* For info on how a {@link Workflow} becomes a {@link Job},
* see {@link JobParams#toJob}.
*
* @type {string}
*/
this.workflowId = null;
}
/**
* This returns a title for display purposes. It will use the first
* available non-empty value of: 1) the name of the file that the job
* packaged, 2) the name of the file that the job uploaded, or 3) a
* title or description of the bag from within the bag's tag files.
* If none of those is available, this will return "Job of <timestamp>",
* where timestamp is date and time the job was created.
*
* @returns {string}
*/
get title() {
// Try to get the name of the file that was created or uploaded.
var name = null;
if (!name && this.packageOp && this.packageOp.packageName) {
name = path.basename(this.packageOp.packageName);
}
if (!name && this.uploadOps.length > 0 && this.uploadOps[0].sourceFiles.length > 0) {
name = path.basename(this.uploadOps[0].sourceFiles[0]);
}
// Try to get a title from the bag.
if (!name && this.bagItProfile) {
for (let tagName of titleTags) {
let tag = this.bagItProfile.firstMatchingTag('tagName', tagName);
if (tag && tag.userValue) {
name = tag.userValue;
break;
}
}
}
// If no title or filename, create a generic name.
if (!name) {
name = Job.genericName(this.createdAt);
}
return Util.truncateString(name, 40);
}
/**
* Returns the datetime on which this job's package operation completed.
*
* @returns {Date}
*/
packagedAt() {
var packagedAt = null;
if (this.packageOp && this.packageOp.result && this.packageOp.result.completed) {
packagedAt = this.packageOp.result.completed;
}
return packagedAt;
}
/**
* Returns true if DART attempted to execute this job's package
* operation.
*
* @returns {boolean}
*/
packageAttempted() {
if (this.packageOp && this.packageOp.result) {
return this.packageOp.result.attempt > 0;
}
return false;
}
/**
* Returns true if DART successfully completed this job's package
* operation. Note that this will return false if packaging failed
* and if packaging was never attempted, so check
* {@link packageAttempted} as well.
*
* @returns {boolean}
*/
packageSucceeded() {
if (this.packageOp && this.packageOp.result) {
return this.packageOp.result.succeeded();
}
return false;
}
/**
* Returns the datetime on which this job's validation operation completed.
*
* @returns {Date}
*/
validatedAt() {
var validatedAt = null;
if (this.validationOp && this.validationOp.result && this.validationOp.result.completed) {
validatedAt = this.validationOp.result.completed;
}
return validatedAt;
}
/**
* Returns true if DART attempted to execute this job's validation
* operation.
*
* @returns {boolean}
*/
validationAttempted() {
if (this.validationOp && this.validationOp.result) {
return this.validationOp.result.attempt > 0;
}
return false;
}
/**
* Returns true if DART successfully completed this job's validation
* operation. See {@link validationAttempted} as well.
*
* @returns {boolean}
*/
validationSucceeded() {
if (this.validationOp && this.validationOp.result) {
return this.validationOp.result.succeeded();
}
return false;
}
/**
* Returns the datetime on which this job's last upload operation completed.
*
* @returns {Date}
*/
uploadedAt() {
var uploadedAt = null;
if (this.uploadOps.length > 0) {
for (let uploadOp of this.uploadOps) {
for (let result of uploadOp.results) {
if (result && result.completed) {
uploadedAt = result.completed;
}
}
}
}
return uploadedAt;
}
/**
* Returns true if DART attempted to execute any of this job's upload
* operations.
*
* @returns {boolean}
*/
uploadAttempted() {
if (this.uploadOps) {
for (let op of this.uploadOps) {
for (let result of op.results) {
if (result.attempt > 0) {
return true;
}
}
}
}
return false;
}
/**
* Returns true if DART successfully completed all of this job's upload
* operations. See {@link uploadAttempted} as well.
*
* @returns {boolean}
*/
uploadSucceeded() {
let anyAttempted = false;
let allSucceeded = true;
if (this.uploadOps) {
for (let op of this.uploadOps) {
for (let result of op.results) {
if (result.attempt > 0) {
anyAttempted = true;
}
if (!result.succeeded()) {
allSucceeded = false;
}
}
}
}
return (anyAttempted && allSucceeded);
}
/**
* validate returns true or false, indicating whether this object
* contains complete and valid data. If it returns false, check
* the errors property for specific errors.
*
* @returns {boolean}
*/
validate() {
super.validate();
if (this.packageOp) {
this.packageOp.validate();
Object.assign(this.errors, this.packageOp.errors);
if (this.packageOp.packageFormat == 'BagIt' && this.bagItProfile == null) {
result.errors['Job.bagItProfile'] = Context.y18n.__('BagIt packaging requires a BagItProfile. Make sure the profile has not been deleted.');
}
// TODO: Mechanism for signifying this is a BagIt job (as opposed to just tar or zip)
}
if (this.validationOp) {
this.validationOp.validate();
Object.assign(this.errors, this.validationOp.errors);
if (!this.bagItProfile && !result.errors['Job.bagItProfile']) {
result.errors['Job.bagItProfile'] = Context.y18n.__('Validation requires a BagItProfile.');
}
}
var opNum = 0;
for (var uploadOp of this.uploadOps) {
uploadOp.validate();
Object.assign(this.errors, uploadOp.errors);
opNum++;
}
return Object.keys(this.errors).length == 0;
}
/**
* Returns a list of errors from all of this job's operations.
*
* @returns {Array<string>}
*/
getRunErrors() {
let errs = [];
if (this.packageOp && this.packageOp.result) {
errs = errs.concat(this.packageOp.result.errors);
}
if (this.validationOp && this.validationOp.result) {
errs = errs.concat(this.validationOp.result.errors);
}
if (this.uploadOps && this.uploadOps.length > 0) {
for (let op of this.uploadOps) {
for (let result of op.results) {
errs = errs.concat(result.errors);
}
}
}
return errs.map(e => Util.formatError(e));
}
/**
* Returns true if this job is currently running.
*
* @returns {boolean}
*/
isRunning() {
for (let dartProcess of Object.values(Context.childProcesses)) {
if (dartProcess.jobId == this.id) {
return true;
}
}
return false;
}
/**
* This returns a generic job name that includes a date and
* time.
*
* @param {Date} date - The date the job was created. Use job.createdAt.
*
* @returns {string}
*/
static genericName(date) {
return Context.y18n.__(
"Job of %s %s",
dateFormat(date, 'shortDate'),
dateFormat(date, 'shortTime'));
}
/**
* This converts the JSON representation of a job as stored in the DB
* to a full-fledged Job object with all of the expected methods.
*
* @param {Object} data - A JavaScript hash.
*
* @returns {Job}
*/
static inflateFrom(data) {
let job = new Job();
Object.assign(job, data);
if (data.bagItProfile) {
job.bagItProfile = BagItProfile.inflateFrom(data.bagItProfile);
}
if (data.packageOp) {
job.packageOp = PackageOperation.inflateFrom(data.packageOp);
}
if (data.validationOp) {
job.validationOp = ValidationOperation.inflateFrom(data.validationOp);
}
if (data.uploadOps) {
job.uploadOps = [];
for (let op of data.uploadOps) {
job.uploadOps.push(UploadOperation.inflateFrom(op));
}
}
return job;
}
/**
* This converts the JSON data in the file at pathToFile into a
* Job object.
*
* @param {string} pathToFile - The path to the JSON file.
*
* @returns {Job}
*/
static inflateFromFile(pathToFile) {
let data = JSON.parse(fs.readFileSync(pathToFile, 'utf8'));
return Job.inflateFrom(data);
}
/**
* find finds the Job with the specified id in the datastore
* and returns it. Returns undefined if the object is not in the datastore.
*
* This overrides the find() method of the PersistentObject to return
* a correctly constructed Job object.
*
* @param {string} id - The id (UUID) of the job you want to find.
*
* @returns {Job}
*/
static find(id) {
let data = Context.db('Job').get(id);
if (data) {
return Job.inflateFrom(data);
}
return undefined;
}
}
// Get static methods from base.
Object.assign(Job, PersistentObject);
module.exports.Job = Job;