Mercurial > cgi-bin > hgweb.cgi > PassMan
comparison src/main/kotlin/name/blackcap/passman/MergeSubcommand.kt @ 11:c69665ff37d0
Add merge subcommand (untested).
author | David Barts <n5jrn@me.com> |
---|---|
date | Sat, 21 Jan 2023 15:39:42 -0800 |
parents | 72619175004e |
children | a38a2a1036c3 |
comparison
equal
deleted
inserted
replaced
10:cbe4c797c9a6 | 11:c69665ff37d0 |
---|---|
1 package name.blackcap.passman | 1 package name.blackcap.passman |
2 | 2 |
3 import org.apache.commons.cli.* | |
4 import java.sql.ResultSet | |
5 import java.util.* | |
6 import kotlin.system.exitProcess | |
7 | |
3 class MergeSubcommand(): Subcommand() { | 8 class MergeSubcommand(): Subcommand() { |
9 private companion object { | |
10 const val FORCE = "force" | |
11 const val HELP = "help" | |
12 } | |
13 private lateinit var commandLine: CommandLine | |
14 private lateinit var db: Database | |
15 | |
4 override fun run(args: Array<String>) { | 16 override fun run(args: Array<String>) { |
5 /* | 17 parseArguments(args) |
6 * To merge, plow through both the old and the new databases in the same | 18 db = Database.open() |
7 * order. By id is sorta idiosyncratic by human standards, but why not? | 19 doMerge() |
8 * If the entries do not match, write the lowest-numbered one to the | |
9 * output database and read the next record from where the lowest-numbered | |
10 * one came. If they do match, rely on modified time (fall back to created | |
11 * time if null) to sort out the winner. Continue till we hit the end | |
12 * of one database, then "drain" the other. Preserve time stamps. | |
13 * | |
14 * Maybe do something special (warning? confirmation prompt?) on mismatched | |
15 * creation times, as this means a new record was created w/o knowledge | |
16 * of an existing one. Choices should be to clobber w/ newest, pick one | |
17 * manually, or rename one so the other can persist under original name. | |
18 * | |
19 */ | |
20 error("not yet implemented") | |
21 } | 20 } |
21 | |
22 private fun parseArguments(args: Array<String>) { | |
23 val options = Options().apply { | |
24 addOption("f", MergeSubcommand.FORCE, false, "Do not ask before overwriting.") | |
25 addOption("h", MergeSubcommand.HELP, false, "Print this help message.") | |
26 } | |
27 try { | |
28 commandLine = DefaultParser().parse(options, args) | |
29 } catch (e: ParseException) { | |
30 die(e.message ?: "syntax error", 2) | |
31 } | |
32 if (commandLine.hasOption(MergeSubcommand.HELP)) { | |
33 HelpFormatter().printHelp("$SHORTNAME merge [options] other_database", options) | |
34 exitProcess(0) | |
35 } | |
36 if (commandLine.args.isEmpty()) { | |
37 die("expecting other database name", 2) | |
38 } | |
39 if (commandLine.args.size > 1) { | |
40 die("unexpected trailing arguments", 2) | |
41 } | |
42 } | |
43 | |
44 private fun doMerge() { | |
45 val otherFile = commandLine.args[0] | |
46 val otherDb = Database.open( | |
47 fileName = otherFile, | |
48 passwordPrompt = "Key for ${see(otherFile)}: ", create = false | |
49 ) | |
50 otherDb.connection.prepareStatement("select name, username, password, notes, created, modified, accessed from passwords").use { stmt -> | |
51 val results = stmt.executeQuery() | |
52 while (results.next()) { | |
53 val otherEntry = makeEntry(results) | |
54 val thisEntry = getEntry(db, otherEntry.name) | |
55 if (thisEntry == null) { | |
56 doInsert(otherEntry) | |
57 } else { | |
58 doCompare(thisEntry, otherEntry) | |
59 thisEntry.password.clear() | |
60 } | |
61 otherEntry.password.clear() | |
62 } | |
63 } | |
64 } | |
65 | |
66 private fun makeEntry(results: ResultSet) = Entry( | |
67 name = results.getDecryptedString(1, db.encryption)!!, | |
68 username = results.getDecryptedString(2, db.encryption)!!, | |
69 password = results.getDecrypted(3, db.encryption)!!, | |
70 notes = results.getDecryptedString(4, db.encryption), | |
71 created = results.getDate(5), | |
72 modified = results.getDate(6), | |
73 accessed = results.getDate(7) | |
74 ) | |
75 | |
76 private fun getEntry(otherDb: Database, name: String): Entry? { | |
77 otherDb.connection.prepareStatement("select name, username, password, notes, created, modified, accessed from passwords where id = ?").use { stmt -> | |
78 stmt.setLong(1, otherDb.makeKey(name)) | |
79 val results = stmt.executeQuery() | |
80 return if (results.next()) makeEntry(results) else null | |
81 } | |
82 } | |
83 | |
84 private fun doInsert(entry: Entry) { | |
85 db.connection.prepareStatement("insert into passwords (id, name, username, password, notes, created, modified, accessed) values (?, ?, ?, ?, ?, ?, ?, ?)") | |
86 .use { | |
87 it.setLong(1, db.makeKey(entry.name)) | |
88 it.setEncryptedString(2, entry.name, db.encryption) | |
89 it.setEncryptedString(3, entry.username, db.encryption) | |
90 it.setEncrypted(4, entry.password, db.encryption) | |
91 it.setEncryptedString(5, entry.notes, db.encryption) | |
92 it.setLongOrNull(6, entry.created?.time) | |
93 it.setLongOrNull(7, entry.modified?.time) | |
94 it.setLongOrNull(8, entry.accessed?.time) | |
95 it.executeUpdate() | |
96 } | |
97 } | |
98 | |
99 private fun doCompare(thisEntry: Entry, otherEntry: Entry) { | |
100 if (otherEntry.modifiedOrCreated.after(thisEntry.modifiedOrCreated) && okToChange(thisEntry, otherEntry)) { | |
101 db.connection.prepareStatement("update passwords set name = ?, username = ?, password = ?, notes = ?, modified = ? where id = ?").use { | |
102 it.setEncryptedString(1, otherEntry.name, db.encryption) | |
103 it.setEncryptedString(2, otherEntry.username, db.encryption) | |
104 it.setEncrypted(3, otherEntry.password, db.encryption) | |
105 it.setEncryptedString(4, otherEntry.notes, db.encryption) | |
106 it.setLong(5, otherEntry.modifiedOrCreated.time) | |
107 it.setLong(6, db.makeKey(thisEntry.name)) | |
108 it.executeUpdate() | |
109 } | |
110 } | |
111 } | |
112 | |
113 private fun okToChange(thisEntry: Entry, otherEntry: Entry): Boolean { | |
114 if (commandLine.hasOption(MergeSubcommand.FORCE)) { | |
115 return true | |
116 } | |
117 val REDACTED = "(redacted)" | |
118 println("EXISTING ENTRY:") | |
119 thisEntry.printLong(REDACTED) | |
120 println() | |
121 println("NEWER ENTRY:") | |
122 otherEntry.printLong(REDACTED) | |
123 println() | |
124 val answer = name.blackcap.passman.readLine("OK to overwrite existing entry? ") | |
125 println() | |
126 return answer.trimStart().firstOrNull()?.uppercaseChar() in setOf('T', 'Y') | |
127 } | |
128 | |
22 } | 129 } |