diff --git a/lib/Redmine/DB/CTX.pm b/lib/Redmine/DB/CTX.pm index 633c913f9c64fa41257ddb16796cbb3ab4123225..824b93304a5dea4bd1162bee7ffad289cedcf61b 100644 --- a/lib/Redmine/DB/CTX.pm +++ b/lib/Redmine/DB/CTX.pm @@ -41,6 +41,34 @@ sub sync_project $ctx->sync_wiki ($sp_id, $dp_id); } +sub sync_cleanup_project +{ + my $ctx= shift; + my $dp_id= shift; + + my $dbh= $ctx->{'dst'}->connect(); + return undef unless (defined ($dbh)); + +=begin comment + + $ctx->{'tlt'}= undef; + +not ready... + + my @tables= qw(watchers); + foreach my $table (@tables) + { + my $ss_0= 'FROM `syncs` WHERE table_name=? AND + my $ss_1= "SELECT id + } + + $ctx->{'tlt'}= undef; + +=end comment +=cut + +} + =head1 TRANSLATION Possibly the most important aspect of a synchronisation job is the diff --git a/lib/Redmine/DB/MySQL.pm b/lib/Redmine/DB/MySQL.pm index ca7c36794a6befeabe9def8cbd63086de8f65028..be0d8524163a2a196426153f67d2ac58d71c6bd1 100644 --- a/lib/Redmine/DB/MySQL.pm +++ b/lib/Redmine/DB/MySQL.pm @@ -11,9 +11,9 @@ use Data::Dumper; my $show_query= 0; my $show_fetched= 0; -sub show_fetched { shift; $show_fetched= shift; } -sub show_query { shift; $show_query= shift; } -sub verbose { shift; $show_fetched= $show_query= shift; } +sub show_fetched { shift; my $ret= $show_fetched; $show_fetched= shift; $ret; } +sub show_query { shift; my $ret= $show_query; $show_query= shift; $ret } +sub verbose { shift; my @ret= ($show_fetched, $show_query); $show_fetched= $show_query= shift; @ret; } sub connect { @@ -22,10 +22,13 @@ sub connect my $dbh= $self->{'_dbh'}; return $dbh if (defined ($dbh)); - my $db_con= join (':', 'dbi', map { $self->{$_} } qw(adapter database host)); - print "db_con=[$db_con]\n"; + my $adapter= $self->{adapter}; + $adapter= 'mysql' if ($adapter eq 'mysql2'); + + my $db_con= join (':', 'dbi', $adapter, map { $self->{$_} } qw(database host)); + # print "db_con=[$db_con]\n"; $dbh= DBI->connect($db_con, map { $self->{$_} } qw(username password)); - print "dbh=[$dbh]\n"; + # print "dbh=[$dbh]\n"; $self->{'_dbh'}= $dbh; } @@ -40,7 +43,7 @@ sub table $t; } -=head2 $con->get_all_x ($table_name, $query_ref) +=head2 $con->get_all_x ($table_name, $query_ref, $field_ref) Query_ref is an array reference where the first parameter gives the WHERE clause (without the string "WHERE"). The query should not contain untrustable values, these should be indicated by placeholders (an "?" for each @@ -57,14 +60,12 @@ sub get_all_x my $self= shift; my $table= shift; my $where= shift; + my $field_ref= shift || '*'; my $dbh= $self->connect(); return undef unless (defined ($dbh)); - # my $project= new Redmine::DB::Project (%par); - # print "project: ", Dumper ($project); - - my $ss= "SELECT * FROM $table"; + my $ss= "SELECT $field_ref FROM $table"; my @v= (); if (defined ($where)) @@ -88,55 +89,130 @@ sub get_all_x my $t= $self->table($table); my $tt= {}; + my $pri= (exists ($self->{PRI}->{$table})) ? $self->{PRI}->{$table} : 'id'; while (defined (my $x= $sth->fetchrow_hashref())) { print "x: ", Dumper ($x) if ($show_fetched); - my $i= $x->{'id'}; + my $i= $x->{$pri}; $t->{$i}= $tt->{$i}= $x; } $tt; } -sub fetch_custom +sub delete_all_x { - my $db= shift; - my $cfid= shift; - my $cfty= shift || 'Issue'; + my $self= shift; + my $table= shift; + my $where= shift; + my $field_ref= shift || '*'; - my $res= $db->get_all_x ('custom_values', - [ "custom_field_id=? and customized_type=?", $cfid, $cfty ]); - $res; + my $dbh= $self->connect(); + return undef unless (defined ($dbh)); + + my $ss= "DELETE $field_ref FROM $table"; + + my @v= (); + if (defined ($where)) + { + # print "where: ", Dumper ($where) if ($show_query); + $ss .= ' WHERE ' . shift (@$where); + @v= @$where; + } + + if ($show_query) + { + print "ss=[$ss]"; + print ' vars: ', join (',', @v) if (@v); + print "\n"; + } + + my $sth= $dbh->prepare($ss) or print $dbh->errstr; + # print "sth=[$sth]\n"; + $sth->execute(@v); } -sub change_custom_value +sub tables { - my $db= shift; - my $cfid= shift; - my $cfty= shift || 'Issue'; - my $cfref= shift; # ticket number or whatever - my $cfrid= shift; # record id - my $cfval= shift; + my $self= shift; - my $data= - { customized_type => $cfty, customized_id => $cfref, - custom_field_id => $cfid, value => $cfval }; + my $dbh= $self->connect(); + return undef unless (defined ($dbh)); - print "change_custom_value: cfrid=[$cfrid] ", join (' ', %$data), "\n"; - # return 0; # TODO: add flag to supress changes + my $ss= "SHOW TABLES"; - my $res; - if (defined ($cfrid)) + if ($show_query) { - $db->update ('custom_values', $cfrid, $data); - $res= $cfrid; + print "ss=[$ss]\n"; } - else + + my $sth= $dbh->prepare($ss) or print $dbh->errstr; + # print "sth=[$sth]\n"; + $sth->execute(); + + my $table_filter= $self->{table_filter}; + + my $table_names= $self->{table_names}= {}; + while (defined (my $table_name= $sth->fetchrow_array())) { - $res= $db->insert ('custom_values', $data); + next if (defined ($table_filter) && &$table_filter($table_name) == 0); + $table_names->{$table_name}= undef; } - $res; + $table_names; +} + +sub desc_all +{ + my $self= shift; + + my $table_names= $self->tables(); + + foreach my $table_name (sort keys %$table_names) + { + $self->desc($table_name); + } +} + +sub desc +{ + my $self= shift; + my $table= shift; + + my $dbh= $self->connect(); + return undef unless (defined ($dbh)); + + my $ss= "DESC `$table`"; + + if ($show_query) + { + print "ss=[$ss]\n"; + } + + my $sth= $dbh->prepare($ss) or print $dbh->errstr; + # print "sth=[$sth]\n"; + $sth->execute(); + + # get table definition + my $td= $self->{table_names}->{$table}; + my $td= $self->{table_names}->{$table}= {} unless (defined ($td)); + my $tt= $td->{'columns'}= []; + + # my @desc_columns= qw(Field Type Null Key Default Extra); + + while (defined (my @x= $sth->fetchrow_array())) + { + last unless (@x); + # print "x: ", Dumper (\@x); # if ($show_fetched); + push (@$tt, \@x); + + if ($x[3] eq 'PRI') + { + $self->{PRI}->{$table}= $x[0]; + } + } + + $td; } sub insert @@ -208,7 +284,8 @@ sub mysql my $self= shift; print "self: ", Dumper ($self); - my @cmd= ('mysql', '-h', $self->{'host'}, '-u', $self->{'username'}, $self->{'database'}, '--password='.$self->{'password'}); + $ENV{MYSQL_PWD}= $self->{password}; + my @cmd= ('mysql', '-h', $self->{'host'}, '-u', $self->{'username'}, $self->{'database'}); print ">> cmd=[", join (' ', @cmd), "]\n"; system (@cmd); } @@ -378,6 +455,47 @@ sub pcx_user_preferences $res; } +sub fetch_custom +{ + my $db= shift; + my $cfid= shift; + my $cfty= shift || 'Issue'; + + my $res= $db->get_all_x ('custom_values', + [ "custom_field_id=? and customized_type=?", $cfid, $cfty ]); + $res; +} + +sub change_custom_value +{ + my $db= shift; + my $cfid= shift; + my $cfty= shift || 'Issue'; + my $cfref= shift; # ticket number or whatever + my $cfrid= shift; # record id + my $cfval= shift; + + my $data= + { customized_type => $cfty, customized_id => $cfref, + custom_field_id => $cfid, value => $cfval }; + + print "change_custom_value: cfrid=[$cfrid] ", join (' ', %$data), "\n"; + # return 0; # TODO: add flag to supress changes + + my $res; + if (defined ($cfrid)) + { + $db->update ('custom_values', $cfrid, $data); + $res= $cfrid; + } + else + { + $res= $db->insert ('custom_values', $data); + } + + $res; +} + 1; __END__ diff --git a/t_sync.pl b/t_sync.pl index 658c3353b65925d907dae50b3fd52e957f896f48..d9213c725da83eb7871f70930de47494b3f0892a 100755 --- a/t_sync.pl +++ b/t_sync.pl @@ -9,13 +9,15 @@ use warnings; =head1 DESCRIPTION -Do stuff on a Redmine database. Used as an experimental sync-tool to -migrate individual projects to another server. +Do stuff on a Redmine MySQL database. Used as an experimental sync-tool +to migrate individual projects to another instance. See https://github.com/gonter/redmine-sync =head1 OPERATION MODES +--mysql <src|dst> ... connect to source or destinance instance's MySQL databae + =cut use lib 'lib'; @@ -56,14 +58,25 @@ my $setup= 'src' => { 'config' => '/home/gg/etc/src/database.yml', - 'db' => 'production', + 'db' => 'production', 'attachment_base' => '/home/backup/redmine-phaidra/files', 'attachment_with_directory' => 0, # Redmine version 1.x does not have that attribute }, 'dst' => { 'config' => '/home/gg/etc/dst/database.yml', - 'db' => 'production', + 'db' => 'production', + 'attachment_base' => '/var/lib/redmine/default/files', + 'attachment_with_directory' => 1, # Redmine version 2.x has that attribute + }, +}; + +my $setup_OLD= +{ + 'dst' => + { + 'config' => '/home/gg/etc/dst/database.yml', + 'db' => 'production', 'attachment_base' => '/var/lib/redmine/default/files', 'attachment_with_directory' => 1, # Redmine version 2.x has that attribute }, @@ -105,6 +118,7 @@ while (my $arg= shift (@ARGV)) foreach my $opt (@opts) { if ($opt eq 'h') { usage(); } + elsif ($opt eq 'X') { $setup= $setup_OLD; } else { die "unknown option [$opt]"; } } } @@ -156,7 +170,7 @@ elsif ($op_mode eq 'prep') my $dst= read_configs($setup, 'dst'); prepare_sync_table ($dst); } -elsif ($op_mode eq 'sdp') # sdp: show destination intance's projects +elsif ($op_mode eq 'sdp') # sdp: show destination instance's projects { my $dst= read_configs($setup, 'dst'); @@ -184,8 +198,10 @@ elsif ($op_mode eq 'user') { my $target= shift (@parameters); usage() unless (defined ($target)); + my $an= shift (@parameters); usage() unless (defined ($an)); + my $cfg= read_configs($setup, $target); foreach my $av (@parameters) @@ -210,6 +226,21 @@ elsif ($op_mode eq 'syncuser') $ctx->sync_user ($s_user_id, $res->{$s_user_id}); } } +elsif ($op_mode eq 'cleanup') +{ + my $dst= read_configs($setup, 'dst'); + + my $ctx= new Redmine::DB::CTX ('ctx_id' => $setup->{'sync_context_id'}, 'dst' => $dst); + foreach my $sp (@{$setup->{'sync_projects'}}) + { + # print "sp: ", Dumper ($sp); + +print "not yet implemented\n"; + # $ctx->sync_cleanup_project ($sp->{'dst_proj'}); + } + + print "\n"x3, '='x72, "\n", "Statistics:", Dumper ($ctx->{'stats'}); +} elsif ($op_mode eq 'sync') { my $src= read_configs($setup, 'src'); @@ -269,7 +300,7 @@ sub read_configs # $ss->{'_cfg'}= my $c= $x->{$db}; - $c->{'adapter'}= 'mysql' if ($c->{'adapter'} eq 'mysql2'); + # $c->{'adapter'}= 'mysql' if ($c->{'adapter'} eq 'mysql2'); my $m= new Redmine::DB::MySQL (%$c); $ss->{'m'}= $m; @@ -293,3 +324,12 @@ sub prepare_sync_table $ddl_syncs, "--- >8 ---\n"; } + +__END__ + +=head1 TODO + + * currently, only MySQL source and targets were used, supporting + PostgreSQL and/or sqlite would be nice as well + +