diff --git a/core/file_entry.js b/core/file_entry.js index 3a5b3df2..f5286dd3 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -11,6 +11,7 @@ const async = require('async'); const _ = require('lodash'); const paths = require('path'); const fse = require('fs-extra'); +const { unlink } = require('graceful-fs'); const FILE_TABLE_MEMBERS = [ 'file_id', 'area_tag', 'file_sha256', 'file_name', 'storage_tag', @@ -541,6 +542,40 @@ module.exports = class FileEntry { }); } + static removeEntry(srcFileEntry, options, cb) { + if(!_.isFunction(cb) && _.isFunction(options)) { + cb = options; + options = {}; + } + + async.series( + [ + function removeFromDatabase(callback) { + fileDb.run( + `DELETE FROM file + WHERE file_id = ?;`, + [ srcFileEntry.fileId ], + err => { + return callback(err); + } + ); + }, + function optionallyRemovePhysicalFile(callback) { + if(true !== options.removePhysFile) { + return callback(null); + } + + unlink(srcFileEntry.filePath, err => { + return callback(err); + }); + } + ], + err => { + return cb(err); + } + ); + } + static moveEntry(srcFileEntry, destAreaTag, destStorageTag, destFileName, cb) { if(!cb && _.isFunction(destFileName)) { cb = destFileName; @@ -550,7 +585,6 @@ module.exports = class FileEntry { const srcPath = srcFileEntry.filePath; const dstDir = FileEntry.getAreaStorageDirectoryByTag(destStorageTag); - if(!dstDir) { return cb(Errors.Invalid('Invalid storage tag')); } diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index 3f791267..d9ad9366 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -438,6 +438,62 @@ function scanFileAreas() { ); } +function expandFileTargets(targets, cb) { + let entries = []; + + // Each entry may be PATH|FILE_ID|SHA|AREA_TAG[@STORAGE_TAG] + const FileEntry = require('../../core/file_entry.js'); + + async.eachSeries(targets, (areaAndStorage, next) => { + const areaInfo = fileArea.getFileAreaByTag(areaAndStorage.areaTag); + + if(areaInfo) { + // AREA_TAG[@STORAGE_TAG] - all files in area@tag + 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) { + entries.push(fileEntry); + } + return nextFileId(err); + }); + }, + err => { + return next(err); + }); + }); + + } else { + // FILENAME_WC|FILE_ID|SHA|PARTIAL_SHA + // :TODO: FULL_PATH -> entries + getFileEntries(areaAndStorage.pattern, (err, fileEntries) => { + if(err) { + return next(err); + } + + entries = entries.concat(fileEntries); + return next(null); + }); + } + }, + err => { + return cb(err, entries); + }); +} + function moveFiles() { // // oputil fb move SRC [SRC2 ...] DST @@ -450,8 +506,9 @@ function moveFiles() { } const moveArgs = argv._.slice(2); - let src = getAreaAndStorage(moveArgs.slice(0, -1)); - let dst = getAreaAndStorage(moveArgs.slice(-1))[0]; + const src = getAreaAndStorage(moveArgs.slice(0, -1)); + const dst = getAreaAndStorage(moveArgs.slice(-1))[0]; + let FileEntry; async.waterfall( @@ -465,8 +522,6 @@ function moveFiles() { }); }, function validateAndExpandSourceAndDest(callback) { - let srcEntries = []; - const areaInfo = fileArea.getFileAreaByTag(dst.areaTag); if(areaInfo) { dst.areaInfo = areaInfo; @@ -474,57 +529,9 @@ function moveFiles() { 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) => { - const areaInfo = fileArea.getFileAreaByTag(areaAndStorage.areaTag); - - if(areaInfo) { - // AREA_TAG[@STORAGE_TAG] - all files in area@tag - 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 { - // FILENAME_WC|FILE_ID|SHA|PARTIAL_SHA - // :TODO: FULL_PATH -> entries - getFileEntries(areaAndStorage.pattern, (err, entries) => { - if(err) { - return next(err); - } - - srcEntries = srcEntries.concat(entries); - return next(null); - }); - } - }, - err => { + expandFileTargets(src, (err, srcEntries) => { return callback(err, srcEntries); }); }, @@ -555,13 +562,80 @@ function moveFiles() { return callback(err); }); } - ] + ], + err => { + if(err) { + process.exitCode = ExitCodes.ERROR; + console.error(err.message); + } + } ); } function removeFiles() { // - // REMOVE FILENAME_WC|SHA|FILE_ID [SHA|FILE_ID ...] + // oputil fb rm|remove|del|delete SRC [SRC2 ...] + // + // SRC: FILENAME_WC|FILE_ID|SHA|AREA_TAG[@STORAGE_TAG] + // + // AREA_TAG[@STORAGE_TAG] remove all entries matching + // supplied area/storage tags + // + // --phys-file removes backing physical file(s) + // + if(argv._.length < 3) { + return printUsageAndSetExitCode(getHelpFor('FileBase'), ExitCodes.ERROR); + } + + const removePhysFile = argv['phys-file']; + + const src = getAreaAndStorage(argv._.slice(2)); + + async.waterfall( + [ + function init(callback) { + return initConfigAndDatabases( err => { + if(!err) { + fileArea = require('../../core/file_base_area.js'); + } + return callback(err); + }); + }, + function expandSources(callback) { + expandFileTargets(src, (err, srcEntries) => { + return callback(err, srcEntries); + }); + }, + function removeEntries(srcEntries, callback) { + const FileEntry = require('../../core/file_entry.js'); + + const extraOutput = removePhysFile ? ' (including physical file)' : ''; + + async.eachSeries(srcEntries, (entry, nextEntry) => { + + process.stdout.write(`Removing ${entry.filePath}${extraOutput}... `); + + FileEntry.removeEntry(entry, { removePhysFile }, err => { + if(err) { + console.info(`Failed: ${err.message}`); + } else { + console.info('Done'); + } + + return nextEntry(err); + }); + }, err => { + return callback(err); + }); + } + ], + err => { + if(err) { + process.exitCode = ExitCodes.ERROR; + console.error(err.message); + } + } + ); } function handleFileBaseCommand() { @@ -582,7 +656,13 @@ function handleFileBaseCommand() { return ({ info : displayFileAreaInfo, scan : scanFileAreas, + + mv : moveFiles, move : moveFiles, + + rm : removeFiles, remove : removeFiles, + del : removeFiles, + delete : removeFiles, }[action] || errUsage)(); } \ No newline at end of file diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index c49200b0..1a2b42bf 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -45,7 +45,7 @@ import-areas args: --type TYPE specifies area import type. valid options are "bbs" and "na" `, FileBase : -`usage: oputil.js fb [] [] +`usage: oputil.js fb [] actions: scan AREA_TAG[@STORAGE_TAG] scan specified area @@ -53,14 +53,16 @@ actions: info AREA_TAG|SHA|FILE_ID display information about areas and/or files SHA may be a full or partial SHA-256 - move SRC [SRC...]] DST move entry(s) from SRC to DST - * SRC: FILENAME_WC|SHA|FILE_ID|AREA_TAG[@STORAGE_TAG] - * DST: AREA_TAG[@STORAGE_TAG] + mv SRC [SRC...] DST move entry(s) from SRC to DST + SRC: FILENAME_WC|SHA|FILE_ID|AREA_TAG[@STORAGE_TAG] + DST: AREA_TAG[@STORAGE_TAG] - remove SHA|FILE_ID removes a entry from the system + rm SRC [SRC...] remove entry(s) from the system matching SRC + SRC: FILENAME_WC|SHA|FILE_ID|AREA_TAG[@STORAGE_TAG] scan args: --tags TAG1,TAG2,... specify tag(s) to assign to discovered entries + --desc-file [PATH] prefer file descriptions from DESCRIPT.ION file over other sources such as FILE_ID.DIZ. if PATH is specified, use DESCRIPT.ION at PATH instead @@ -71,7 +73,7 @@ info args: --show-desc display short description, if any remove args: - --delete also remove underlying physical file + --phys-file also remove underlying physical file `, FileOpsInfo : `