12
|
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 } |