ui/controllers/job_files_controller.js
const $ = require('jquery');
const { BagItProfile } = require('../../bagit/bagit_profile');
const { BaseController } = require('./base_controller');
const { Context } = require('../../core/context');
const FileSystemReader = require('../../plugins/formats/read/file_system_reader');
const fs = require('fs');
const { Job } = require('../../core/job');
const { JobTagsForm } = require('../forms/job_tags_form');
const Templates = require('../common/templates');
const { Util } = require('../../core/util');
/**
* The JobFilesController displays the Job files page, where
* users can drag and drop the files that a Job will package
* and/or uploade.
*
* @param {URLSearchParams} params - The URL search params parsed
* from the URL used to reach this page. This should contain at
* least the Job Id.
*
* @param {string} params.id - The id of the Job being worked
* on. Job.id is a UUID string.
*/
class JobFilesController extends BaseController {
constructor(params) {
super(params, 'Jobs');
this.model = Job;
this.nameProperty = 'title';
this.job = Job.find(this.params.get('id'));
}
/**
* This displays the Job files UI, where the user can drag
* and drop files.
*/
show() {
let data = {
alertMessage: this.alertMessage,
job: this.job
}
this.alertMessage = null;
let html = Templates.jobFiles(data);
return this.containerContent(html);
}
/**
* This attaches required events to the Job files UI and
* adds the list of files and folders to be packaged to
* the UI.
*/
postRenderCallback(fnName) {
this.attachDragAndDropEvents();
this.attachDeleteEvents();
this.addItemsToUI();
}
/**
* This attaches drag and drop events, so users can add files
* and folders to the job by dragging them into the application
* window.
*/
attachDragAndDropEvents() {
let controller = this;
$('#dropZone').on('drop', function(e) {
e.preventDefault();
e.stopPropagation();
// When drag event is attached to document, use
// e.dataTransfer.files instead of what's below.
for (let f of e.originalEvent.dataTransfer.files) {
let containingItem = controller.findContainingItem(f.path);
if (containingItem) {
let msg = Context.y18n.__(
'%s has already been added to this package as part of %s',
`${f.path}\n\n`,
`\n\n${containingItem}`
);
alert(msg);
continue;
}
controller.addFileToPackageSources(f.path);
controller.addItemToUI(f.path);
}
$(e.currentTarget).removeClass('drop-zone-over');
return false;
});
$('#dropZone').on('dragover', function(e) {
e.preventDefault();
e.stopPropagation();
$(e.currentTarget).addClass('drop-zone-over');
return false;
});
$('#dropZone').on('dragleave', function(e) {
e.preventDefault();
e.stopPropagation();
$(e.currentTarget).removeClass('drop-zone-over');
return false;
});
$('#dropZone').on('dragend', function(e) {
e.preventDefault();
e.stopPropagation();
$(e.currentTarget).removeClass('drop-zone-over');
return false;
});
}
/**
* This attaches the deletion handler to the red X beside
* each file and folder. Clicking the red delete icon causes
* the filepath to be removed from the UI and from this Job's
* list of sourceFiles in {@link PackageOperation}.
*/
attachDeleteEvents() {
let controller = this;
$('#filesTable').on('click', 'td.delete-file', function(e) {
let filepath = $(e.currentTarget).data('filepath');
controller.removeItemFromUI(filepath);
Util.deleteFromArray(controller.job.packageOp.sourceFiles, filepath);
controller.job.save();
return false;
});
}
/**
* This adds files and folders to the display.
*/
addItemsToUI() {
// Do not try to process any files or directories that
// the user may have deleted.
this.job.packageOp.pruneSourceFilesUnlessJobCompleted();
var files = this.job.packageOp.sourceFiles.slice();
for(var filepath of files) {
this.addItemToUI(filepath);
}
}
/**
* This adds a single file or folder to the UI.
*/
addItemToUI(filepath) {
let controller = this;
let stats = fs.statSync(filepath);
if (stats.isFile()) {
this.addRow(filepath, 'file', 1, 0, stats.size);
} else if (stats.isDirectory()) {
let fsReader = new FileSystemReader(filepath);
fsReader.on('end', function() {
// Add 1 to dirCount, because item itself is a directory.
controller.addRow(filepath, 'directory', fsReader.fileCount,
fsReader.dirCount + 1, fsReader.byteCount);
});
fsReader.list();
}
}
/**
* This adds one row to the table that lists which files and
* folders this job will package.
*
* @param {string} filepath - The absolute path to a file or folder
* that is to be packaged as part of this job.
*
* @param {string} type - The type of item that filepath represents.
* This should be either "file" or "directory".
*
* @param {number} fileCount - The number of files contained by the
* item in param filepath. For files, this will be 1. For directories,
* it will be the total number of files in the directory and all of
* its subdirectories. (This info is readily available from the
* {@link FileSystemReader} plugin.)
*
* @param {number} dirCount - The number of directories contained by
* the item in param filepath. For files, this will be 0. For directories,
* this will be 1 (for the directory itself) plus the total count of
* all directories within that directory and all of its subdirectories.
* (This info is readily available from the {@link FileSystemReader}
* plugin.)
*
* @param {number} byteCount - The total number of bytes contained by
* the item at filepath. For files, this will be the filesize. For
* directories, this will be the sum of bytes contained by all the files
* within the directory and all its subdirectories. (This info is readily
* available from the {@link FileSystemReader} plugin.)
*
*/
addRow(filepath, type, fileCount, dirCount, byteCount) {
$('#filesPanel').show();
let row = this.getTableRow(filepath, type, fileCount, dirCount, byteCount);
$(row).insertBefore('#fileTotals');
this.updateTotals(fileCount, dirCount, byteCount);
}
/**
* Updates the total number of files, folders, and bytes to be
* packaged.
*
* @param {number} fileCount - The number of files contained by the
* item in param filepath. For files, this will be 1. For directories,
* it will be the total number of files in the directory and all of
* its subdirectories. (This info is readily available from the
* {@link FileSystemReader} plugin.) This number will be added to
* the total file count.
*
* @param {number} dirCount - The number of directories contained by
* the item in param filepath. For files, this will be 0. For directories,
* this will be 1 (for the directory itself) plus the total count of
* all directories within that directory and all of its subdirectories.
* (This info is readily available from the {@link FileSystemReader}
* plugin.) This number will be added to the total directory count.
*
* @param {number} byteCount - The total number of bytes contained by
* the item at filepath. For files, this will be the filesize. For
* directories, this will be the sum of bytes contained by all the files
* within the directory and all its subdirectories. (This info is readily
* available from the {@link FileSystemReader} plugin.) This number will
* be added to the total byte count.
*
*/
updateTotals(fileCount, dirCount, byteCount) {
let filesTotal = this.updateTotal('#totalFileCount', fileCount);
let dirTotal = this.updateTotal('#totalDirCount', dirCount);
let byteTotal = this.updateTotal('#totalByteCount', byteCount);
this.job.fileCount = filesTotal;
this.job.dirCount = dirTotal;
this.job.byteCount = byteTotal;
this.job.save();
}
/**
* Updates one of the total fields at the bottom of the list of
* files and folders, and returns the calculated total.
*
* @param {string} elementId - The id (css selector) of the element
* whose text should be updated.
*
* @param {number} amountToAdd - The number to add to the existing
* total already displayed in the cell. This will be negative in
* cases where you're removing files or folders.
*
* @returns {number} The total value displayed in the field.
*/
updateTotal(elementId, amountToAdd) {
let element = $(elementId);
let newTotal = parseInt(element.data('total'), 10) + amountToAdd;
element.data('total', newTotal);
if (elementId == '#totalByteCount') {
element.text(Util.toHumanSize(newTotal));
} else {
element.text(newTotal);
}
return newTotal;
}
/**
* Returns the HTML for a single table row in the files/folders
* display.
*
* @param {string} filepath - The absolute path to a file or folder
* that is to be packaged as part of this job.
*
* @param {string} type - The type of item that filepath represents.
* This should be either "file" or "directory".
*
* @param {number} fileCount - The number of files contained by the
* item in param filepath. For files, this will be 1. For directories,
* it will be the total number of files in the directory and all of
* its subdirectories. (This info is readily available from the
* {@link FileSystemReader} plugin.)
*
* @param {number} dirCount - The number of directories contained by
* the item in param filepath. For files, this will be 0. For directories,
* this will be 1 (for the directory itself) plus the total count of
* all directories within that directory and all of its subdirectories.
* (This info is readily available from the {@link FileSystemReader}
* plugin.)
*
* @param {number} byteCount - The total number of bytes contained by
* the item at filepath. For files, this will be the filesize. For
* directories, this will be the sum of bytes contained by all the files
* within the directory and all its subdirectories. (This info is readily
* available from the {@link FileSystemReader} plugin.)
*
* @returns {string} A string of HTML representing a table row.
*/
getTableRow(filepath, type, fileCount, dirCount, byteCount) {
let iconType = (type == 'file' ? 'file' : 'folder-closed');
let data = {
iconType: iconType,
filepath: filepath,
dirCount: dirCount,
fileCount: fileCount,
byteCount: byteCount,
size: Util.toHumanSize(byteCount)
}
return Templates.jobFileRow(data);
}
/**
* This adds a file or folder to the list of items that will be
* packaged in this job. The item is added to the sourceFiles
* list of the Job's {@link PackageOperation} attribute.
*
* @param {string} filepath - The absolute path to a file or folder
* that is to be packaged as part of this job.
*
*/
addFileToPackageSources(filepath) {
if (!Array.isArray(this.job.packageOp.sourceFiles)) {
this.job.packageOp.sourceFiles = [];
}
this.job.packageOp.sourceFiles.push(filepath);
}
/**
* This checks the list of source files and folders to see if
* any of the items already set to be packaged contains the item
* that the user just dragged into the window. We make this check
* to avoid adding duplicate files or folders to the list of
* sources.
*
* @param {string} filepath - The absolute path to a file or folder
* that is to be packaged as part of this job.
*
* @return {string} - The path the already-added folder that contains
* filepath, or null if filepath's containing folder has not already
* been added to the list of source files.
*
*/
findContainingItem(filepath) {
if (Array.isArray(this.job.packageOp.sourceFiles)) {
for (let item of this.job.packageOp.sourceFiles) {
if (item === filepath || (filepath.startsWith(item) && Util.isDirectory(item))) {
return item;
}
}
}
return null;
}
/**
* This deletes a file or folder from the list of files and folders
* in the UI (but does not delete the item from the underlying
* {@link PackageOperation}).
*
* @param {string} filepath - The absolute path to a file or folder
* to be removed from the UI.
*
*/
removeItemFromUI(filepath) {
let row = $(`tr[data-filepath="${Util.escapeBackslashes(filepath)}"]`)
let dirCount = -1 * parseInt(row.find('td.dirCount').text(), 10);
let fileCount = -1 * parseInt(row.find('td.fileCount').text(), 10);
let fileSize = -1 * parseInt(row.find('td.fileSize').data('bytes'), 10);
row.remove();
this.updateTotals(fileCount, dirCount, fileSize)
if ($('tr.filepath').length == 0) {
$('#filesPanel').hide();
}
}
}
module.exports.JobFilesController = JobFilesController;