* WIP on upload scan/processing
* WIP on user add/edit data to uploads * Add write access (upload) to area ACS * Add upload collision handling * Add upload stats
This commit is contained in:
parent
4c1c05e4da
commit
e265e3cc97
|
@ -27,4 +27,5 @@ exports.Errors = {
|
|||
DoesNotExist : (reason, reasonCode) => new EnigError('Object does not exist', -33002, reason, reasonCode),
|
||||
AccessDenied : (reason, reasonCode) => new EnigError('Access denied', -32003, reason, reasonCode),
|
||||
Invalid : (reason, reasonCode) => new EnigError('Invalid', -32004, reason, reasonCode),
|
||||
ExternalProcess : (reason, reasonCode) => new EnigError('External process error', -32005, reason, reasonCode),
|
||||
};
|
||||
|
|
|
@ -27,7 +27,7 @@ exports.getDefaultFileAreaTag = getDefaultFileAreaTag;
|
|||
exports.getFileAreaByTag = getFileAreaByTag;
|
||||
exports.getFileEntryPath = getFileEntryPath;
|
||||
exports.changeFileAreaWithOptions = changeFileAreaWithOptions;
|
||||
//exports.addOrUpdateFileEntry = addOrUpdateFileEntry;
|
||||
exports.scanFile = scanFile;
|
||||
exports.scanFileAreaForChanges = scanFileAreaForChanges;
|
||||
|
||||
const WellKnownAreaTags = exports.WellKnownAreaTags = {
|
||||
|
@ -43,16 +43,18 @@ function getAvailableFileAreas(client, options) {
|
|||
options = options || { };
|
||||
|
||||
// perform ACS check per conf & omit internal if desired
|
||||
return _.omit(Config.fileBase.areas, (area, areaTag) => {
|
||||
if(!options.includeSystemInternal && isInternalArea(areaTag)) {
|
||||
const allAreas = _.map(Config.fileBase.areas, (areaInfo, areaTag) => Object.assign(areaInfo, { areaTag : areaTag } ));
|
||||
|
||||
return _.omit(allAreas, areaInfo => {
|
||||
if(!options.includeSystemInternal && isInternalArea(areaInfo.areaTag)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(options.writeAcs && !client.acs.hasFileAreaWrite(area)) {
|
||||
if(options.writeAcs && !client.acs.hasFileAreaWrite(areaInfo)) {
|
||||
return true; // omit
|
||||
}
|
||||
|
||||
return !client.acs.hasFileAreaRead(area);
|
||||
return !client.acs.hasFileAreaRead(areaInfo);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -326,42 +328,16 @@ function populateFileEntryWithArchive(fileEntry, filePath, archiveType, cb) {
|
|||
);
|
||||
}
|
||||
|
||||
function populateFileEntry(fileEntry, filePath, archiveType, cb) {
|
||||
function populateFileEntryNonArchive(fileEntry, filePath, archiveType, cb) {
|
||||
// :TODO: implement me!
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
function addNewFileEntry(fileEntry, filePath, cb) {
|
||||
const archiveUtil = ArchiveUtil.getInstance();
|
||||
|
||||
// :TODO: Use detectTypeWithBuf() once avail - we *just* read some file data
|
||||
|
||||
async.series(
|
||||
[
|
||||
function populateInfo(callback) {
|
||||
archiveUtil.detectType(filePath, (err, archiveType) => {
|
||||
if(archiveType) {
|
||||
// save this off
|
||||
fileEntry.meta.archive_type = archiveType;
|
||||
|
||||
populateFileEntryWithArchive(fileEntry, filePath, archiveType, err => {
|
||||
if(err) {
|
||||
populateFileEntry(fileEntry, filePath, err => {
|
||||
// :TODO: log err
|
||||
return callback(null); // ignore err
|
||||
});
|
||||
} else {
|
||||
return callback(null);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
populateFileEntry(fileEntry, filePath, err => {
|
||||
// :TODO: log err
|
||||
return callback(null); // ignore err
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
function addNewDbRecord(callback) {
|
||||
return fileEntry.persist(callback);
|
||||
}
|
||||
|
@ -376,6 +352,102 @@ function updateFileEntry(fileEntry, filePath, cb) {
|
|||
|
||||
}
|
||||
|
||||
function scanFile(filePath, options, cb) {
|
||||
|
||||
if(_.isFunction(options) && !cb) {
|
||||
cb = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
const fileEntry = new FileEntry({
|
||||
areaTag : options.areaTag,
|
||||
meta : options.meta,
|
||||
hashTags : options.hashTags, // Set() or Array
|
||||
fileName : paths.basename(filePath),
|
||||
storageTag : options.storageTag,
|
||||
});
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function processPhysicalFileGeneric(callback) {
|
||||
let byteSize = 0;
|
||||
const sha1 = crypto.createHash('sha1');
|
||||
const sha256 = crypto.createHash('sha256');
|
||||
const md5 = crypto.createHash('md5');
|
||||
const crc32 = new CRC32();
|
||||
|
||||
const stream = fs.createReadStream(filePath);
|
||||
|
||||
stream.on('data', data => {
|
||||
byteSize += data.length;
|
||||
|
||||
sha1.update(data);
|
||||
sha256.update(data);
|
||||
md5.update(data);
|
||||
crc32.update(data);
|
||||
});
|
||||
|
||||
stream.on('end', () => {
|
||||
fileEntry.meta.byte_size = byteSize;
|
||||
|
||||
// sha-1 is in basic file entry
|
||||
fileEntry.fileSha1 = sha1.digest('hex');
|
||||
|
||||
// others are meta
|
||||
fileEntry.meta.file_sha256 = sha256.digest('hex');
|
||||
fileEntry.meta.file_md5 = md5.digest('hex');
|
||||
fileEntry.meta.file_crc32 = crc32.finalize().toString(16);
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
|
||||
stream.on('error', err => {
|
||||
return callback(err);
|
||||
});
|
||||
},
|
||||
function processPhysicalFileByType(callback) {
|
||||
const archiveUtil = ArchiveUtil.getInstance();
|
||||
|
||||
archiveUtil.detectType(filePath, (err, archiveType) => {
|
||||
if(archiveType) {
|
||||
// save this off
|
||||
fileEntry.meta.archive_type = archiveType;
|
||||
|
||||
populateFileEntryWithArchive(fileEntry, filePath, archiveType, err => {
|
||||
if(err) {
|
||||
populateFileEntryNonArchive(fileEntry, filePath, err => {
|
||||
// :TODO: log err
|
||||
return callback(null); // ignore err
|
||||
});
|
||||
} else {
|
||||
return callback(null);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
populateFileEntryNonArchive(fileEntry, filePath, err => {
|
||||
// :TODO: log err
|
||||
return callback(null); // ignore err
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
function fetchExistingEntry(callback) {
|
||||
getExistingFileEntriesBySha1(fileEntry.fileSha1, (err, existingEntries) => {
|
||||
return callback(err, existingEntries);
|
||||
});
|
||||
}
|
||||
],
|
||||
(err, existingEntries) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
return cb(null, fileEntry, existingEntries);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
function addOrUpdateFileEntry(areaInfo, storageLocation, fileName, options, cb) {
|
||||
|
||||
const fileEntry = new FileEntry({
|
||||
|
@ -444,6 +516,7 @@ function addOrUpdateFileEntry(areaInfo, storageLocation, fileName, options, cb)
|
|||
}
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
||||
function scanFileAreaForChanges(areaInfo, cb) {
|
||||
const storageLocations = getAreaStorageLocations(areaInfo);
|
||||
|
@ -472,9 +545,28 @@ function scanFileAreaForChanges(areaInfo, cb) {
|
|||
return nextFile(null);
|
||||
}
|
||||
|
||||
addOrUpdateFileEntry(areaInfo, storageLoc, fileName, { }, err => {
|
||||
return nextFile(err);
|
||||
});
|
||||
scanFile(
|
||||
fullPath,
|
||||
{
|
||||
areaTag : areaInfo.areaTag,
|
||||
storageTag : storageLoc.storageTag
|
||||
},
|
||||
(err, fileEntry, existingEntries) => {
|
||||
if(err) {
|
||||
// :TODO: Log me!!!
|
||||
return nextFile(null); // try next anyway
|
||||
}
|
||||
|
||||
if(existingEntries.length > 0) {
|
||||
// :TODO: Handle duplidates -- what to do here???
|
||||
} else {
|
||||
addNewFileEntry(fileEntry, fullPath, err => {
|
||||
// pass along error; we failed to insert a record in our DB or something else bad
|
||||
return nextFile(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}, err => {
|
||||
return callback(err);
|
||||
|
@ -495,49 +587,3 @@ function scanFileAreaForChanges(areaInfo, cb) {
|
|||
return cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
function scanFileAreaForChanges2(areaInfo, cb) {
|
||||
const areaPhysDir = getAreaStorageDirectory(areaInfo);
|
||||
|
||||
async.series(
|
||||
[
|
||||
function scanPhysFiles(callback) {
|
||||
fs.readdir(areaPhysDir, (err, files) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
async.eachSeries(files, (fileName, next) => {
|
||||
const fullPath = paths.join(areaPhysDir, fileName);
|
||||
|
||||
fs.stat(fullPath, (err, stats) => {
|
||||
if(err) {
|
||||
// :TODO: Log me!
|
||||
return next(null); // always try next file
|
||||
}
|
||||
|
||||
if(!stats.isFile()) {
|
||||
return next(null);
|
||||
}
|
||||
|
||||
addOrUpdateFileEntry(areaInfo, fileName, { areaTag : areaInfo.areaTag }, err => {
|
||||
return next(err);
|
||||
});
|
||||
});
|
||||
}, err => {
|
||||
return callback(err);
|
||||
});
|
||||
});
|
||||
},
|
||||
function scanDbEntries(callback) {
|
||||
// :TODO: Look @ db entries for area that were *not* processed above
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
*/
|
|
@ -213,6 +213,16 @@ module.exports = class FileEntry {
|
|||
);
|
||||
}
|
||||
|
||||
setHashTags(hashTags) {
|
||||
if(_.isString(hashTags)) {
|
||||
this.hashTags = new Set(hashTags.split(/[\s,]+/));
|
||||
} else if(Array.isArray(hashTags)) {
|
||||
this.hashTags = new Set(hashTags);
|
||||
} else if(hashTags instanceof Set) {
|
||||
this.hashTags = hashTags;
|
||||
}
|
||||
}
|
||||
|
||||
static getWellKnownMetaValues() { return Object.keys(FILE_WELL_KNOWN_META); }
|
||||
|
||||
static findFiles(filter, cb) {
|
||||
|
|
|
@ -148,8 +148,7 @@ function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) {
|
|||
//
|
||||
if(_.has(formForId, 'mci') || _.has(formForId, 'submit')) {
|
||||
Log.trace('Using generic configuration');
|
||||
cb(null, formForId);
|
||||
return;
|
||||
return cb(null, formForId);
|
||||
}
|
||||
|
||||
cb(new Error('No matching form configuration found for key \'' + mciReqKey + '\''));
|
||||
|
|
|
@ -409,10 +409,10 @@ function MultiLineEditTextView(options) {
|
|||
|
||||
this.insertCharactersInText = function(c, index, col) {
|
||||
self.textLines[index].text = [
|
||||
self.textLines[index].text.slice(0, col),
|
||||
c,
|
||||
self.textLines[index].text.slice(col)
|
||||
].join('');
|
||||
self.textLines[index].text.slice(0, col),
|
||||
c,
|
||||
self.textLines[index].text.slice(col)
|
||||
].join('');
|
||||
|
||||
//self.cursorPos.col++;
|
||||
self.cursorPos.col += c.length;
|
||||
|
|
|
@ -444,7 +444,7 @@ function createCleanAnsi(input, options, cb) {
|
|||
//while(col <= canvas[row][0].width) {
|
||||
while(col < options.width) {
|
||||
if(!canvas[row][col].char) {
|
||||
canvas[row][col].char = 'P';
|
||||
canvas[row][col].char = ' ';
|
||||
if(!canvas[row][col].sgr) {
|
||||
// :TODO: fix duplicate SGR's in a row here - we just need one per sequence
|
||||
canvas[row][col].sgr = ANSI.reset();
|
||||
|
@ -459,12 +459,12 @@ function createCleanAnsi(input, options, cb) {
|
|||
if(col <= options.width) {
|
||||
canvas[row][col] = canvas[row][col] || {};
|
||||
|
||||
//canvas[row][col].char = '\r\n';
|
||||
canvas[row][col].char = '\r\n';
|
||||
canvas[row][col].sgr = ANSI.reset();
|
||||
|
||||
// :TODO: don't splice, just reset + fill with ' ' till end
|
||||
for(let fillCol = col; fillCol <= options.width; ++fillCol) {
|
||||
canvas[row][fillCol].char = 'X';
|
||||
canvas[row][fillCol].char = ' ';
|
||||
}
|
||||
|
||||
//canvas[row] = canvas[row].splice(0, col + 1);
|
||||
|
|
|
@ -100,18 +100,15 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||
this.sentFileIds = [];
|
||||
}
|
||||
|
||||
get isSending() {
|
||||
return 'send' === this.direction;
|
||||
isSending() {
|
||||
return ('send' === this.direction);
|
||||
}
|
||||
|
||||
restorePipeAfterExternalProc(pipe) {
|
||||
restorePipeAfterExternalProc() {
|
||||
if(!this.pipeRestored) {
|
||||
this.pipeRestored = true;
|
||||
|
||||
this.client.restoreDataHandler();
|
||||
|
||||
//this.client.term.output.unpipe(pipe);
|
||||
//this.client.term.output.resume();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -154,16 +151,62 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||
}
|
||||
}
|
||||
|
||||
moveFileWithCollisionHandling(src, dst, cb) {
|
||||
//
|
||||
// Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc.
|
||||
// in the case of collisions.
|
||||
//
|
||||
const dstPath = paths.dirname(dst);
|
||||
const dstFileExt = paths.extname(dst);
|
||||
const dstFileSuffix = paths.basename(dst, dstFileExt);
|
||||
|
||||
let renameIndex = 0;
|
||||
let movedOk = false;
|
||||
let tryDstPath;
|
||||
|
||||
async.until(
|
||||
() => movedOk, // until moved OK
|
||||
(cb) => {
|
||||
if(0 === renameIndex) {
|
||||
// try originally supplied path first
|
||||
tryDstPath = dst;
|
||||
} else {
|
||||
tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`);
|
||||
}
|
||||
|
||||
fse.move(src, tryDstPath, err => {
|
||||
if(err) {
|
||||
if('EEXIST' === err.code) {
|
||||
renameIndex += 1;
|
||||
return cb(null); // keep trying
|
||||
}
|
||||
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
movedOk = true;
|
||||
return cb(null, tryDstPath);
|
||||
});
|
||||
},
|
||||
(err, finalPath) => {
|
||||
return cb(err, finalPath);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
recvFiles(cb) {
|
||||
this.executeExternalProtocolHandlerForRecv( (err, tempWorkingDir) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
this.receivedFiles = [];
|
||||
this.recvFilePaths = [];
|
||||
|
||||
if(this.recvFileName) {
|
||||
// file name specified - we expect a single file in |tempWorkingDir|
|
||||
|
||||
// :TODO: support non-blind: Move file to dest path, add to recvFilePaths, etc.
|
||||
|
||||
return cb(null);
|
||||
} else {
|
||||
//
|
||||
|
@ -176,19 +219,19 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||
}
|
||||
|
||||
async.each(files, (file, nextFile) => {
|
||||
fse.move(
|
||||
this.moveFileWithCollisionHandling(
|
||||
paths.join(tempWorkingDir, file),
|
||||
paths.join(this.recvDirectory, file),
|
||||
err => {
|
||||
(err, destPath) => {
|
||||
if(err) {
|
||||
// :TODO: IMPORTANT: Handle collisions - rename to FILE(1).EXT, etc.
|
||||
this.client.log.warn(
|
||||
{ tempWorkingDir : tempWorkingDir, recvDirectory : this.recvDirectory, file : file, error : err.message },
|
||||
'Failed to move upload file to destination directory'
|
||||
);
|
||||
} else {
|
||||
this.receivedFiles.push(file);
|
||||
this.recvFilePaths.push(destPath);
|
||||
}
|
||||
|
||||
return nextFile(null); // don't pass along err; try next
|
||||
}
|
||||
);
|
||||
|
@ -324,16 +367,16 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||
});
|
||||
|
||||
externalProc.once('close', () => {
|
||||
return this.restorePipeAfterExternalProc(externalProc);
|
||||
return this.restorePipeAfterExternalProc();
|
||||
});
|
||||
|
||||
externalProc.once('exit', (exitCode) => {
|
||||
this.client.log.debug( { cmd : cmd, args : args, exitCode : exitCode }, 'Process exited' );
|
||||
|
||||
this.restorePipeAfterExternalProc(externalProc);
|
||||
this.restorePipeAfterExternalProc();
|
||||
externalProc.removeAllListeners();
|
||||
|
||||
return cb(null);
|
||||
return cb(exitCode ? Errors.ExternalProcess(`Process exited with exit code ${exitCode}`, 'EBADEXIT') : null);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -366,7 +409,11 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||
}
|
||||
|
||||
getMenuResult() {
|
||||
return { sentFileIds : this.sentFileIds };
|
||||
if(this.isSending()) {
|
||||
return { sentFileIds : this.sentFileIds };
|
||||
} else {
|
||||
return { recvFilePaths : this.recvFilePaths };
|
||||
}
|
||||
}
|
||||
|
||||
updateSendStats(cb) {
|
||||
|
@ -383,9 +430,8 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||
fileIds.push(queueItem.fileId);
|
||||
}
|
||||
|
||||
downloadCount += 1;
|
||||
|
||||
if(_.isNumber(queueItem.byteSize)) {
|
||||
downloadCount += 1;
|
||||
downloadBytes += queueItem.byteSize;
|
||||
return next(null);
|
||||
}
|
||||
|
@ -395,6 +441,7 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||
if(err) {
|
||||
this.client.log.warn( { error : err.message, path : queueItem.path }, 'File stat failed' );
|
||||
} else {
|
||||
downloadCount += 1;
|
||||
downloadBytes += stats.size;
|
||||
}
|
||||
|
||||
|
@ -416,8 +463,30 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||
}
|
||||
|
||||
updateRecvStats(cb) {
|
||||
// :TODO: update user & system upload stats
|
||||
return cb(null);
|
||||
let uploadBytes = 0;
|
||||
let uploadCount = 0;
|
||||
|
||||
async.each(this.recvFilePaths, (filePath, next) => {
|
||||
// we just have a path - figure it out
|
||||
fs.stat(filePath, (err, stats) => {
|
||||
if(err) {
|
||||
this.client.log.warn( { error : err.message, path : filePath }, 'File stat failed' );
|
||||
} else {
|
||||
uploadCount += 1;
|
||||
uploadBytes += stats.size;
|
||||
}
|
||||
|
||||
return next(null);
|
||||
});
|
||||
}, () => {
|
||||
StatLog.incrementUserStat(this.client.user, 'ul_total_count', uploadCount);
|
||||
StatLog.incrementUserStat(this.client.user, 'ul_total_bytes', uploadBytes);
|
||||
StatLog.incrementSystemStat('ul_total_count', uploadCount);
|
||||
StatLog.incrementSystemStat('ul_total_bytes', uploadBytes);
|
||||
|
||||
|
||||
return cb(null);
|
||||
});
|
||||
}
|
||||
|
||||
initSequence() {
|
||||
|
@ -428,7 +497,7 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||
async.series(
|
||||
[
|
||||
function validateConfig(callback) {
|
||||
if(self.isSending) {
|
||||
if(self.isSending()) {
|
||||
if(!Array.isArray(self.sendQueue)) {
|
||||
self.sendQueue = [ self.sendQueue ];
|
||||
}
|
||||
|
@ -437,7 +506,7 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||
return callback(null);
|
||||
},
|
||||
function transferFiles(callback) {
|
||||
if(self.isSending) {
|
||||
if(self.isSending()) {
|
||||
self.sendFiles( err => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
|
@ -475,7 +544,7 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||
});
|
||||
},
|
||||
function updateUserAndSystemStats(callback) {
|
||||
if(self.isSending) {
|
||||
if(self.isSending()) {
|
||||
return self.updateSendStats(callback);
|
||||
} else {
|
||||
return self.updateRecvStats(callback);
|
||||
|
@ -488,9 +557,10 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||
}
|
||||
|
||||
// Wait for a key press - attempt to avoid issues with some terminals after xfer
|
||||
self.client.term.write('|00\nTransfer(s) complete. Press a key\n');
|
||||
// :TODO: display ANSI if it exists else prompt -- look @ Obv/2 for filename
|
||||
self.client.term.pipeWrite('|00|07\nTransfer(s) complete. Press a key\n');
|
||||
self.client.waitForKeyPress( () => {
|
||||
self.prevMenu();
|
||||
return self.prevMenu();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -333,6 +333,7 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||
if(_.isString(self.currentFileEntry.desc)) {
|
||||
const descView = self.viewControllers.browse.getView(MciViewIds.browse.desc);
|
||||
if(descView) {
|
||||
/* :TODO: finish createCleanAnsi() and use here!!!
|
||||
createCleanAnsi(
|
||||
self.currentFileEntry.desc,
|
||||
{ height : self.client.termHeight, width : descView.dimens.width },
|
||||
|
@ -345,6 +346,8 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||
return callback(null);
|
||||
}
|
||||
);
|
||||
*/
|
||||
|
||||
descView.setText( self.currentFileEntry.desc );
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -44,6 +44,10 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
|
|||
this.sentFileIds = options.lastMenuResult.sentFileIds;
|
||||
}
|
||||
|
||||
if(_.has(options, 'lastMenuResult.recvFilePaths')) {
|
||||
this.recvFilePaths = options.lastMenuResult.recvFilePaths;
|
||||
}
|
||||
|
||||
this.fallbackOnly = options.lastMenuResult ? true : false;
|
||||
|
||||
this.menuMethods = {
|
||||
|
@ -69,11 +73,15 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
|
|||
if(this.sentFileIds) {
|
||||
return { sentFileIds : this.sentFileIds };
|
||||
}
|
||||
|
||||
if(this.recvFilePaths) {
|
||||
return { recvFilePaths : this.recvFilePaths };
|
||||
}
|
||||
}
|
||||
|
||||
initSequence() {
|
||||
if(this.sentFileIds) {
|
||||
// nothing to do here; move along
|
||||
if(this.sentFileIds || this.recvFilePaths) {
|
||||
// nothing to do here; move along (we're just falling through)
|
||||
this.prevMenu();
|
||||
} else {
|
||||
super.initSequence();
|
||||
|
|
239
mods/upload.js
239
mods/upload.js
|
@ -10,10 +10,13 @@ const Errors = require('../core/enig_error.js').Errors;
|
|||
const stringFormat = require('../core/string_format.js');
|
||||
const getSortedAvailableFileAreas = require('../core/file_area.js').getSortedAvailableFileAreas;
|
||||
const getAreaDefaultStorageDirectory = require('../core/file_area.js').getAreaDefaultStorageDirectory;
|
||||
const scanFile = require('../core/file_area.js').scanFile;
|
||||
const getAreaStorageDirectoryByTag = require('../core/file_area.js').getAreaStorageDirectoryByTag;
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const paths = require('path');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Upload',
|
||||
|
@ -23,7 +26,8 @@ exports.moduleInfo = {
|
|||
|
||||
const FormIds = {
|
||||
options : 0,
|
||||
fileDetails : 1,
|
||||
processing : 1,
|
||||
fileDetails : 2,
|
||||
|
||||
};
|
||||
|
||||
|
@ -35,10 +39,16 @@ const MciViewIds = {
|
|||
navMenu : 4, // next/cancel/etc.
|
||||
},
|
||||
|
||||
processing : {
|
||||
// 10+ = customs
|
||||
},
|
||||
|
||||
fileDetails : {
|
||||
tags : 1, // tag(s) for item
|
||||
desc : 2, // defaults to 'desc' (e.g. from FILE_ID.DIZ)
|
||||
accept : 3, // accept fields & continue
|
||||
desc : 1, // defaults to 'desc' (e.g. from FILE_ID.DIZ)
|
||||
tags : 2, // tag(s) for item
|
||||
estYear : 3,
|
||||
accept : 4, // accept fields & continue
|
||||
// 10+ = customs
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -47,14 +57,15 @@ exports.getModule = class UploadModule extends MenuModule {
|
|||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
if(_.has(options, 'lastMenuResult.recvFilePaths')) {
|
||||
this.recvFilePaths = options.lastMenuResult.recvFilePaths;
|
||||
}
|
||||
|
||||
this.availAreas = getSortedAvailableFileAreas(this.client, { writeAcs : true } );
|
||||
|
||||
this.menuMethods = {
|
||||
navContinue : (formData, extraArgs, cb) => {
|
||||
optionsNavContinue : (formData, extraArgs, cb) => {
|
||||
if(this.isBlindUpload()) {
|
||||
// jump to fileDetails form
|
||||
// :TODO: support blind
|
||||
} else {
|
||||
// jump to protocol selection
|
||||
const areaUploadDir = this.getSelectedAreaUploadDirectory();
|
||||
|
||||
|
@ -66,19 +77,53 @@ exports.getModule = class UploadModule extends MenuModule {
|
|||
};
|
||||
|
||||
return this.gotoMenu(this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', modOpts, cb);
|
||||
} else {
|
||||
// jump to fileDetails form
|
||||
// :TODO: support non-blind: collect info/filename -> upload -> complete
|
||||
}
|
||||
},
|
||||
|
||||
fileDetailsContinue : (formData, extraArgs, cb) => {
|
||||
|
||||
|
||||
// see notes in displayFileDetailsPageForEntry() about this hackery:
|
||||
cb(null);
|
||||
return this.fileDetailsCurrentEntrySubmitCallback(null, formData.value); // move on to the next entry, if any
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getSelectedAreaUploadDirectory() {
|
||||
const areaSelectView = this.viewControllers.options.getView(MciViewIds.options.area);
|
||||
const selectedArea = this.availAreas[areaSelectView.getData()];
|
||||
getSaveState() {
|
||||
const saveState = {
|
||||
uploadType : this.uploadType,
|
||||
|
||||
return getAreaDefaultStorageDirectory(selectedArea);
|
||||
};
|
||||
|
||||
if(this.isBlindUpload()) {
|
||||
saveState.areaInfo = this.getSelectedAreaInfo();
|
||||
}
|
||||
|
||||
return saveState;
|
||||
}
|
||||
|
||||
restoreSavedState(savedState) {
|
||||
if(savedState.areaInfo) {
|
||||
this.areaInfo = savedState.areaInfo;
|
||||
}
|
||||
}
|
||||
|
||||
getSelectedAreaInfo() {
|
||||
const areaSelectView = this.viewControllers.options.getView(MciViewIds.options.area);
|
||||
return this.availAreas[areaSelectView.getData()];
|
||||
}
|
||||
|
||||
getSelectedAreaUploadDirectory() {
|
||||
const areaInfo = this.getSelectedAreaInfo();
|
||||
return getAreaDefaultStorageDirectory(areaInfo);
|
||||
}
|
||||
|
||||
isBlindUpload() { return 'blind' === this.uploadType; }
|
||||
isFileTransferComplete() { return !_.isUndefined(this.recvFilePaths); }
|
||||
|
||||
initSequence() {
|
||||
const self = this;
|
||||
|
@ -89,7 +134,11 @@ exports.getModule = class UploadModule extends MenuModule {
|
|||
return self.beforeArt(callback);
|
||||
},
|
||||
function display(callback) {
|
||||
return self.displayOptionsPage(false, callback);
|
||||
if(self.isFileTransferComplete()) {
|
||||
return self.displayProcessingPage(callback);
|
||||
} else {
|
||||
return self.displayOptionsPage(callback);
|
||||
}
|
||||
}
|
||||
],
|
||||
() => {
|
||||
|
@ -98,6 +147,110 @@ exports.getModule = class UploadModule extends MenuModule {
|
|||
);
|
||||
}
|
||||
|
||||
finishedLoading() {
|
||||
if(this.isFileTransferComplete()) {
|
||||
return this.processUploadedFiles();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
scanFiles(cb) {
|
||||
const self = this;
|
||||
|
||||
const results = {
|
||||
newEntries : [],
|
||||
dupes : [],
|
||||
};
|
||||
|
||||
async.eachSeries(this.recvFilePaths, (filePath, nextFilePath) => {
|
||||
// :TODO: virus scanning/etc. should occur around here
|
||||
|
||||
// :TODO: update scanning status art or display line "scanning {fileName}..." type of thing
|
||||
|
||||
self.client.term.pipeWrite(`|00|07\nScanning ${paths.basename(filePath)}...`);
|
||||
|
||||
scanFile(
|
||||
filePath,
|
||||
{
|
||||
areaTag : self.areaInfo.areaTag,
|
||||
storageTag : self.areaInfo.storageTags[0],
|
||||
},
|
||||
(err, fileEntry, existingEntries) => {
|
||||
if(err) {
|
||||
return nextFilePath(err);
|
||||
}
|
||||
|
||||
self.client.term.pipeWrite(' done\n');
|
||||
|
||||
// new or dupe?
|
||||
if(existingEntries.length > 0) {
|
||||
// 1:n dupes found
|
||||
results.dupes = results.dupes.concat(existingEntries);
|
||||
} else {
|
||||
// new one
|
||||
results.newEntries.push(fileEntry);
|
||||
}
|
||||
|
||||
return nextFilePath(null);
|
||||
}
|
||||
);
|
||||
}, err => {
|
||||
return cb(err, results);
|
||||
});
|
||||
}
|
||||
|
||||
processUploadedFiles() {
|
||||
//
|
||||
// For each file uploaded, we need to process & gather information
|
||||
//
|
||||
const self = this;
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function scan(callback) {
|
||||
return self.scanFiles(callback);
|
||||
},
|
||||
function displayDupes(scanResults, callback) {
|
||||
if(0 === scanResults.dupes.length) {
|
||||
return callback(null, scanResults);
|
||||
}
|
||||
|
||||
// :TODO: display dupe info
|
||||
return callback(null, scanResults);
|
||||
},
|
||||
function prepDetails(scanResults, callback) {
|
||||
async.eachSeries(scanResults.newEntries, (newEntry, nextEntry) => {
|
||||
self.displayFileDetailsPageForEntry(newEntry, (err, newValues) => {
|
||||
if(!err) {
|
||||
// if the file entry did *not* have a desc, take the user desc
|
||||
if(!self.fileEntryHasDetectedDesc(newEntry)) {
|
||||
newEntry.desc = newValues.shortDesc.trim();
|
||||
}
|
||||
|
||||
if(newValues.estYear.length > 0) {
|
||||
newEntry.meta.est_release_year = newValues.estYear;
|
||||
}
|
||||
|
||||
if(newValues.tags.length > 0) {
|
||||
newEntry.setHashTags(newValues.tags);
|
||||
}
|
||||
}
|
||||
|
||||
return nextEntry(err);
|
||||
});
|
||||
}, err => {
|
||||
delete self.fileDetailsCurrentEntrySubmitCallback;
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
],
|
||||
err => {
|
||||
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
displayOptionsPage(cb) {
|
||||
const self = this;
|
||||
|
||||
|
@ -130,6 +283,7 @@ exports.getModule = class UploadModule extends MenuModule {
|
|||
}
|
||||
});
|
||||
|
||||
self.uploadType = 'blind';
|
||||
uploadTypeView.setFocusItemIndex(0); // default to blind
|
||||
fileNameView.setText(blindFileNameText);
|
||||
areaSelectView.redraw();
|
||||
|
@ -145,4 +299,59 @@ exports.getModule = class UploadModule extends MenuModule {
|
|||
);
|
||||
}
|
||||
|
||||
displayProcessingPage(cb) {
|
||||
// :TODO: If art is supplied, display & start processing + update status/etc.; if no art, we'll just write each status update on a new line
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
fileEntryHasDetectedDesc(fileEntry) {
|
||||
return (fileEntry.desc && fileEntry.desc.length > 0);
|
||||
}
|
||||
|
||||
displayFileDetailsPageForEntry(fileEntry, cb) {
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function prepArtAndViewController(callback) {
|
||||
return self.prepViewControllerWithArt(
|
||||
'fileDetails',
|
||||
FormIds.fileDetails,
|
||||
{ clearScreen : true, trailingLF : false },
|
||||
callback
|
||||
);
|
||||
},
|
||||
function populateViews(callback) {
|
||||
const descView = self.viewControllers.fileDetails.getView(MciViewIds.fileDetails.desc);
|
||||
|
||||
if(self.fileEntryHasDetectedDesc(fileEntry)) {
|
||||
descView.setText(fileEntry.desc);
|
||||
descView.setPropertyValue('mode', 'preview');
|
||||
|
||||
// :TODO: it would be nice to take this out of the focus order
|
||||
}
|
||||
|
||||
const tagsView = self.viewControllers.fileDetails.getView(MciViewIds.fileDetails.tags);
|
||||
tagsView.setText( Array.from(fileEntry.hashTags).join(',') ); // :TODO: optional 'hashTagsSep' like file list/browse
|
||||
|
||||
const yearView = self.viewControllers.fileDetails.getView(MciViewIds.fileDetails.estYear);
|
||||
yearView.setText(fileEntry.meta.est_release_year || '');
|
||||
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
//
|
||||
// we only call |cb| here if there is an error
|
||||
// else, wait for the current from to be submit - then call -
|
||||
// this way we'll move on to the next file entry when ready
|
||||
//
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
self.fileDetailsCurrentEntrySubmitCallback = cb; // stash for moduleMethods.fileDetailsContinue
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue