Current File : //scripts/convert_roundcube_mysql2sqlite
#!/usr/local/cpanel/3rdparty/bin/perl
# cpanel - scripts/convert_roundcube_mysql2sqlite Copyright 2022 cPanel, L.L.C.
# All rights reserved.
# copyright@cpanel.net http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited
package Script::RCube::Mysql2Sqlite;
use strict;
## no critic (RequireUseWarnings)
use Try::Tiny;
use Cpanel::AccessIds::ReducedPrivileges ();
use Cpanel::Config::LoadCpConf ();
use Cpanel::MysqlUtils ();
use Cpanel::DbUtils ();
use Cpanel::MysqlUtils::Connect ();
use Cpanel::MysqlUtils::Command ();
use Cpanel::MysqlRun ();
use Cpanel::Config::LoadUserDomains ();
use Cpanel::Email::RoundCube ();
use Cpanel::Email::RoundCube::DBI ();
use Cpanel::Quota::Temp ();
use Cpanel::PwCache ();
use Cpanel::Logger ();
use Cpanel::Filesys::Home ();
use File::Basename ();
use File::Copy ();
use File::Path ();
use File::Slurper ();
use DBI;
use XML::Simple ();
use IPC::Open3 ();
$XML::Simple::PREFERRED_PARSER = "XML::SAX::PurePerl";
my $sqlite_table_file = '/usr/local/cpanel/base/3rdparty/roundcube/SQL/sqlite.initial.sql';
my $log_file = '/usr/local/cpanel/logs/roundcube_sqlite_convert_log';
my %opts = ( 'alternate_logfile' => $log_file );
my $logger = Cpanel::Logger->new( \%opts );
my $mysql_dbname = 'roundcube';
my $dbh;
# If this updates, then do_all_rcube_xml_to_db() needs to be updated to match
my @dumptables = qw(users identities contacts contactgroups contactgroupmembers calendars caldav_calendars events caldav_events attachments caldav_attachments itipinvitations responses);
my $time = time();
## if invoked as a script, there is nothing in the call stack
my $invoked_as_script = !caller();
__PACKAGE__->script(@ARGV) if ($invoked_as_script);
my $mysql_plus_sqlite;
sub script {
my ( $package, $opt_user, $opt_dbname ) = @_;
my $cpconf = Cpanel::Config::LoadCpConf::loadcpconf();
my $db_type = 'mysql';
if ( exists $cpconf->{'roundcube_db'} ) {
$db_type = 'sqlite' if $cpconf->{'roundcube_db'} eq 'sqlite';
$mysql_plus_sqlite = $cpconf->{'roundcube_db'} eq 'mysql_plus_sqlite';
}
if ( defined $opt_user ) {
## the optional user arg feature is currently *only* called as a script;
## otherwise, all these exits would check $invoked_as_script
## Transfers.pm is the only user of $opt_user, which it uses in conjunction with $opt_user;
## this changes the global variable $opt_dbname just in time to be used in &init_check
if ( defined $opt_dbname ) {
$mysql_dbname = $opt_dbname;
}
if ( $db_type eq 'sqlite' && init_check($cpconf) ) {
my $wasSuccess = convert_mysql_roundcube_to_sqlite($opt_user);
if ($wasSuccess) {
## Transfers.pm operates over a different dbname, and is responsible for the
## removal of the temporary database
unless ( defined $opt_dbname ) {
Cpanel::Email::RoundCube::archive_and_drop_mysql_roundcube($logger);
}
return 1;
}
die "Conversion for user '$opt_user' was not successful.\n";
}
## SOMEDAY @GOLIVE: update-roundcube-sqlite-db gets moved to update-roundcube-db. Update this message and the conditional @GOIVE.
die "The optional user argument is to be used only when Roundcube has been converted to sqlite. Use bin/update-roundcube-sqlite-db Exiting.\n";
}
## Has the roundcube conversion already happened?
## note: this check is here, and not in init_check, because Transfers.pm calls into this
## from a different context. The src server uses MySQL but the dest machine has done
## the sqlite conversion.
if ( $db_type eq 'sqlite' ) {
$logger->info("Roundcube conversion already occurred, bailing out.");
if ($invoked_as_script) {
exit(0);
}
return 1;
}
unless ( init_check($cpconf) ) {
if ($invoked_as_script) {
exit(0);
}
return 1;
}
my %TRUEDOMAINS;
Cpanel::Config::LoadUserDomains::loadtrueuserdomains( \%TRUEDOMAINS );
my @users = sort values %TRUEDOMAINS;
my $success_cnt = 0;
my $success_verify = scalar @users;
for my $user (@users) {
$success_cnt += convert_mysql_roundcube_to_sqlite($user);
}
unless ( $success_cnt == $success_verify ) {
$logger->warn("Roundcube Mysql to sqlite conversion was not completely successful. Please check $log_file for details.");
if ($invoked_as_script) {
exit(1);
}
return;
}
## Conversion is a success, so switch Roundcube configuration to use SQLite before dropping the database.
my $DIR = '/usr/local/cpanel/base/3rdparty';
Cpanel::Email::RoundCube::generate_roundcube_config_sqlite( $DIR, $logger );
## if the conversion was a complete success (as checked above), we can safely archive and
## delete the MySQL roundcube database. This will prevent future problems with transfers,
## and in fact is the final solution for the original Roundcube case 12162!
## already asserted via &init_check
my $archive_success = Cpanel::Email::RoundCube::archive_and_drop_mysql_roundcube($logger);
require Cpanel::Config::CpConfGuard;
my $cpconf_guard = Cpanel::Config::CpConfGuard->new();
$cpconf->{'roundcube_db'} = $cpconf_guard->{'data'}->{'roundcube_db'} = 'sqlite';
$cpconf_guard->save();
if ($invoked_as_script) {
Cpanel::Email::RoundCube::restart_cpsrvd();
exit(0);
}
return 1;
}
sub convert_mysql_roundcube_to_sqlite {
my ($user) = @_;
my @domains = Cpanel::Email::RoundCube::collect_domains($user);
my @uid_name_pairs = collect_roundcube_user_info( \@domains, $user );
## returning 1: no conversion necessary
return 1 unless ( scalar @uid_name_pairs );
## e.g. '/home'
my $best_mnt_point = Cpanel::Filesys::Home::get_homematch_with_most_free_space();
my $tmpdir_root_base = "$best_mnt_point/roundcube_convert";
if ( -d $tmpdir_root_base ) {
rename( $tmpdir_root_base, "$tmpdir_root_base.$time" );
}
mkdir($tmpdir_root_base);
my $tmpdir_root_sys = "$tmpdir_root_base/$user";
mkdir($tmpdir_root_sys);
my @pwinfo = Cpanel::PwCache::getpwnam($user);
my ( $mmuid, $mmgid, $homedir ) = @pwinfo[ 2, 3, 7 ];
my $tmpdir_emailuser_base = "$homedir/tmp/roundcube_convert";
my $success_cnt = 0;
my %has_sqlite_trapper_keeper;
for my $email_user_info (@uid_name_pairs) {
my ( $uid, $email_user ) = @$email_user_info;
if ($mysql_plus_sqlite) {
my $db_path = _roundcube_db_path( $user, $homedir, $email_user );
$has_sqlite_trapper_keeper{$db_path} = -f $db_path;
next if $has_sqlite_trapper_keeper{$db_path};
}
my $tmpdir_root_sys_emailuser = "$tmpdir_root_sys/$email_user";
mkdir($tmpdir_root_sys_emailuser);
do_mysqldumps_for_user( $uid, $tmpdir_root_sys_emailuser );
}
## ensures that $best/rcube/$sysuser/$emailuser is readable by $sysuser,
## so that $sysuser can move it to their $homedir after the setuid
my $tempquota = Cpanel::Quota::Temp->new( user => $user, log => 1 );
$tempquota->disable();
my $rv_chmod = system("chmod -R 700 $tmpdir_root_sys");
my $rv_chown = system("chown -Rf $mmuid:$mmgid $tmpdir_root_sys");
for my $email_user_info (@uid_name_pairs) {
my ( $uid, $email_user ) = @$email_user_info;
if ($mysql_plus_sqlite) {
my $db_path = _roundcube_db_path( $user, $homedir, $email_user );
if ( $has_sqlite_trapper_keeper{$db_path} ) {
$logger->info("$email_user already has an SQLite DB, skipping...");
$success_cnt++;
next;
}
}
chdir($tmpdir_root_base);
my $tmpdir_root_sys_emailuser = "$tmpdir_root_sys/$email_user";
my $rv_convert = xml_to_sqlite( $user, $tmpdir_root_sys_emailuser, $tmpdir_emailuser_base, $email_user, $homedir );
unless ($rv_convert) {
$logger->warn("Conversion was not successful for user '$user'.");
}
$success_cnt += $rv_convert;
## note: no need to "rmdir($tmpdir_root_sys_emailuser)", as this dir was moved
## to $homedir/tmp/rcube/$emailuser (during the setuid)
## SOMEDAY @GOLIVE: remove the mysql roundcube database (steal mysql/roundcube backup/archive
## clauses from update-roundcube-db, the mysql version)
}
$tempquota->restore();
rmdir($tmpdir_root_sys);
rmdir($tmpdir_root_base);
return $success_cnt == scalar(@uid_name_pairs);
}
sub collect_roundcube_user_info {
my ( $ar_domains, $user ) = @_;
my $regexp = '@(' . join( '|', map { s/\./\\./gr } @$ar_domains ) . ')$';
my @ids;
try {
@ids = $dbh->selectall_array( "SELECT user_id, username FROM users WHERE username REGEXP ? or username = ?", {}, $regexp, $user );
};
return @ids;
}
## Stolen from pkgacct; needs to be modularized!
## SOMEDAY: this version has converged; move pkgacct version to module, and get rid of below
sub mysqldumpdb {
my ($args) = @_;
my @options = @{ $args->{'options'} };
my $db = $args->{'db'};
my $table = $args->{'table'};
my $file = $args->{'file'};
my $file_mode = $args->{'append'} ? '>>' : '>';
# for testing
my @extra_options = _get_extra_options();
my $mysqldump = Cpanel::DbUtils::find_mysqldump();
my @db = ($db);
if ($table) {
push @db, $table;
}
my $pid = IPC::Open3::open3( my $w, my $r, '', $mysqldump, @extra_options, @options, @db );
my $first_line = 1;
if ( open( my $fh, $file_mode, $file ) ) {
while (<$r>) {
if ( $first_line && ( !$_ || m/^mysqldump:/ ) ) {
warn join( '.', @db ) . ': ' . $_;
close $w;
close $r;
waitpid( $pid, 0 );
$first_line = 0;
my $mysqlcheck = Cpanel::DbUtils::find_mysqlcheck();
system( $mysqlcheck, '--repair', @extra_options, @db );
$pid = IPC::Open3::open3( $w, $r, '', $mysqldump, @extra_options, @options, @db );
}
else {
print {$fh} $_;
}
}
}
close $w;
close $r;
waitpid( $pid, 0 );
return;
}
sub do_mysqldumps_for_user {
my ( $uid, $tmp_convertdir ) = @_;
for my $tbl (@dumptables) {
## these two tables do not key off user_id
next if grep { $tbl eq $_ } qw{contactgroupmembers events ical_events caldav_events attachments ical_attachments caldav_attachments caldav_calendars};
## XML output, not suppressing table structure, with a where clause on user_id list
my @opts = ( '--xml', '-w', qq{user_id = $uid} );
mysqldumpdb(
{
'options' => [@opts],
'db' => $mysql_dbname,
'file' => "$tmp_convertdir/rcube.$tbl.xml",
'table' => $tbl
}
);
}
## contactgroupmembers is gathered in two steps, otherwise would be a LEFT JOIN that
## &mysqldumpdb can not currently support
my $contacts = _get_ids_from_col( 'contactgroups', 'contactgroup_id', $uid, 'user_id' );
_dump_stuff_in( 'contactgroupmembers', 'contactgroup_id', $contacts, $tmp_convertdir ) if scalar(@$contacts);
# Handle events & attachments for calendaring, as those are identified by
# calendar & event respectively.
foreach my $type (qw{default ical caldav}) {
my $prefix = $type eq 'default' ? '' : "${type}_";
my $cals = _get_ids_from_col( "${prefix}calendars", 'calendar_id', $uid, 'user_id' );
next if !scalar(@$cals);
my @events2export;
my @attachments2export;
foreach my $cal (@$cals) {
my $events = _get_ids_from_col( "${prefix}events", 'event_id', $cal, 'calendar_id' );
# Can't have attachments if no events exist to attach em to, so
# next here is appropriate.
next if !scalar(@$events);
push @events2export, @$events;
my $attachments = _get_ids_from_col( "${prefix}attachments", 'attachment_id', $events, 'event_id' );
next if !scalar(@$attachments);
push @attachments2export, @$attachments;
}
_dump_stuff_in( "${prefix}events", 'event_id', \@events2export, $tmp_convertdir ) if @events2export;
_dump_stuff_in( "${prefix}attachments", 'attachment_id', \@attachments2export, $tmp_convertdir ) if @attachments2export;
}
return undef;
}
# Only here so that I don't "repeat myself".
sub _get_ids_from_col {
my ( $tbl, $col, $search_term, $filter_on, $filter_mode ) = @_;
my @ids;
try {
# Quoting anything other than values here is actually harmful.
# Don't do it as such. Same for IN statement here.
my $where_clause = "WHERE $filter_on = ?";
if ( ref $search_term eq 'ARRAY' ) {
my $count = scalar(@$search_term);
die "Bad call to _get_ids_from_col: no search term specified!" if !$count;
my $qs = ( '?,' x ( $count - 1 ) ) . '?';
$where_clause = "WHERE $filter_on IN ($qs)";
@ids = map { $_->[0] } $dbh->selectall_array( "SELECT $col FROM $tbl $where_clause", {}, @$search_term );
}
else {
@ids = map { $_->[0] } $dbh->selectall_array( "SELECT $col FROM $tbl $where_clause", {}, $search_term );
}
}
catch {
$logger->warn( "Attempt to find necessary data from $col in $tbl failed: " . $dbh->errstr() );
};
return \@ids;
}
# Only here so that I don't "repeat myself".
sub _dump_stuff_in {
my ( $tbl, $col, $in, $tmp_convertdir ) = @_;
my $csv = join( ',', @$in );
mysqldumpdb(
{
'options' => [ '--xml', '-w', qq{$col IN ($csv)} ],
'db' => $mysql_dbname,
'file' => "$tmp_convertdir/rcube.$tbl.xml",
'table' => $tbl,
}
);
return;
}
sub create_rcube_sqlite_tables {
my ($dbh) = @_;
Cpanel::Email::RoundCube::DBI::ensure_schema_update( $dbh, 'sqlite' );
return undef;
}
# This used to do all sorts of things like telling rcube_xml_to_db what needed
# quoting, what needed to be set to NULL, etc. -- thankfully all that is
# absolutely unnecessary with sqlite3.
sub do_all_rcube_xml_to_db {
my ( $dbh, $xmldir ) = @_;
my $success_cnt = 0;
## verify the number of XML docs to convert to SQL
my $success_verify = scalar(@dumptables);
{
## note: there is a disparity in the sqlite.initial.sql, and the "table_structure" for "users";
## preferences is not NULLABLE.
my @_cols = qw(user_id username mail_host created last_login language preferences);
$success_cnt += rcube_xml_to_db( $xmldir, 'users', $dbh, \@_cols );
}
{
my @_cols = qw( identity_id user_id changed del standard name organization email
reply-to bcc signature html_signature);
$success_cnt += rcube_xml_to_db( $xmldir, 'identities', $dbh, \@_cols );
}
{
my @_cols = qw(contact_id user_id changed del name email firstname surname vcard);
$success_cnt += rcube_xml_to_db( $xmldir, 'contacts', $dbh, \@_cols );
}
## new contactgroups table
{
my @_cols = qw( contactgroup_id user_id changed del name );
$success_cnt += rcube_xml_to_db( $xmldir, 'contactgroups', $dbh, \@_cols );
}
## new contactgroupmembers table
## guard clause: contactgroupmembers is gathered conditionally, so it is the only file which
## may not exist
if ( -e _xml_file( $xmldir, 'contactgroupmembers' ) ) {
my @_cols = qw( contactgroup_id contact_id created );
$success_cnt += rcube_xml_to_db( $xmldir, 'contactgroupmembers', $dbh, \@_cols );
}
else {
$success_cnt++;
}
# Restore calendar data
foreach my $cal_tbl (qw{calendars caldav_calendars}) {
unless ( -e _xml_file( $xmldir, $cal_tbl ) ) {
$success_cnt++;
next;
}
my @_cols = qw( calendar_id user_id name color showalarms );
push( @_cols, qw{ical_url ical_user ical_pass ical_last_change} ) if $cal_tbl eq 'ical_calendars';
push( @_cols, qw{readonly caldav_url caldav_tag caldav_user caldav_pass caldav_oauth_provider caldav_last_change} ) if $cal_tbl eq 'caldav_calendars';
$success_cnt += rcube_xml_to_db( $xmldir, $cal_tbl, $dbh, \@_cols );
}
foreach my $evt_tbl (qw{events caldav_events}) {
unless ( -e _xml_file( $xmldir, $evt_tbl ) ) {
$success_cnt++;
next;
}
my @_cols = qw( event_id calendar_id recurrence_id uid instance isexception created changed sequence start end recurrence title description location categories url all_day free_busy priority sensitivity status alarms attendees notifyat );
push( @_cols, qw{ical_url ical_last_change} ) if $evt_tbl eq 'ical_events';
push( @_cols, qw{caldav_url caldav_tag caldav_last_change} ) if $evt_tbl eq 'caldav_events';
$success_cnt += rcube_xml_to_db( $xmldir, $evt_tbl, $dbh, \@_cols );
}
foreach my $atc_tbl (qw{attachments caldav_attachments}) {
unless ( -e _xml_file( $xmldir, $atc_tbl ) ) {
$success_cnt++;
next;
}
my @_cols = qw( attachment_id event_id filename mimetype size data );
$success_cnt += rcube_xml_to_db( $xmldir, $atc_tbl, $dbh, \@_cols );
}
if ( -e _xml_file( $xmldir, 'itipinvitations' ) ) {
my @_cols = qw(token event_uid user_id event expires cancelled);
$success_cnt += rcube_xml_to_db( $xmldir, 'itipinvitations', $dbh, \@_cols );
}
else {
$success_cnt++;
}
## Restore responses table, if available
if ( -e _xml_file( $xmldir, 'responses' ) ) {
my @_cols = qw( response_id user_id name data is_html changed del );
$success_cnt += rcube_xml_to_db( $xmldir, 'responses', $dbh, \@_cols );
}
else {
$success_cnt++;
}
return $success_cnt == $success_verify;
}
sub _xml_file {
my ( $xmldir, $tbl ) = @_;
return "$xmldir/rcube.$tbl.xml";
}
sub rcube_xml_to_db {
my ( $xmldir, $tbl, $dbh, $ar_cols ) = @_;
my $xml_fname = _xml_file( $xmldir, $tbl );
my %common_opts = ( ForceArray => 1, KeyAttr => [], ContentKey => '__content' );
## $ref->{database}->[0]->{table_data}->[0]->{row}->[$x]->{field}->[$x]->{__content}
my $ref;
my $err;
try {
$ref = XML::Simple::XMLin( $xml_fname, %common_opts );
}
catch {
$err = $_;
};
if ($err) {
$logger->warn("Failed to load xml for $tbl: $err");
return;
}
my $rows = $ref->{'database'}->[0]->{'table_data'}->[0]->{'row'};
## FWIW, Perl does not complain if $rows is undef, when called in a for loop context.
for my $row (@$rows) {
# XXX Placeholders don't work on columns, but stuff with dashes
# still needs quoting. As such, quote all col names.
# Sorry, couldn't find a way around it.
my $update_hr = { map { $dbh->quote_identifier( $_->{'name'} ) => $_->{'__content'} || '' } @{ $row->{'field'} } };
my $rv = $dbh->do( rcube_make_sql( $tbl, $update_hr ) );
unless ($rv) {
$logger->warn( "Conversion was not successful: " . $dbh->errstr() );
return;
}
}
return 1;
}
sub rcube_make_sql {
my ( $table, $update_hr ) = @_;
# Lets hope these actually are nonzero
my @values = values(%$update_hr);
my $qs = ( '?,' x ( scalar(@values) - 1 ) ) . '?';
return (
"INSERT OR REPLACE INTO $table (" . join( ",", keys(%$update_hr) ) . ") VALUES ($qs)",
undef,
@values,
);
}
sub check_mysqlup {
## note: this call is cached with a ttl of 600
my $isrunning = Cpanel::MysqlRun::running();
if ($isrunning) {
return 1;
}
return undef;
}
sub ensure_roundcube_tables {
my ($dbh) = @_;
my %tables = map { $_ => undef } $dbh->tables();
# system table is created during a migration
my @expected = qw(session cache system);
push( @expected, @dumptables );
for my $exp (@expected) {
if ( !exists $tables{$exp} && !exists $tables{qq{"main"."$exp"}} ) {
$logger->warn("ERROR: missing $exp table");
return;
}
}
return 1;
}
sub init_check {
my ($cpconf) = @_;
## Ensure root
return unless ( 0 == $> );
unless ( -e $sqlite_table_file ) {
$logger->info("Roundcube is not installed; conversion is irrelevant.");
return;
}
if ( exists $cpconf->{'skiproundcube'} and $cpconf->{'skiproundcube'} ) {
$logger->info("Roundcube should be skipped, bailing out.");
return;
}
my $mysqlup = check_mysqlup();
unless ($mysqlup) {
$logger->warn("Mysql not currently running, bailing out.");
return;
}
## Does this installation use Roundcube/MySQL?
unless ( Cpanel::MysqlUtils::Command::db_exists($mysql_dbname) ) {
$logger->warn("mysql database $mysql_dbname is missing");
return;
}
my $error;
try {
my $dbc = Cpanel::MysqlUtils::Connect->new( database => $mysql_dbname );
$dbh = $dbc->db_handle();
}
catch {
$error = $_;
};
if ($error) {
$logger->warn("Failed to connect to MySQL database '$mysql_dbname'.");
return;
}
return 1;
}
##############################################################
# These functions allow tests to use a temporary MySQL server
sub _get_extra_options_string {
if ( scalar _get_extra_options() ) {
return join( ' ', _get_extra_options() );
}
return;
}
sub _get_extra_options {
return @Cpanel::MysqlUtils::_EXTRA_MYSQL_ARGS;
}
#
##############################################################
sub xml_to_sqlite {
my ( $user, $tmpdir_root, $tmpdir_convert_base, $email_user, $homedir ) = @_;
my $code_ref = _closure_maker( $user, $tmpdir_root, $tmpdir_convert_base, $email_user, $homedir );
#fixup a user who's purposefully deleted their "etc" dir
Cpanel::AccessIds::ReducedPrivileges::call_as_user( sub { File::Path::make_path( File::Basename::dirname( _roundcube_db_path( $user, $homedir, $email_user ) ), { chmod => 0750 } ); }, $user, 'mail' );
my $rv = Cpanel::AccessIds::ReducedPrivileges::call_as_user( $code_ref, $user );
return $rv;
}
sub _roundcube_db_path {
my ( $system_user, $homedir, $mail_account ) = @_;
my ( $mail_user, $mail_domain ) = split( '@', $mail_account );
my $db_path = ( defined $mail_domain ) ? "$homedir/etc/$mail_domain/$mail_user.rcube.db" : "$homedir/etc/$mail_user.rcube.db";
return $db_path;
}
## defining as a closure, as ::run_as_user does not currently handle sub args
sub _closure_maker {
my ( $system_user, $tmpdir_root, $tmpdir_convert_base, $email_user, $homedir ) = @_;
my $code_ref = sub {
my $tmp_convertdir = "$tmpdir_convert_base/$email_user";
if ( -d $tmp_convertdir ) {
rename( $tmp_convertdir, "$tmp_convertdir.$time" );
}
# Copy the XML files over to the user's tempdir.
# Hopefully these are simple renames. But directories may be on different
# mount points requiring a copy.
File::Path::make_path($tmp_convertdir);
for my $table (@dumptables) {
my $src = _xml_file( $tmpdir_root, $table );
my $dest = _xml_file( $tmp_convertdir, $table );
if ( -e $src ) {
File::Copy::move( $src, $dest );
}
}
# Clean up empty source directory.
# This will fail if the above moves failed.
rmdir($tmpdir_root);
## note: similar clause in cpsrvd to set up $ENV{'_RCUBE'}
my ( $mail_user, $mail_domain ) = split( '@', $email_user );
my $rcube_sqlite_loc8 = _roundcube_db_path( $system_user, $homedir, $email_user );
if ( -e $rcube_sqlite_loc8 ) {
rename( $rcube_sqlite_loc8, "$rcube_sqlite_loc8.$time" );
}
my $dbh = DBI->connect( "dbi:SQLite:dbname=$rcube_sqlite_loc8", "", "" ) or do {
$logger->info("Connection error to $rcube_sqlite_loc8: $!");
return;
};
## note: $dbh->{sqlite_version} is confirmed 2.8.15
create_rcube_sqlite_tables($dbh);
return unless ensure_roundcube_tables($dbh);
unless ( do_all_rcube_xml_to_db( $dbh, $tmp_convertdir ) ) {
$logger->info("Conversion of roundcube XML dump to sqlite database failed");
return;
}
$dbh->disconnect();
chmod( 0600, $rcube_sqlite_loc8 );
## the return values on these rm calls are not critical
## only remove the /tmp directory if the XML converion has been successful thus far
try {
File::Path::remove_tree($tmp_convertdir);
};
## attempt to rmdir, which will intentionally fail if any of the converts did not succeed
rmdir($tmpdir_convert_base);
return 1;
};
return $code_ref;
}
1;
Rosenblum TV: Video training, virtual workshops, classes, tutorials
RE-INVENTING THE TELEVISION NEWS BUSINESS*
A revolution in video storytelling
Creating entirely new & cost-effective production methods
From the world leaders in video production training and the creators of Character Driven Storytelling™
*and every other business that uses video
WHAT WE DO
Over the past 35 years, we have designed, built or restructured some of the most powerful news and journalism companies in the world.
We replace the traditional TV news ‘crew’ with one highly trained journalist, working alone with nothing but an iPhone.
No more TV news ‘crews’, no editors and no field producers.
This is television news done the way newspaper journalism is done – one reporter with their electronic pad and pencil.
In doing this, we can cut the cost of production by as much as 75% while increasing ratings and audience engagement.
In the place of conventional TV news ‘packages’ – ie, reporter stand up, interview, b-roll, man on the street, we marry great journalism with Netflix and Hollywood storytelling.
It’s a combination that works.
We have taken most of our clients to #1 in their respective markets.
And it’s not just for news. Any company, any profit, any NGO and anyone else who is online needs to tell their story in compelling yet cost-effective video. We can teach you to do that. Either in person or virtually.
EXAMPLES OF WHAT WE CAN TEACH YOUR STAFF TO PRODUCE
ITAY HOD
Itay Hod, MMJ with KPIX/CBS in San Francisco, took the 5-Day Intensive Video Storytelling Bootcamp in 2018.
Because he works alone, with only an iPhone, he was able to embed himself with a homeless family.
Here’s the story he produced in a one-day turn.
KIET DO
Kiet Do, an MMJ with KPIX/CBS in San Francisco, took the 5-Day Intensive Video Storytelling Bootcamp in 2021.
Here is a story he produced, all on his own, with only an iPhone and in a one-day turn.
TAYLOR SCHAUB
Taylor Schaub, an MMJ with Spectrum News 1 in LA, took the 5-Day Intensive Video Storytelling Bootcamp in 2023.
Here is a story he turned in only one day, using only an iPhone. It was the first video story he ever did and it was nominated for an Emmy.
THE BOOTCAMP
How do we convert stations and whole networks to working in this way?
Since 1988, we have run intensive 5-Day Video Storytelling Bootcamps
We have done these all over the world.
These are hands-on bootcamps, and participants learn an entirely new way of creating TV news stories.
-We shoot at a 3:1 ratio or lower, so turnaround times are fast.
-We go directly from camera to timelilne and edit – no written scripts. We work in the medium of pictures and sound.
-We are entirely character-driven.
-We are driven by pictures and real events.
-We are focused almost entirely on ’the viewer experience’.
Since 1988, more than 70,000 journalists around the world have taken our bootcamps, either in person on virtualy.
We have started to work with CBS News, bringing our ideas of character-driven storytelling to one of the most successful and biggest networks in the United States. Since beginning to work with them ratings have climbed and more importantly, audience engagement is through the ceiling.
We started New York Times Television in 1990 and it was the first paper to be brought into the world of TV. It quickly became one of the most successful non-fiction production companies in the United States. The series and documentaries we produced won many awards including multiple Emmys.
We have been working with the BBC since the year 2000 helping to convert their national news network to our visual storytelling technique. Most recently we have trained teams from their sports, documentaries, and comedy divisions to make character-driven stories using only smartphones.
For the past five years we have worked with Spectrum News to introduce and train their journalists on visual, character driven storytelling using smartphones helping to create a different kind of local news for their network of 24-Hour News Stations across the United States.
In 2006, we were approached by the United Nations. Rather than rely on news outlets, it would be much easier to train the field operatives to produce their own stories. We spent two years working with the UN, training more than 100 of their staff in bootcamps in Geneva and Nairobi.
We trained 50 print reporters at the paper to shoot and tell their own stories, in conjunction with their print work. We built a TV newsroom in their existing print newsroom – you could not ask for a better set and they began to live stream their stories in conjunction with their print work.
We spent two years with McGraw Hill, training more than 150 of their staffers, making them completely video literate. McGraw/Hill media properties we transit included Business Week, Aviation Week, (what was the name of the architecture magazine), and JD Power and Associates.
In 1990, we were approached by The Voice of America, the official broadcasting agency for the United States Government. When we met with VOA, they were only a short wave radio broadcaster, but working with them, we took them into television, launching VOA-TV.
British based Oyster Yachts makes some of the finest yachts in the world. Like every other company, they had to find a way to feed the never-ending video demands of social media – sites like Instagram and TikTok. We trained the Oyster staff to tell their own stories, using only iPhones.
We were approached by SEPA, the Scottish Environmental Protection Agency because they had to continually find a way to ‘feed the media beast’. The result was that SEPA was able to tell their own stories, whenever they wanted, and at almost no additional cost.
Michael Rosenblum has been writing about the media since 1988. His work and ideas have appeared in The Guardian, The Huffington Post, Ilkeston Life and many other publications.
He has been blogging regularly for the past 35 years on this subject. Having taught media studies at Columbia University, NYU and now the University of Oxford, he is considered an expert on this subject.
Continue reading this post or look back at previous posts.