Mercurial > cgi-bin > hgweb.cgi > PassMan
view src/main/kotlin/name/blackcap/passman/ImportSubcommand.kt @ 12:a38a2a1036c3
Add import subcommand.
author | David Barts <n5jrn@me.com> |
---|---|
date | Sun, 22 Jan 2023 09:22:53 -0800 |
parents | |
children | 302d224bbd57 |
line wrap: on
line source
package name.blackcap.passman import com.opencsv.CSVParserBuilder import com.opencsv.CSVReaderBuilder import org.apache.commons.cli.* import java.io.FileReader import java.io.IOException import java.text.SimpleDateFormat import java.util.* import kotlin.system.exitProcess class ImportSubcommand(): Subcommand() { private companion object { val CSV_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").apply { timeZone = TimeZone.getTimeZone("UTC") isLenient = false } const val ESCAPE = "escape" const val FORCE = "force" const val HELP = "help" const val IGNORE = "ignore" const val QUOTE = "quote" const val SEPARATOR = "separator" const val SKIP = "skip" const val NFIELDS = 7 } private lateinit var commandLine: CommandLine private lateinit var db: Database /* default option values */ private var escape = '\\' private var quote = '"' private var separator = ',' override fun run(args: Array<String>) { parseArguments(args) db = Database.open() try { doImport() } catch (e: IOException) { die(e.message ?: "I/O error") } } private fun parseArguments(args: Array<String>) { val options = Options().apply { addOption("e", ImportSubcommand.ESCAPE, true, "CSV escape character (default $escape).") addOption("f", ImportSubcommand.FORCE, false, "Do not ask before overwriting.") addOption("h", ImportSubcommand.HELP, false, "Print this help message.") addOption("i", ImportSubcommand.IGNORE, false, "Ignore white space before quoted strings.") addOption("q", ImportSubcommand.QUOTE, true, "CSV string-quoting character (default $quote).") addOption("s", ImportSubcommand.SEPARATOR, true, "CSV separator character (default $separator).") addOption("k", ImportSubcommand.SKIP, false, "Skip first line of input.") } try { commandLine = DefaultParser().parse(options, args) } catch (e: ParseException) { die(e.message ?: "syntax error", 2) } if (commandLine.hasOption(ImportSubcommand.HELP)) { HelpFormatter().printHelp("$SHORTNAME merge [options] csv_file", options) exitProcess(0) } if (commandLine.args.isEmpty()) { die("expecting other CSV file name", 2) } if (commandLine.args.size > 1) { die("unexpected trailing arguments", 2) } escape = getOptionChar(ImportSubcommand.ESCAPE, escape) quote = getOptionChar(ImportSubcommand.QUOTE, quote) separator = getOptionChar(ImportSubcommand.SEPARATOR, separator) } private fun getOptionChar(optionName: String, defaultValue: Char): Char { val optionValue = commandLine.getOptionValue(optionName) ?: return defaultValue val ret = optionValue.firstOrNull() if (ret == null) { die("--$optionName value must not be empty") } return ret!! } private fun doImport() { val csvParser = CSVParserBuilder() .withEscapeChar(escape) .withQuoteChar(quote) .withSeparator(separator) .withIgnoreLeadingWhiteSpace(commandLine.hasOption(ImportSubcommand.IGNORE)) .build() val csvReader = CSVReaderBuilder(FileReader(commandLine.args[0])) .withCSVParser(csvParser) .build() csvReader.use { if (commandLine.hasOption(ImportSubcommand.SKIP)) { it.skip(1) } it.iterator().forEach { fields -> val importedEntry = fromCsv(fields) val thisEntry = Entry.fromDatabase(db, importedEntry.name) try { if (okToChange(thisEntry, importedEntry)) { if (thisEntry == null) { importedEntry.insert(db) } else { importedEntry.update(db) } } } finally { thisEntry?.password?.clear() } } } } private fun fromCsv(fields: Array<String>): Entry { if (fields.size != NFIELDS) { die("expected $NFIELDS fields but got ${fields.size}") } return Entry( name = fields[0], username = fields[1], password = fields[2].toCharArray(), notes = if (saysNull(fields[3])) null else fields[3], created = parseCsvTime(fields[4]) ?: Date(), modified = parseCsvTime(fields[5]), accessed = parseCsvTime(fields[6]) ) } private fun parseCsvTime(unparsed: String): Date? { if (saysNull(unparsed)) { return null } try { return CSV_DATE_FORMAT.parse(unparsed) } catch (e: ParseException) { die("${see(unparsed)} - invalid date/time string") throw e /* kotlin is too stupid to realize this never happens */ } } private fun okToChange(thisEntry: Entry?, otherEntry: Entry): Boolean = thisEntry == null || commandLine.hasOption(FORCE) || askUserIfOkToOverwrite(thisEntry, otherEntry) private fun saysNull(string: String) = string.lowercase() == "null" }