view src/main/kotlin/name/blackcap/passman/ImportSubcommand.kt @ 27:3a3067ba673b

Add idle-time detection to interactive mode, clean up imports.
author David Barts <n5jrn@me.com>
date Sat, 27 Jul 2024 09:50:54 -0700
parents ea65ab890f66
children
line wrap: on
line source

package name.blackcap.passman

import com.opencsv.CSVParserBuilder
import com.opencsv.CSVReaderBuilder
import com.opencsv.exceptions.CsvException
import org.apache.commons.cli.ParseException
import java.io.FileInputStream
import java.io.IOException
import java.io.InputStreamReader
import java.text.SimpleDateFormat
import java.util.*

class ImportSubcommand(): Subcommand() {
    private companion object {
        const val NFIELDS = 7
    }
    private lateinit var csvDateFormat: SimpleDateFormat
    private lateinit var db: Database
    private val options = ImportExportArguments()
    private lateinit var csvFile: String
    private var line = 0

    override fun run(args: Array<String>) {
        parseArguments(args)
        db = Database.default
        try {
            doImport()
        } catch (e: IOException) {
            throw SubcommandException(message = e.message ?: "I/O error", cause = e)
        } catch (e: CsvException) {
            val baseMessage = e.message ?: "CSV error"
            throw SubcommandException(message = "line $line, $baseMessage", cause = e)
        }
    }

    private fun parseArguments(args: Array<String>) {
        val params = parseInto("import", args, options)
        when (params.size) {
            0 -> throw SubcommandException(message = "expecting CSV file name", status = 2)
            1 -> csvFile = params[0]
            else -> throw SubcommandException(message = "unexpected trailing arguments", status = 2)
        }
        csvDateFormat = SimpleDateFormat(options.format).apply {
            timeZone = TimeZone.getTimeZone(options.zone)
            isLenient = false
        }
    }

    private fun doImport() {
        val csvParser = CSVParserBuilder()
            .withEscapeChar(options.escape)
            .withQuoteChar(options.quote)
            .withSeparator(options.separator)
            .withIgnoreLeadingWhiteSpace(options.ignore)
            .build()

        val csvReader = CSVReaderBuilder(InputStreamReader(FileInputStream(csvFile), options.charset))
            .withCSVParser(csvParser)
            .build()

        csvReader.use {
            if (options.skip) {
                line++
                it.skip(1)
            }

            it.iterator().forEach { fields ->
                line++
                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) {
            throw SubcommandException(message = "line $line, 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 csvDateFormat.parse(unparsed)
        } catch (e: ParseException) {
            throw SubcommandException(message = "${see(unparsed)} - invalid date/time string", cause = e)
        }
    }

    private fun okToChange(thisEntry: Entry?, otherEntry: Entry): Boolean =
        thisEntry == null || options.force || askUserIfOkToOverwrite(thisEntry, otherEntry)

    private fun saysNull(string: String) = string.lowercase() == "null"

}