mailshears 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Rakefile +32 -0
- data/bin/install-fixtures.sh +27 -0
- data/bin/mailshears +124 -0
- data/doc/LICENSE +661 -0
- data/doc/TODO +16 -0
- data/doc/mailshears.example.conf.yml +37 -0
- data/doc/man1/mailshears.1 +184 -0
- data/lib/common/agendav_plugin.rb +54 -0
- data/lib/common/configuration.rb +116 -0
- data/lib/common/davical_plugin.rb +104 -0
- data/lib/common/domain.rb +64 -0
- data/lib/common/dovecot_plugin.rb +130 -0
- data/lib/common/errors.rb +15 -0
- data/lib/common/exit_codes.rb +9 -0
- data/lib/common/filesystem.rb +43 -0
- data/lib/common/plugin.rb +238 -0
- data/lib/common/postfixadmin_plugin.rb +180 -0
- data/lib/common/roundcube_plugin.rb +96 -0
- data/lib/common/runner.rb +73 -0
- data/lib/common/user.rb +120 -0
- data/lib/common/user_interface.rb +53 -0
- data/lib/mailshears.rb +7 -0
- data/lib/mv/mv_dummy_runner.rb +45 -0
- data/lib/mv/mv_plugin.rb +40 -0
- data/lib/mv/mv_runner.rb +56 -0
- data/lib/mv/plugins/agendav.rb +46 -0
- data/lib/mv/plugins/davical.rb +43 -0
- data/lib/mv/plugins/dovecot.rb +64 -0
- data/lib/mv/plugins/postfixadmin.rb +70 -0
- data/lib/mv/plugins/roundcube.rb +44 -0
- data/lib/prune/plugins/agendav.rb +13 -0
- data/lib/prune/plugins/davical.rb +13 -0
- data/lib/prune/plugins/dovecot.rb +11 -0
- data/lib/prune/plugins/postfixadmin.rb +13 -0
- data/lib/prune/plugins/roundcube.rb +14 -0
- data/lib/prune/prune_dummy_runner.rb +44 -0
- data/lib/prune/prune_plugin.rb +66 -0
- data/lib/prune/prune_runner.rb +34 -0
- data/lib/rm/plugins/agendav.rb +38 -0
- data/lib/rm/plugins/davical.rb +38 -0
- data/lib/rm/plugins/dovecot.rb +48 -0
- data/lib/rm/plugins/postfixadmin.rb +114 -0
- data/lib/rm/plugins/roundcube.rb +42 -0
- data/lib/rm/rm_dummy_runner.rb +39 -0
- data/lib/rm/rm_plugin.rb +77 -0
- data/lib/rm/rm_runner.rb +51 -0
- data/mailshears.gemspec +39 -0
- data/test/mailshears.test.conf.yml +36 -0
- data/test/mailshears_test.rb +250 -0
- data/test/sql/agendav-fixtures.sql +9 -0
- data/test/sql/agendav.sql +157 -0
- data/test/sql/davical-fixtures.sql +23 -0
- data/test/sql/davical.sql +4371 -0
- data/test/sql/postfixadmin-fixtures.sql +48 -0
- data/test/sql/postfixadmin.sql +737 -0
- data/test/sql/roundcube-fixtures.sql +4 -0
- data/test/sql/roundcube.sql +608 -0
- data/test/test_mv.rb +174 -0
- data/test/test_prune.rb +121 -0
- data/test/test_rm.rb +154 -0
- metadata +133 -0
@@ -0,0 +1,53 @@
|
|
1
|
+
# Class methods for the creation and manipulation of our command-line
|
2
|
+
# user interface.
|
3
|
+
#
|
4
|
+
class UserInterface
|
5
|
+
|
6
|
+
# Construct a usage string showing how to invoke the program.
|
7
|
+
#
|
8
|
+
# @param program_name [String] the name of this program, used to
|
9
|
+
# construct the usage string.
|
10
|
+
#
|
11
|
+
# @return [String] a string showing the format of a correct program
|
12
|
+
# invocation.
|
13
|
+
#
|
14
|
+
def self.usage(program_name)
|
15
|
+
return "#{program_name} [prune | rm <target> | mv <src> <dst>]"
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
# Construct the header that precedes our other output. An example is,
|
20
|
+
#
|
21
|
+
# mailshears, 2015-11-06 09:57:06 -0500 (Plugin: PrunePlugin)
|
22
|
+
# ------------------------------------------------------------
|
23
|
+
#
|
24
|
+
# @param program_name [String] the name of this program, to appear
|
25
|
+
# in the header.
|
26
|
+
#
|
27
|
+
# @param plugin_name [String] the name of the mode (prune, mv, etc.)
|
28
|
+
# plugin that is being run.
|
29
|
+
#
|
30
|
+
# @return [String] a string containing the output header.
|
31
|
+
#
|
32
|
+
def self.make_header(program_name, plugin_name)
|
33
|
+
header = "#{program_name}, "
|
34
|
+
|
35
|
+
current_time = Time.now()
|
36
|
+
if current_time.respond_to?(:iso8601)
|
37
|
+
# Somehow this method is missing on some machines.
|
38
|
+
header += current_time.iso8601.to_s()
|
39
|
+
else
|
40
|
+
# Fall back to whatever this looks like.
|
41
|
+
header += current_time.to_s()
|
42
|
+
end
|
43
|
+
|
44
|
+
header += ' (Plugin: ' + plugin_name + ")\n"
|
45
|
+
|
46
|
+
# Underline the header, accounting for the newline.
|
47
|
+
header += '-' * (header.size() - 1)
|
48
|
+
|
49
|
+
return header
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
end
|
data/lib/mailshears.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'common/runner'
|
2
|
+
|
3
|
+
# Dummy implementation of a {MvRunner}. Its <tt>run()</tt> method will
|
4
|
+
# tell you what would have been moved, but will not actually perform
|
5
|
+
# the operation.
|
6
|
+
#
|
7
|
+
class MvDummyRunner
|
8
|
+
include Runner
|
9
|
+
|
10
|
+
# Pretend to move *src* to *dst* with *plugin*. Some "what if"
|
11
|
+
# information will be output to stdout. This is useful to see if
|
12
|
+
# there would be (for example) a username collision at *dst* before
|
13
|
+
# attempting the move in earnest.
|
14
|
+
#
|
15
|
+
# @param cfg [Configuration] the configuration options to pass to
|
16
|
+
# the *plugin* we're runnning.
|
17
|
+
#
|
18
|
+
# @param plugin [Class] plugin class that will perform the move.
|
19
|
+
#
|
20
|
+
# @param src [User] the source user to be moved.
|
21
|
+
#
|
22
|
+
# @param dst [User] the destination user, to which we will move *src*.
|
23
|
+
#
|
24
|
+
def run(cfg, plugin, src, dst)
|
25
|
+
|
26
|
+
if src.is_a?(Domain) or dst.is_a?(Domain) then
|
27
|
+
msg = 'only users can be moved'
|
28
|
+
raise NotImplementedError.new(msg)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Since we're not actually moving anything, the destination
|
32
|
+
# description is really only useful for seeing whether or not we'd
|
33
|
+
# be trying to move in on top of an existing account.
|
34
|
+
src_description = plugin.describe(src)
|
35
|
+
dst_description = plugin.describe(dst)
|
36
|
+
|
37
|
+
msg = "Would move user "
|
38
|
+
msg += add_description(src, src_description)
|
39
|
+
msg += " to "
|
40
|
+
add_description(dst, dst_description)
|
41
|
+
msg += "."
|
42
|
+
report(plugin, msg)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
data/lib/mv/mv_plugin.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'common/plugin.rb'
|
2
|
+
|
3
|
+
# Plugins for moving (renaming) users. Moving domains is not supported.
|
4
|
+
#
|
5
|
+
module MvPlugin
|
6
|
+
|
7
|
+
# Absorb the subclass run() magic from the Plugin::Run module.
|
8
|
+
extend Plugin::Run
|
9
|
+
|
10
|
+
# The runner class associated with move plugins.
|
11
|
+
#
|
12
|
+
# @return [Class] the {MvRunner} class.
|
13
|
+
#
|
14
|
+
def self.runner()
|
15
|
+
return MvRunner
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
# The "dummy" runner class associated with move plugins.
|
20
|
+
#
|
21
|
+
# @return [Class] the {MvDummyRunner} class.
|
22
|
+
#
|
23
|
+
def self.dummy_runner()
|
24
|
+
return MvDummyRunner
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
# The interface for the "move a user" operation. Subclasses need to
|
29
|
+
# implement this method so that it moves (renames) the user *src* to
|
30
|
+
# the user *dst*.
|
31
|
+
#
|
32
|
+
# @param src [User] the source user to be moved.
|
33
|
+
#
|
34
|
+
# @param dst [User] the destination user to which we'll move *src*.
|
35
|
+
#
|
36
|
+
def mv_user(src, dst)
|
37
|
+
raise NotImplementedError
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
data/lib/mv/mv_runner.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'common/domain'
|
2
|
+
require 'common/errors'
|
3
|
+
require 'common/runner'
|
4
|
+
|
5
|
+
# Perform the moving (renaming) of users/domains using {MvPlugin}s.
|
6
|
+
#
|
7
|
+
class MvRunner
|
8
|
+
include Runner
|
9
|
+
|
10
|
+
# Run *plugin* to move the user *src* to *dst*. The method
|
11
|
+
# signature includes the unused *cfg* for consistency with the
|
12
|
+
# runners that do need a {Configuration}.
|
13
|
+
#
|
14
|
+
# @param cfg [Configuration] unused.
|
15
|
+
#
|
16
|
+
# @param plugin [Class] plugin class that will perform the move.
|
17
|
+
#
|
18
|
+
# @param src [User] the source user to be moved.
|
19
|
+
#
|
20
|
+
# @param dst [User] the destination user being moved to.
|
21
|
+
#
|
22
|
+
def run(cfg, plugin, src, dst)
|
23
|
+
|
24
|
+
if src.is_a?(Domain) or dst.is_a?(Domain) then
|
25
|
+
msg = 'only users can be moved'
|
26
|
+
raise NotImplementedError.new(msg)
|
27
|
+
end
|
28
|
+
|
29
|
+
begin
|
30
|
+
src_description = plugin.describe(src)
|
31
|
+
plugin.mv_user(src, dst)
|
32
|
+
dst_description = plugin.describe(dst)
|
33
|
+
|
34
|
+
msg = "Moved user "
|
35
|
+
msg += add_description(src, src_description)
|
36
|
+
msg += " to "
|
37
|
+
msg += add_description(dst, dst_description)
|
38
|
+
msg += "."
|
39
|
+
report(plugin, msg)
|
40
|
+
|
41
|
+
rescue NonexistentUserError => e
|
42
|
+
# This means that the SOURCE user didn't exist, since a
|
43
|
+
# nonexistent destination user is perfectly expected.
|
44
|
+
report(plugin, "Source user #{src.to_s()} not found.")
|
45
|
+
rescue NonexistentDomainError => e
|
46
|
+
# This could mean that the source domain doesn't exist, but in
|
47
|
+
# that case, we just report that the source user doesn't
|
48
|
+
# exist. So a nonexistent domain refers to a nonexistent
|
49
|
+
# DESTINATION domain.
|
50
|
+
report(plugin, "Destination domain #{dst.domainpart()} not found.")
|
51
|
+
rescue UserAlreadyExistsError => e
|
52
|
+
report(plugin, "Destination user #{dst.to_s()} already exists.")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'pg'
|
2
|
+
|
3
|
+
require 'common/agendav_plugin'
|
4
|
+
require 'mv/mv_plugin'
|
5
|
+
|
6
|
+
|
7
|
+
# Handle moving (renaming) Agendav users in its database. Agendav has
|
8
|
+
# no concept of domains.
|
9
|
+
#
|
10
|
+
class AgendavMv
|
11
|
+
|
12
|
+
include AgendavPlugin
|
13
|
+
include MvPlugin
|
14
|
+
|
15
|
+
# Move the user *src* to *dst* within the Agendav database. This
|
16
|
+
# should "rename" him in _every_ table where he is referenced.
|
17
|
+
#
|
18
|
+
# This can fail is *src* does not exist, or if *dst* already exists
|
19
|
+
# before the move. It should also be an error if the destination
|
20
|
+
# domain doesn't exist. But Agendav doesn't know about domains, so
|
21
|
+
# we let that slide.
|
22
|
+
#
|
23
|
+
# @param src [User] the source user to be moved.
|
24
|
+
#
|
25
|
+
# @param dst [User] the destination user being moved to.
|
26
|
+
#
|
27
|
+
def mv_user(src, dst)
|
28
|
+
raise NonexistentUserError.new(src.to_s()) if not user_exists(src)
|
29
|
+
raise UserAlreadyExistsError.new(dst.to_s()) if user_exists(dst)
|
30
|
+
|
31
|
+
sql_queries = ['UPDATE prefs SET username = $1 WHERE username = $2;']
|
32
|
+
sql_queries << 'UPDATE shared SET user_from = $1 WHERE user_from = $2;'
|
33
|
+
sql_queries << 'UPDATE shared SET user_which = $1 WHERE user_which = $2;'
|
34
|
+
|
35
|
+
connection = PG::Connection.new(@db_hash)
|
36
|
+
begin
|
37
|
+
sql_queries.each do |sql_query|
|
38
|
+
connection.query(sql_query, [dst.to_s(), src.to_s()])
|
39
|
+
end
|
40
|
+
ensure
|
41
|
+
# Make sure the connection gets closed even if a query explodes.
|
42
|
+
connection.close()
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'pg'
|
2
|
+
|
3
|
+
require 'common/davical_plugin'
|
4
|
+
require 'rm/rm_plugin'
|
5
|
+
|
6
|
+
# Handle moving (renaming) DAViCal users in its database. DAViCal has
|
7
|
+
# no concept of domains.
|
8
|
+
#
|
9
|
+
class DavicalMv
|
10
|
+
include DavicalPlugin
|
11
|
+
include MvPlugin
|
12
|
+
|
13
|
+
|
14
|
+
# Move the user *src* to *dst* within the DAViCal database. This
|
15
|
+
# should "rename" him in _every_ table where he is referenced.
|
16
|
+
# DAViCal uses foreign keys properly, so we let the ON UPDATE
|
17
|
+
# CASCADE trigger handle most of the work.
|
18
|
+
#
|
19
|
+
# This can fail is *src* does not exist, or if *dst* already exists
|
20
|
+
# before the move. It should also be an error if the destination
|
21
|
+
# domain doesn't exist. But DAViCal doesn't know about domains, so
|
22
|
+
# we let that slide.
|
23
|
+
#
|
24
|
+
# @param src [User] the source user to be moved.
|
25
|
+
#
|
26
|
+
# @param dst [User] the destination user being moved to.
|
27
|
+
#
|
28
|
+
def mv_user(src, dst)
|
29
|
+
raise NonexistentUserError.new(src.to_s()) if not user_exists(src)
|
30
|
+
raise UserAlreadyExistsError.new(dst.to_s()) if user_exists(dst)
|
31
|
+
|
32
|
+
sql_query = 'UPDATE usr SET username = $1 WHERE username = $2;'
|
33
|
+
|
34
|
+
connection = PG::Connection.new(@db_hash)
|
35
|
+
begin
|
36
|
+
connection.query(sql_query, [dst.to_s(), src.to_s()])
|
37
|
+
ensure
|
38
|
+
# Make sure the connection gets closed even if the query explodes.
|
39
|
+
connection.close()
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
require 'common/filesystem'
|
4
|
+
require 'common/dovecot_plugin'
|
5
|
+
require 'mv/mv_plugin'
|
6
|
+
|
7
|
+
|
8
|
+
# Handle moving (renaming) Dovecot users on the filesystem.
|
9
|
+
#
|
10
|
+
class DovecotMv
|
11
|
+
|
12
|
+
include DovecotPlugin
|
13
|
+
include MvPlugin
|
14
|
+
|
15
|
+
|
16
|
+
# Move the Dovecot user *src* to *dst*. This relocates the user's
|
17
|
+
# directory within the Dovecot mailstore (on the filesystem).
|
18
|
+
#
|
19
|
+
# This fails if the source user does not exist, or if the
|
20
|
+
# destination user already exists before the move.
|
21
|
+
#
|
22
|
+
# But is it an error if the target domain does not exist? That's a
|
23
|
+
# bit subtle... The domain may exist in the database, but if it
|
24
|
+
# has not received any mail yet, then its directory won't exist
|
25
|
+
# on-disk.
|
26
|
+
#
|
27
|
+
# There are two possible "oops" scenarios resulting from the fact
|
28
|
+
# that we may run either the Postfixadmin move first or the
|
29
|
+
# Dovecot move first. If we move the user in the database, we
|
30
|
+
# definitely want to move him on disk (that is, we should create
|
31
|
+
# the directory here). But if we move him on disk first, then we
|
32
|
+
# don't know if the database move will fail! We don't want to move
|
33
|
+
# his mail files if he won't get moved in the database.
|
34
|
+
#
|
35
|
+
# Faced with two equally-bad (but easy-to-fix) options, we do the
|
36
|
+
# simplest thing and fail if the destination domain directory
|
37
|
+
# doesn't exist. If nothing else, this is at least consistent.
|
38
|
+
#
|
39
|
+
# @param src [User] the source user to be moved.
|
40
|
+
#
|
41
|
+
# @param dst [User] the destination user being moved to.
|
42
|
+
#
|
43
|
+
def mv_user(src, dst)
|
44
|
+
raise NonexistentUserError.new(src.to_s()) if not user_exists(src)
|
45
|
+
raise UserAlreadyExistsError.new(dst.to_s()) if user_exists(dst)
|
46
|
+
|
47
|
+
# See the docstring...
|
48
|
+
if not self.domain_exists(dst.domain()) then
|
49
|
+
raise NonexistentDomainError.new(dst.domainpart())
|
50
|
+
end
|
51
|
+
|
52
|
+
# We may need to create the target domain directory, even if the
|
53
|
+
# domain exists in the database.
|
54
|
+
FileUtils.mkdir_p(self.get_domain_path(dst.domain()))
|
55
|
+
|
56
|
+
# The parent of dst_path exists because we just created it.The
|
57
|
+
# source path should exist too, because the "source user" does,
|
58
|
+
# and, well, how did we determine that?
|
59
|
+
src_path = self.get_user_path(src)
|
60
|
+
dst_path = self.get_user_path(dst)
|
61
|
+
FileUtils.mv(src_path, dst_path)
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'pg'
|
2
|
+
|
3
|
+
require 'common/postfixadmin_plugin'
|
4
|
+
require 'mv/mv_plugin'
|
5
|
+
|
6
|
+
|
7
|
+
# Handle moving (renaming) of users in the Postfixadmin database.
|
8
|
+
#
|
9
|
+
class PostfixadminMv
|
10
|
+
|
11
|
+
include PostfixadminPlugin
|
12
|
+
include MvPlugin
|
13
|
+
|
14
|
+
|
15
|
+
# Move the user *src* to *dst* within the Postfixadmin
|
16
|
+
# database. This should "rename" him in _every_ table where he is
|
17
|
+
# referenced. Unfortunately that must be done manually.
|
18
|
+
#
|
19
|
+
# This can fail is *src* does not exist, or if *dst* already exists
|
20
|
+
# before the move. It will also fail if the domain associated with
|
21
|
+
# the user *dst* does not exist.
|
22
|
+
#
|
23
|
+
# @param src [User] the source user to be moved.
|
24
|
+
#
|
25
|
+
# @param dst [User] the destination user being moved to.
|
26
|
+
#
|
27
|
+
def mv_user(src, dst)
|
28
|
+
raise NonexistentUserError.new(src.to_s()) if not user_exists(src)
|
29
|
+
|
30
|
+
if not domain_exists(dst.domain())
|
31
|
+
raise NonexistentDomainError.new(dst.domain.to_s())
|
32
|
+
end
|
33
|
+
|
34
|
+
raise UserAlreadyExistsError.new(dst.to_s()) if user_exists(dst)
|
35
|
+
|
36
|
+
mailbox_query = 'UPDATE mailbox SET '
|
37
|
+
mailbox_query += ' username=$1,'
|
38
|
+
mailbox_query += ' domain=$2,'
|
39
|
+
mailbox_query += " maildir=CONCAT($2, '/', $3, '/'),"
|
40
|
+
mailbox_query += ' local_part=$3 '
|
41
|
+
mailbox_query += 'WHERE username=$4;'
|
42
|
+
|
43
|
+
alias_query1 = 'UPDATE alias SET '
|
44
|
+
alias_query1 += ' address=$1,'
|
45
|
+
alias_query1 += ' domain=$2,'
|
46
|
+
alias_query1 += ' goto=REPLACE(goto, $4, $1) '
|
47
|
+
alias_query1 += 'WHERE address=$4;'
|
48
|
+
|
49
|
+
alias_query2 = 'UPDATE alias SET '
|
50
|
+
alias_query2 += 'goto=REPLACE(goto, $4, $1);'
|
51
|
+
|
52
|
+
sql_queries = [mailbox_query, alias_query1, alias_query2]
|
53
|
+
|
54
|
+
connection = PG::Connection.new(@db_hash)
|
55
|
+
begin
|
56
|
+
sql_queries.each do |sql_query|
|
57
|
+
varchar = 1043 # from pg_type.h
|
58
|
+
params = [{:value => dst.to_s(), :type => varchar},
|
59
|
+
{:value => dst.domainpart(), :type => varchar},
|
60
|
+
{:value => dst.localpart(), :type => varchar},
|
61
|
+
{:value => src.to_s(), :type => varchar}]
|
62
|
+
connection.query(sql_query, params)
|
63
|
+
end
|
64
|
+
ensure
|
65
|
+
# Make sure the connection gets closed even if a query explodes.
|
66
|
+
connection.close()
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'pg'
|
2
|
+
|
3
|
+
require 'common/roundcube_plugin'
|
4
|
+
require 'mv/mv_plugin'
|
5
|
+
|
6
|
+
# Handle moving (renaming) of users in the Roundcube
|
7
|
+
# database. Roundcube has no concept of domains.
|
8
|
+
#
|
9
|
+
class RoundcubeMv
|
10
|
+
|
11
|
+
include RoundcubePlugin
|
12
|
+
include MvPlugin
|
13
|
+
|
14
|
+
|
15
|
+
# Move the user *src* to *dst* within the Roundcube database. This
|
16
|
+
# should "rename" him in _every_ table where he is referenced.
|
17
|
+
# Roundcube uses foreign keys properly, so we let the ON UPDATE
|
18
|
+
# CASCADE trigger handle most of the work.
|
19
|
+
#
|
20
|
+
# This can fail is *src* does not exist, or if *dst* already exists
|
21
|
+
# before the move. It should also be an error if the destination
|
22
|
+
# domain doesn't exist. But Roundcube doesn't know about domains, so
|
23
|
+
# we let that slide.
|
24
|
+
#
|
25
|
+
# @param src [User] the source user to be moved.
|
26
|
+
#
|
27
|
+
# @param dst [User] the destination user being moved to.
|
28
|
+
#
|
29
|
+
def mv_user(src, dst)
|
30
|
+
raise NonexistentUserError.new(src.to_s()) if not user_exists(src)
|
31
|
+
raise UserAlreadyExistsError.new(dst.to_s()) if user_exists(dst)
|
32
|
+
|
33
|
+
sql_query = 'UPDATE users SET username = $1 WHERE username = $2;'
|
34
|
+
|
35
|
+
connection = PG::Connection.new(@db_hash)
|
36
|
+
begin
|
37
|
+
connection.query(sql_query, [dst.to_s(), src.to_s()])
|
38
|
+
ensure
|
39
|
+
# Make sure the connection gets closed even if the query explodes.
|
40
|
+
connection.close()
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|