#!/usr/bin/perl -w
#
# Copyright (c) 2020 Adrian Schroeter, SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
#
# optional source publishing process
#

BEGIN {
  my ($wd) = $0 =~ m-(.*)/- ;
  $wd ||= '.';
  unshift @INC,  "$wd";
}

use XML::Structured ':bytes';

use BSConfiguration;
use BSUtil;
use BSRevision;
use BSStdRunner;
use BSSQLite;
use Data::Dumper;

use strict;

my $eventdir = "$BSConfig::bsdir/events";
my $srcrep = "$BSConfig::bsdir/sources";
my $uploaddir = "$srcrep/:upload";

my $rsyncserver = $BSConfig::sourcepublish_sync;

my $maxchild = 1;
$maxchild = $BSConfig::sourcepublish_maxchild if defined $BSConfig::sourcepublish_maxchild;

my $myeventdir = "$eventdir/sourcepublish";
my $sqlitedir = "$BSConfig::bsdir/db/sqlite";

# source publish db init
my $publishedsourcedbfile = "$sqlitedir/publishedsource";
my $publishedsourcedb;

sub init_db {
  mkdir_p($sqlitedir);
  $publishedsourcedb = BSSQLite::connectdb($publishedsourcedbfile);
  my %dbtables = map {$_ => 1} BSSQLite::list_tables($publishedsourcedb);
  if (!$dbtables{'publishedsource'}) {
    BSSQLite::dbdo($publishedsourcedb, <<'EOS');
  CREATE TABLE IF NOT EXISTS publishedsource(
    project TEXT,
    package TEXT,
    srcmd5 TEXT,
    UNIQUE(project,package,srcmd5)
  )
EOS
  }
  BSSQLite::dbdo($publishedsourcedb, 'CREATE INDEX IF NOT EXISTS publishedsource_idx on publishedsource(project,package,srcmd5)');
}

sub linksourceintodir {
  my ($basedir, $projid, $packid, $srcmd5) = @_;

  # get files to send to service daemon
  my $rev;
  my $files;
  eval {
   $rev = BSRevision::getrev_local($projid, $packid, $srcmd5);
   $rev ||= BSRevision::getrev_deleted_srcmd5($projid, $packid, $srcmd5);
   $files = BSRevision::lsrev($rev);
  };
  if ($@) {
    BSUtil::printlog "warning: $projid, $packid, $srcmd5 not found, skipping\n";
    return;
  }

  my $dir = "$basedir/$projid/$packid/$srcmd5";
  mkdir_p($dir);
  for my $filename (sort keys %$files) {
    BSRevision::revcopyoutfile($rev, $filename, $files->{$filename}, "$dir/$filename") unless -e "$dir/$filename";
  }
  return 1;
}

sub qsystem {
  my @args = @_;
  my $pid;
  local (*RH, *WH);
  if ($args[0] eq 'echo') {
    pipe(RH, WH) || die("pipe: $!\n");
  }
  if (!($pid = xfork())) {
    if ($args[0] eq 'echo') {
      close WH;
      open(STDIN, "<&RH");
      close RH;
      splice(@args, 0, 2);
    }
    if ($args[0] eq 'chdir') {
      chdir($args[1]) || die("chdir $args[1]: $!\n");
      splice(@args, 0, 2);
    }
    if ($args[0] eq 'stdout') {
      if ($args[1] ne '') {
        open(STDOUT, '>', $args[1]) || die("$args[1]: $!\n");
      }
      splice(@args, 0, 2);
    } else {
      open(STDOUT, ">/dev/null");
    }
    eval {
      exec(@args);
      die("$args[0]: $!\n");
    };
    warn($@) if $@;
    exit 1;
  }
  if ($args[0] eq 'echo') {
    close RH;
    print WH $args[1];
    close WH;
  }
  waitpid($pid, 0) == $pid || die("waitpid $pid: $!\n");
  return $?;
}

sub add_publishedsource {
  my ($projid, $packid, $srcmd5) = @_;

  my $h = $publishedsourcedb->{'sqlite'} || BSSQLite::connectdb($publishedsourcedbfile);
  BSSQLite::begin_work($h);
  BSSQLite::dbdo_bind($h, 'INSERT OR IGNORE INTO publishedsource(project,package,srcmd5) VALUES(?,?,?)', [ $projid ], [ $packid ], [ $srcmd5 ]);
  BSSQLite::commit($h);
}

sub has_publishedsource {
  my ($projid, $packid, $srcmd5) = @_;

  my $h = $publishedsourcedb->{'sqlite'} || BSSQLite::connectdb($publishedsourcedbfile);
  my @ary = BSSQLite::selectrow($h, "SELECT project,package,srcmd5 FROM publishedsource WHERE project = ? AND package = ? AND srcmd5 = ?", $projid, $packid, $srcmd5);
  return $ary[0] ? 1 : 0;
}

sub syncsource {
  my ($dir, $projid, $packid, $srcmd5) = @_;

  my @rsync_extra_options;
  @rsync_extra_options = split(' ', $BSConfig::rsync_extra_options) if $BSConfig::rsync_extra_options;
  # we can trust our sym links...
  my $server = $rsyncserver;
  my $command = "rsync";
  if ($server =~ /^rsync-ssl:/) {
    $server =~ s/^rsync-ssl:/rsync:/;
    $command = "rsync-ssl";
  }
  qsystem('echo', "$projid/$packid/$srcmd5\0", $command, '-ar0', '--copy-unsafe-links', '--no-munge-links', @rsync_extra_options, '--fuzzy', '--timeout', '7200', '--files-from=-', $dir, $server) && die("    rsync failed at ".localtime(time).": $?\n");

}

sub sourcepublish {
  my ($projid, $packid, $srcmd5, $included) = @_;

  return unless $BSConfig::sourcepublish_sync && $BSConfig::sourcepublish_sync =~ /^rsync(-ssl)?:\//;
  # Filtering by $BSConfig::sourcepublish_filter happens in bs_publish since
  # it must happend by released project name and not build project.

  BSUtil::printlog("source publish event for $projid/$packid $srcmd5");

  # strip multibuild flavor
  $packid =~ s/:.*// if $packid =~ /(?<!^_product)(?<!^_patchinfo):./;

  my $update;
  my $new_entry = !has_publishedsource($projid, $packid, $srcmd5);
  return if !$new_entry && !@{$included || []};		# fast return if there is nothing to do

  # get files to sync
  my $odir = "$uploaddir/sourcepublish$$/";
  BSUtil::cleandir("$odir") if -d $odir;
  linksourceintodir($odir, $projid, $packid, $srcmd5) || die ("unable to link sources for $projid $packid $srcmd5");

  # check for included sources, eg in images
  for my $source (@{$included || []}) {
    my $package = $source->{'package'};
    # strip multibuild flavor
    $package =~ s/:.*// if $package =~ /(?<!^_product)(?<!^_patchinfo):./;
    my $exists = has_publishedsource($source->{'project'}, $package, $source->{'srcmd5'});
    if (!$exists) {
      if (linksourceintodir($odir, $source->{'project'}, $package, $source->{'srcmd5'})) {
        syncsource($odir, $source->{'project'}, $package, $source->{'srcmd5'});
        add_publishedsource($source->{'project'}, $package, $source->{'srcmd5'});
        $exists = 1;
      }
    }
    if ($exists) {
      my $symlink = "$odir/$projid/$packid/$srcmd5/INCLUDED";
      mkdir_p($symlink);
      $symlink .= "/$source->{'project'}::${package}::$source->{'srcmd5'}";
      if ( ! -l $symlink ) {
        symlink("../../../../$source->{'project'}/$package/$source->{'srcmd5'}", $symlink) || die("symlink creation failed for $symlink: $!");
      }
      $update = 1;
    }
  }

  # sync main package
  syncsource($odir, $projid, $packid, $srcmd5) if $update || $new_entry;
  add_publishedsource($projid, $packid, $srcmd5) if $new_entry;

  BSUtil::cleandir($odir);
  rmdir($odir);
}

sub sourcepublishevent {
  my ($req, @args) = @_;
  eval {
    sourcepublish(@args);
  };
  if ($@) {
    # retry in 10 minutes
    BSStdRunner::setdue($req, time() + 10 * 60);
    die($@);
  }
  return 1;
}

sub run {
  my ($conf) = @_;
  init_db();
  BSRunner::run($conf);
}

my $dispatches = [
  'sourcepublish $project $package $srcmd5 $included:*' => \&sourcepublishevent,
];

my $conf = {
  'runname' => 'bs_sourcepublish',
  'run' => \&run,
  'eventdir' => $myeventdir,
  'dispatches' => $dispatches,
  'maxchild' => $maxchild,
  'inprogress' => 1,
};

BSStdRunner::run('sourcepublish', \@ARGV, $conf);
