enigma-bbs/core/oputil/oputil_file_base.js

464 lines
12 KiB
JavaScript

/* jslint node: true */
/* eslint-disable no-console */
'use strict';
const printUsageAndSetExitCode = require('./oputil_common.js').printUsageAndSetExitCode;
const ExitCodes = require('./oputil_common.js').ExitCodes;
const argv = require('./oputil_common.js').argv;
const initConfigAndDatabases = require('./oputil_common.js').initConfigAndDatabases;
const getHelpFor = require('./oputil_help.js').getHelpFor;
const getAreaAndStorage = require('./oputil_common.js').getAreaAndStorage;
const Errors = require('../../core/enig_error.js').Errors;
const getDescFromFileName = require('../../core/file_base_area.js').getDescFromFileName;
const async = require('async');
const fs = require('fs');
const paths = require('path');
const _ = require('lodash');
const moment = require('moment');
const inq = require('inquirer');
exports.handleFileBaseCommand = handleFileBaseCommand;
/*
:TODO:
Global options:
--yes: assume yes
--no-prompt: try to avoid user input
Prompt for import and description before scan
* Only after finding duplicate-by-path
* Default to filename -> desc if auto import
*/
let fileArea; // required during init
function finalizeEntryAndPersist(fileEntry, cb) {
async.series(
[
function getDescIfNeeded(callback) {
if(false === argv.prompt || ( fileEntry.desc && fileEntry.desc.length > 0 ) ) {
return callback(null);
}
const questions = [
{
name : 'desc',
message : `Description for ${fileEntry.fileName}:`,
type : 'input',
default : getDescFromFileName(fileEntry.fileName),
}
];
inq.prompt(questions).then( answers => {
fileEntry.desc = answers.desc;
return callback(null);
});
},
function persist(callback) {
fileEntry.persist( err => {
return callback(err);
});
}
],
err => {
return cb(err);
}
);
}
function scanFileAreaForChanges(areaInfo, options, cb) {
const storageLocations = fileArea.getAreaStorageLocations(areaInfo).filter(sl => {
return options.areaAndStorageInfo.find(asi => {
return !asi.storageTag || sl.storageTag === asi.storageTag;
});
});
async.eachSeries(storageLocations, (storageLoc, nextLocation) => {
async.series(
[
function scanPhysFiles(callback) {
const physDir = storageLoc.dir;
fs.readdir(physDir, (err, files) => {
if(err) {
return callback(err);
}
async.eachSeries(files, (fileName, nextFile) => {
const fullPath = paths.join(physDir, fileName);
fs.stat(fullPath, (err, stats) => {
if(err) {
// :TODO: Log me!
return nextFile(null); // always try next file
}
if(!stats.isFile()) {
return nextFile(null);
}
process.stdout.write(`Scanning ${fullPath}... `);
fileArea.scanFile(
fullPath,
{
areaTag : areaInfo.areaTag,
storageTag : storageLoc.storageTag
},
(err, fileEntry, dupeEntries) => {
if(err) {
// :TODO: Log me!!!
console.info(`Error: ${err.message}`);
return nextFile(null); // try next anyway
}
if(dupeEntries.length > 0) {
// :TODO: Handle duplidates -- what to do here???
console.info('Dupe');
return nextFile(null);
} else {
console.info('Done!');
if(Array.isArray(options.tags)) {
options.tags.forEach(tag => {
fileEntry.hashTags.add(tag);
});
}
finalizeEntryAndPersist(fileEntry, err => {
return nextFile(err);
});
}
}
);
});
}, err => {
return callback(err);
});
});
},
function scanDbEntries(callback) {
// :TODO: Look @ db entries for area that were *not* processed above
return callback(null);
}
],
err => {
return nextLocation(err);
}
);
},
err => {
return cb(err);
});
}
function dumpAreaInfo(areaInfo, areaAndStorageInfo, cb) {
console.info(`areaTag: ${areaInfo.areaTag}`);
console.info(`name: ${areaInfo.name}`);
console.info(`desc: ${areaInfo.desc}`);
areaInfo.storage.forEach(si => {
console.info(`storageTag: ${si.storageTag} => ${si.dir}`);
});
console.info('');
return cb(null);
}
function getSpecificFileEntry(pattern, cb) {
// spec: FILE_ID|SHA|PARTIAL_SHA
const FileEntry = require('../../core/file_entry.js');
async.waterfall(
[
function getByFileId(callback) {
const fileId = parseInt(pattern);
if(!/^[0-9]+$/.test(pattern) || isNaN(fileId)) {
return callback(null, null);
}
const fileEntry = new FileEntry();
fileEntry.load(fileId, () => {
return callback(null, fileEntry); // try sha
});
},
function getBySha(fileEntry, callback) {
if(fileEntry) {
return callback(null, fileEntry); // already got it by sha
}
FileEntry.findFileBySha(pattern, (err, fileEntry) => {
return callback(err, fileEntry);
});
},
],
(err, fileEntry) => {
return cb(err, fileEntry);
}
);
}
function dumpFileInfo(shaOrFileId, cb) {
async.waterfall(
[
function getEntry(callback) {
getSpecificFileEntry(shaOrFileId, (err, fileEntry) => {
return callback(err, fileEntry);
});
},
function dumpInfo(fileEntry, callback) {
const fullPath = paths.join(fileArea.getAreaStorageDirectoryByTag(fileEntry.storageTag), fileEntry.fileName);
console.info(`file_id: ${fileEntry.fileId}`);
console.info(`sha_256: ${fileEntry.fileSha256}`);
console.info(`area_tag: ${fileEntry.areaTag}`);
console.info(`storage_tag: ${fileEntry.storageTag}`);
console.info(`path: ${fullPath}`);
console.info(`hashTags: ${Array.from(fileEntry.hashTags).join(', ')}`);
console.info(`uploaded: ${moment(fileEntry.uploadTimestamp).format()}`);
_.each(fileEntry.meta, (metaValue, metaName) => {
console.info(`${metaName}: ${metaValue}`);
});
if(argv['show-desc']) {
console.info(`${fileEntry.desc}`);
}
console.info('');
return callback(null);
}
],
err => {
return cb(err);
}
);
}
function displayFileAreaInfo() {
// AREA_TAG[@STORAGE_TAG]
// SHA256|PARTIAL
// if sha: dump file info
// if area/stoarge dump area(s) +
async.series(
[
function init(callback) {
return initConfigAndDatabases(callback);
},
function dumpInfo(callback) {
const Config = require('../../core/config.js').config;
let suppliedAreas = argv._.slice(2);
if(!suppliedAreas || 0 === suppliedAreas.length) {
suppliedAreas = _.map(Config.fileBase.areas, (areaInfo, areaTag) => areaTag);
}
const areaAndStorageInfo = getAreaAndStorage(suppliedAreas);
fileArea = require('../../core/file_base_area.js');
async.eachSeries(areaAndStorageInfo, (areaAndStorage, nextArea) => {
const areaInfo = fileArea.getFileAreaByTag(areaAndStorage.areaTag);
if(areaInfo) {
return dumpAreaInfo(areaInfo, areaAndStorageInfo, nextArea);
} else {
return dumpFileInfo(areaAndStorage.areaTag, nextArea);
}
},
err => {
return callback(err);
});
}
],
err => {
if(err) {
process.exitCode = ExitCodes.ERROR;
console.error(err.message);
}
}
);
}
function scanFileAreas() {
const options = {};
const tags = argv.tags;
if(tags) {
options.tags = tags.split(',');
}
options.areaAndStorageInfo = getAreaAndStorage(argv._.slice(2));
async.series(
[
function init(callback) {
return initConfigAndDatabases(callback);
},
function scanAreas(callback) {
fileArea = require('../../core/file_base_area.js');
async.eachSeries(options.areaAndStorageInfo, (areaAndStorage, nextAreaTag) => {
const areaInfo = fileArea.getFileAreaByTag(areaAndStorage.areaTag);
if(!areaInfo) {
return nextAreaTag(new Error(`Invalid file base area tag: ${areaAndStorage.areaTag}`));
}
console.info(`Processing area "${areaInfo.name}":`);
scanFileAreaForChanges(areaInfo, options, err => {
return callback(err);
});
}, err => {
return callback(err);
});
}
],
err => {
if(err) {
process.exitCode = ExitCodes.ERROR;
console.error(err.message);
}
}
);
}
function moveFiles() {
//
// oputil fb move SRC [SRC2 ...] DST
//
// SRC: PATH|FILE_ID|SHA|AREA_TAG[@STORAGE_TAG]
// DST: AREA_TAG[@STORAGE_TAG]
//
if(argv._.length < 4) {
return printUsageAndSetExitCode(getHelpFor('FileBase'), ExitCodes.ERROR);
}
const moveArgs = argv._.slice(2);
let src = getAreaAndStorage(moveArgs.slice(0, -1));
let dst = getAreaAndStorage(moveArgs.slice(-1))[0];
let FileEntry;
async.waterfall(
[
function init(callback) {
return initConfigAndDatabases( err => {
if(!err) {
fileArea = require('../../core/file_base_area.js');
}
return callback(err);
});
},
function validateAndExpandSourceAndDest(callback) {
let srcEntries = [];
const areaInfo = fileArea.getFileAreaByTag(dst.areaTag);
if(areaInfo) {
dst.areaInfo = areaInfo;
} else {
return callback(Errors.DoesNotExist('Invalid or unknown destination area'));
}
// Each SRC may be PATH|FILE_ID|SHA|AREA_TAG[@STORAGE_TAG]
FileEntry = require('../../core/file_entry.js');
async.eachSeries(src, (areaAndStorage, next) => {
//
// If this entry represents a area tag, it means *all files* in that area
//
const areaInfo = fileArea.getFileAreaByTag(areaAndStorage.areaTag);
if(areaInfo) {
src.areaInfo = areaInfo;
const findFilter = {
areaTag : areaAndStorage.areaTag,
};
if(areaAndStorage.storageTag) {
findFilter.storageTag = areaAndStorage.storageTag;
}
FileEntry.findFiles(findFilter, (err, fileIds) => {
if(err) {
return next(err);
}
async.each(fileIds, (fileId, nextFileId) => {
const fileEntry = new FileEntry();
fileEntry.load(fileId, err => {
if(!err) {
srcEntries.push(fileEntry);
}
return nextFileId(err);
});
},
err => {
return next(err);
});
});
} else {
// PATH|FILE_ID|SHA|PARTIAL_SHA
getSpecificFileEntry(areaAndStorage.pattern, (err, fileEntry) => {
if(err) {
return next(err);
}
srcEntries.push(fileEntry);
return next(null);
});
}
},
err => {
return callback(err, srcEntries);
});
},
function moveEntries(srcEntries, callback) {
if(!dst.storageTag) {
dst.storageTag = dst.areaInfo.storageTags[0];
}
const destDir = FileEntry.getAreaStorageDirectoryByTag(dst.storageTag);
async.eachSeries(srcEntries, (entry, nextEntry) => {
const srcPath = entry.filePath;
const dstPath = paths.join(destDir, entry.fileName);
process.stdout.write(`Moving ${srcPath} => ${dstPath}... `);
FileEntry.moveEntry(entry, dst.areaTag, dst.storageTag, err => {
if(err) {
console.info(`Failed: ${err.message}`);
} else {
console.info('Done');
}
return nextEntry(null); // always try next
});
},
err => {
return callback(err);
});
}
]
);
}
function handleFileBaseCommand() {
if(true === argv.help) {
return printUsageAndSetExitCode(getHelpFor('FileBase'), ExitCodes.ERROR);
}
const action = argv._[1];
switch(action) {
case 'info' : return displayFileAreaInfo();
case 'scan' : return scanFileAreas();
case 'move' : return moveFiles();
default : return printUsageAndSetExitCode(getHelpFor('FileBase'), ExitCodes.ERROR);
}
}