From e265e3cc970174c3903a3331a2ff2f6d4b36c7f6 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 11 Jan 2017 22:51:00 -0700 Subject: [PATCH] * 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 --- core/enig_error.js | 1 + core/file_area.js | 208 +++++++++++++--------- core/file_entry.js | 10 ++ core/menu_util.js | 3 +- core/multi_line_edit_text_view.js | 8 +- core/string_util.js | 6 +- core/transfer_file.js | 118 ++++++++++--- mods/file_area_list.js | 3 + mods/file_base_download_manager.js | 2 +- mods/file_transfer_protocol_select.js | 12 +- mods/upload.js | 241 ++++++++++++++++++++++++-- 11 files changed, 479 insertions(+), 133 deletions(-) diff --git a/core/enig_error.js b/core/enig_error.js index 69c6fb3c..98e3cb50 100644 --- a/core/enig_error.js +++ b/core/enig_error.js @@ -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), }; diff --git a/core/file_area.js b/core/file_area.js index e3aa2081..09072d55 100644 --- a/core/file_area.js +++ b/core/file_area.js @@ -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); - } - ); -} -*/ \ No newline at end of file diff --git a/core/file_entry.js b/core/file_entry.js index f816a62b..42712541 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -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) { diff --git a/core/menu_util.js b/core/menu_util.js index f62dbd6c..68751f33 100644 --- a/core/menu_util.js +++ b/core/menu_util.js @@ -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 + '\'')); diff --git a/core/multi_line_edit_text_view.js b/core/multi_line_edit_text_view.js index 4ef55590..05ac3586 100644 --- a/core/multi_line_edit_text_view.js +++ b/core/multi_line_edit_text_view.js @@ -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; diff --git a/core/string_util.js b/core/string_util.js index edb0e613..68778533 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -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); diff --git a/core/transfer_file.js b/core/transfer_file.js index bed1a613..1807f88d 100644 --- a/core/transfer_file.js +++ b/core/transfer_file.js @@ -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(); }); } ); diff --git a/mods/file_area_list.js b/mods/file_area_list.js index 58e72c35..33adc8ba 100644 --- a/mods/file_area_list.js +++ b/mods/file_area_list.js @@ -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 { diff --git a/mods/file_base_download_manager.js b/mods/file_base_download_manager.js index 017a3141..77b6609c 100644 --- a/mods/file_base_download_manager.js +++ b/mods/file_base_download_manager.js @@ -45,7 +45,7 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { if(_.has(options, 'lastMenuResult.sentFileIds')) { this.sentFileIds = options.lastMenuResult.sentFileIds; } - + this.fallbackOnly = options.lastMenuResult ? true : false; this.menuMethods = { diff --git a/mods/file_transfer_protocol_select.js b/mods/file_transfer_protocol_select.js index 42e78bb4..1d250d2e 100644 --- a/mods/file_transfer_protocol_select.js +++ b/mods/file_transfer_protocol_select.js @@ -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(); diff --git a/mods/upload.js b/mods/upload.js index 47fbd9b4..63c99961 100644 --- a/mods/upload.js +++ b/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,20 +77,54 @@ 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 } }; } + getSaveState() { + const saveState = { + uploadType : this.uploadType, + + }; + + 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 areaSelectView = this.viewControllers.options.getView(MciViewIds.options.area); - const selectedArea = this.availAreas[areaSelectView.getData()]; - - return getAreaDefaultStorageDirectory(selectedArea); + 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 + } + ); + } };