comparison 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
comparison
equal deleted inserted replaced
11:c69665ff37d0 12:a38a2a1036c3
1 package name.blackcap.passman
2
3 import com.opencsv.CSVParserBuilder
4 import com.opencsv.CSVReaderBuilder
5 import org.apache.commons.cli.*
6 import java.io.FileReader
7 import java.io.IOException
8 import java.text.SimpleDateFormat
9 import java.util.*
10 import kotlin.system.exitProcess
11
12 class ImportSubcommand(): Subcommand() {
13 private companion object {
14 val CSV_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").apply {
15 timeZone = TimeZone.getTimeZone("UTC")
16 isLenient = false
17 }
18 const val ESCAPE = "escape"
19 const val FORCE = "force"
20 const val HELP = "help"
21 const val IGNORE = "ignore"
22 const val QUOTE = "quote"
23 const val SEPARATOR = "separator"
24 const val SKIP = "skip"
25 const val NFIELDS = 7
26
27 }
28 private lateinit var commandLine: CommandLine
29 private lateinit var db: Database
30
31 /* default option values */
32 private var escape = '\\'
33 private var quote = '"'
34 private var separator = ','
35
36 override fun run(args: Array<String>) {
37 parseArguments(args)
38 db = Database.open()
39 try {
40 doImport()
41 } catch (e: IOException) {
42 die(e.message ?: "I/O error")
43 }
44 }
45
46 private fun parseArguments(args: Array<String>) {
47 val options = Options().apply {
48 addOption("e", ImportSubcommand.ESCAPE, true, "CSV escape character (default $escape).")
49 addOption("f", ImportSubcommand.FORCE, false, "Do not ask before overwriting.")
50 addOption("h", ImportSubcommand.HELP, false, "Print this help message.")
51 addOption("i", ImportSubcommand.IGNORE, false, "Ignore white space before quoted strings.")
52 addOption("q", ImportSubcommand.QUOTE, true, "CSV string-quoting character (default $quote).")
53 addOption("s", ImportSubcommand.SEPARATOR, true, "CSV separator character (default $separator).")
54 addOption("k", ImportSubcommand.SKIP, false, "Skip first line of input.")
55 }
56 try {
57 commandLine = DefaultParser().parse(options, args)
58 } catch (e: ParseException) {
59 die(e.message ?: "syntax error", 2)
60 }
61 if (commandLine.hasOption(ImportSubcommand.HELP)) {
62 HelpFormatter().printHelp("$SHORTNAME merge [options] csv_file", options)
63 exitProcess(0)
64 }
65 if (commandLine.args.isEmpty()) {
66 die("expecting other CSV file name", 2)
67 }
68 if (commandLine.args.size > 1) {
69 die("unexpected trailing arguments", 2)
70 }
71 escape = getOptionChar(ImportSubcommand.ESCAPE, escape)
72 quote = getOptionChar(ImportSubcommand.QUOTE, quote)
73 separator = getOptionChar(ImportSubcommand.SEPARATOR, separator)
74 }
75
76 private fun getOptionChar(optionName: String, defaultValue: Char): Char {
77 val optionValue = commandLine.getOptionValue(optionName) ?: return defaultValue
78 val ret = optionValue.firstOrNull()
79 if (ret == null) {
80 die("--$optionName value must not be empty")
81 }
82 return ret!!
83 }
84
85 private fun doImport() {
86 val csvParser = CSVParserBuilder()
87 .withEscapeChar(escape)
88 .withQuoteChar(quote)
89 .withSeparator(separator)
90 .withIgnoreLeadingWhiteSpace(commandLine.hasOption(ImportSubcommand.IGNORE))
91 .build()
92
93 val csvReader = CSVReaderBuilder(FileReader(commandLine.args[0]))
94 .withCSVParser(csvParser)
95 .build()
96
97 csvReader.use {
98 if (commandLine.hasOption(ImportSubcommand.SKIP)) {
99 it.skip(1)
100 }
101 it.iterator().forEach { fields ->
102 val importedEntry = fromCsv(fields)
103 val thisEntry = Entry.fromDatabase(db, importedEntry.name)
104 try {
105 if (okToChange(thisEntry, importedEntry)) {
106 if (thisEntry == null) {
107 importedEntry.insert(db)
108 } else {
109 importedEntry.update(db)
110 }
111 }
112 } finally {
113 thisEntry?.password?.clear()
114 }
115 }
116 }
117 }
118
119 private fun fromCsv(fields: Array<String>): Entry {
120 if (fields.size != NFIELDS) {
121 die("expected $NFIELDS fields but got ${fields.size}")
122 }
123 return Entry(
124 name = fields[0],
125 username = fields[1],
126 password = fields[2].toCharArray(),
127 notes = if (saysNull(fields[3])) null else fields[3],
128 created = parseCsvTime(fields[4]) ?: Date(),
129 modified = parseCsvTime(fields[5]),
130 accessed = parseCsvTime(fields[6])
131 )
132 }
133
134 private fun parseCsvTime(unparsed: String): Date? {
135 if (saysNull(unparsed)) {
136 return null
137 }
138 try {
139 return CSV_DATE_FORMAT.parse(unparsed)
140 } catch (e: ParseException) {
141 die("${see(unparsed)} - invalid date/time string")
142 throw e /* kotlin is too stupid to realize this never happens */
143 }
144 }
145
146 private fun okToChange(thisEntry: Entry?, otherEntry: Entry): Boolean =
147 thisEntry == null || commandLine.hasOption(FORCE) || askUserIfOkToOverwrite(thisEntry, otherEntry)
148
149 private fun saysNull(string: String) = string.lowercase() == "null"
150
151 }