OpenMAP Suite Code
OpenMAP Suite is a document management suite based on Subversion
Brought to you by:
hoefer
#! /usr/bin/perl
#
# OpenMAP (C) 2012,2013 Oxinia GmbH, Schweiz
# All rights reserved.
#
use strict;
use SVN::Client;
use POSIX;
use Getopt::Long;
use Cwd;
use DateTime;
$SVN::Error::handler = sub {
# override the SVN default error handler
# don't do anything for now
};
my $language = "en";
if ($::ENV{LANG} =~ /de_/) {
$language = "de";
}
my %strings = (
de => {
0 => "OpenMAP verlangt eine reine UTF-8 Umgebung!\nBitte setzen Sie die Umgebungsvariable LANG systemweit\n(beispielsweise in /etc/profile) auf de_CH.UTF-8 oder\neine aehnliche locale (export LANG=de_CH.UTF-8)",
19 => "Eine Instanz von 'openmap sync' laeuft bereits!",
999 => "Starte OpenMAP Sync\n(C) 2012-2013 Oxinia GmbH, http://www.openmap.ch\n",
998 => "OpenMAP Konfiguration",
997 => "Benutzername",
996 => "Passwort",
995 => "Server URL",
994 => "Benutzerangaben ok\n",
993 => "Falsche URL, falsches Passwort oder falscher Benutzername!\n",
1 => "Zuerst Konfiguration erstellen!",
992 => "Konnte das Lockfile nicht erstellen",
2 => "Konnte keine Verbindung zum Server herstellen, oder falsche Kontoinformationen!",
18 => "Lösche Gruppenverzeichnis",
3 => "*** Synchronisiere",
4 => "Räume auf...",
5 => "Führe Datenupdate vom Server durch...",
6 => "Konflikte gefunden und behoben",
7 => "Uebertrage lokale Aenderungen zum Server...",
991 => "Weitere Konflikte existieren. Führe erweiterte Konfliktlösung durch...",
990 => "Problem bei der Synchronisation:\nNicht alle Konflikte konnten automatisch aufgeloest werden!\n\nEin mögliches Vorgehen zur manuellen Konfliktbehebung ist:\n\n 1) Kopieren Sie die von Ihnen geaenderten Daten der\n Laufwerke mit Status 'Konflikt' auf ein anderes Laufwerk\n 2) Fuehren Sie 'openmap wipe' aus\n 3) Kopieren Sie die veränderten Daten erneut in das\n OpenMAP Laufwerk und synchronisieren Sie.\n\n(ENTER zum Beenden)",
8 => "Konflikte übrig, kontaktieren Sie Ihren Support!",
9 => "Synchronisation ok",
16 => "Synchronisiere OpenMAP Kalenderdaten",
17 => "Bei der Synchronisation des Kalenders ist ein Problem aufgetreten!",
899 => "Starte OpenMAP revert/wipe\n(C) 2012-2013 Oxinia GmbH, http://www.openmap.ch\n\n!!! ACHTUNG ACHTUNG ACHTUNG !!!\n\nDieses Programm wird alle Ihre lokalen OpenMAP Drive Daten\nzurücksetzen. Nicht synchronisierte Aenderungen auf Ihrem Gerät\nwerden dabei verloren gehen. Wir empfehlen dringend, dass Sie\nzuvor eine Kopie der Daten auf ein anderes Laufwerk machen.\n\nDas OpenMAP Drive darf bei diesem Vogang nicht verbunden\nsein!\n",
898 => "Möchten Sie fortfahren und alle OpenMAP Daten rücksetzen? (JA/NEIN) ",
897 => "Bitte JA oder NEIN eingeben",
896 => "Setze alle Daten zurueck. Bitte warten...",
895 => "Vorgang abgebrochen.",
894 => "Mount ist bereits aktiv. Bitte vorher 'openmap unmount' ausführen",
20 => "Abbruch durch Benutzer",
10 => "Konfliktdatei",
13 => "Konflikt(e)!",
14 => "Die Konfigurationsdatei konnte nicht geladen werden",
21 => "Geschichte von",
23 => "Sie müssen als erstes Argument eine gültige Datei angeben!",
24 => "Sie müssen als zweites Argument eine Revisionsnummer oder ein Datum im Format 'yyyy-mm-ddThh:mm:ss' angeben!",
25 => "Diese Funktion kann nicht mit einem Verzeichnis ausgeführt werden!",
26 => "Unbekannter 'tag' Unterbefehl!",
27 => "Falsches tag-Format ([-a-zA-Z0-9_]{1,32})",
28 => "Datei existiert nicht"
},
en => {
0 => "OpenMAP expects an UTF-8 environment!\nPlease set the environment variable LANG to en_US.UTF-8\nor something similar (e.g. 'export LANG=de_CH.UTF-8' in /etc/profile)",
19 => "An instance of 'openmap sync' is already running!",
999 => "Starting OpenMAP Sync\n(C) 2012-2013 Oxinia GmbH, http://www.openmap.ch\n",
998 => "OpenMAP Configuration",
997 => "Username",
996 => "Password",
995 => "Server URL",
994 => "Credentials ok",
993 => "Wrong credentials!\n",
1 => "Create configuration first!",
992 => "Could not create lock file",
2 => "No connection to server, or wrong account credentials",
18 => "Deleting group directory",
3 => "Synchronizing",
4 => "Cleaning up...",
5 => "Getting update from server",
6 => "Conflicts found and resolved",
7 => "Committing changes to server",
991 => "More conflicts found. Resolving...",
990 => "Error during synchronisation:\nNot all conflicts could be resolved automatically!\n\nA possible solution might be:\n\n 1) Copy any files you changed somwhere\n\n save.\n\n 2) Execute 'openmap wipe'\n\n 3) Copy back the changed data onto the OpenMAP drive and synchronize\n\n(ENTER to quit)",
8 => "Conflicts remaining, please contact support",
9 => "Synchronisation ok",
16 => "Synchronizing OpenMAP calendar",
17 => "Calendar synchronisation problem occurred!",
899 => "Starting OpenMAP revert/wipe\n(C) 2012-2013 Oxinia GmbH, http://www.openmap.ch\n\n!!!DANGER DANGER DANGER!!!\n\nThis program will reset all of your local OpenMAP drive data.\nAny data that has not been synchronised will be lost.\nBefore you proceed, save any data to another drive.\nWhen proceeding, the OpenMAP drive must not be opened!\n",
898 => "Do you want to proceed and reset any data? (YES/NO) ",
897 => "Please enter YES or NO",
896 => "Resetting data. Please wait...",
895 => "Cancelled",
894 => "Mount is already active (try 'openmap unmount' first)!",
20 => "Cancelled by user",
10 => "Conflicting file",
13 => "One or more conflicts detected",
14 => "Configuration file does not exist or is not readable",
21 => "History of",
23 => "You need to supply a valid filename as first argument!",
24 => "The second argument must be a valid revision number or date in the form 'yyyy-mm-ddThh:mm:ss'!",
25 => "Can't apply this function to a directory!",
26 => "Unknown 'tagging' subcommand!",
27 => "invalid tag format ([-a-zA-Z0-9_]{1,32})",
28 => "File does not exist"
}
);
sub get_lang {
my $id = shift;
if (defined($strings{$language}->{$id})) {
if ($::opt_machine) {
return sprintf("[%03d] %s", $id, $strings{$language}->{$id});
}
else {
return $strings{$language}->{$id};
}
}
else {
if ($::opt_machine) {
return sprintf("[%03d]", $id);
}
else {
return "";
}
}
}
$| = 1; # autoflush on
$::username = "";
$::password = "";
$::serverurl = "https://openmap.ch"; # the default
$::checkssl = 1; # check SSL certificate
$::opt_machine = 0;
$::recursive = 0;
GetOptions('m' => \$::opt_machine, 'a' => \$::opt_all, 'r' => \$::recursive);
my $command = $ARGV[0];
my $home = $::ENV{HOME};
# if this is a Windows network path, use a /home/ path directly (probably a domain computer then)
if ($home =~ /^\/\//) {
$home = "/home/".$::ENV{USER};
}
my $basepath = $home."/.openmap";
my $mountpath = $home."/openmap";
if (! -d $basepath) {
mkdir $basepath;
}
chmod 0700, $basepath;
if ($command eq "sync") {
$::human = 1;
$::cancelall = 0;
$::SIG{TERM} = sub {
$::cancelall = 1;
};
# make sure there is only one sync process running
$::pidfile = $basepath."/.syncpid";
if ($::opt_machine) {
$::human = 0;
}
if ($::ENV{LANG} !~ /UTF-8$/) {
if ($::human) {
print get_lang(0)."\n";
}
else {
openmap_die(get_lang(0)."\n");
}
}
my $pid = getpid();
if (defined($pid)) {
if ($::human) {
print get_lang(19)."\n";
exit(0);
}
else {
openmap_die(get_lang(19)."\n");
}
}
else {
if (open my $fd, ">".$::pidfile) {
print $fd $$;
close $fd;
}
}
if ($::human) {
print get_lang(999);
}
if (! -e ${basepath}."/.config") {
if ($::human) {
while ( ! -e ${basepath}."/.config" ) {
if (! -e $basepath) {
mkdir $basepath;
}
print get_lang(998)."\n";
print " =====================\n\n";
print get_lang(995)." : ";
my $serverurl = <STDIN>;
chomp $serverurl;
print get_lang(997)." : ";
my $username = <STDIN>;
chomp $username;
print get_lang(996)." : ";
my $password = <STDIN>;
chomp $password;
if (length($serverurl) == 0) {
$serverurl = $::serverurl;
}
#if (!system("svn --trust-server-cert --non-interactive --username '$username' --password '$password' list https://www.openmap.ch/svn/repos/test \&> /dev/null")) {
if (!system("svn --trust-server-cert --non-interactive --username '$username' --password '$password' list ${serverurl}/svn/repos/om_home \&> /dev/null")) {
print "\n".get_lang(994)."\n";
if (open my $fd, ">".${basepath}."/.config") {
print $fd "serverurl = $serverurl\n";
print $fd "username = $username\n";
print $fd "password = $password\n";
}
else {
print "\n".get_lang(993)."\n";
}
}
}
}
else {
openmap_die(get_lang(1)."\n");
}
}
# the settings
my %settings;
load_configuration(\%settings);
#my $groupname = $settings{groupname};
$::username = $settings{username};
$::password = $settings{password};
# if no server URL was specified, take the default one at Oxinia
if (length($settings{serverurl}) > 0) {
$::serverurl = $settings{serverurl};
}
else {
# STHTODO: this is a legacy fix for existing installations as of OpenMAP 1.3
# these old installations use www.openmap.ch as server address for already checked out data
# needs to be resolved by eliminating old client installations/configurations
$::checkssl = 0;
}
if (length($::username) == 0) {
exit(2);
}
# nothing to be configured below this line!
my $datapath = $basepath."/data";
my $lockfd;
my $commitok = 1;
if (!open $lockfd, ">".$basepath."/openmap.lock") {
openmap_die(get_lang(992)."\n");
}
if (! -d $datapath) {
mkdir $datapath;
}
my @groups;
if (open my $fd, "wget '$::serverurl/administration/getgroups.php?username=$::username&psk=sfkj27SDFmmcne42jsdfk' --no-check-certificate -O - -q |") {
my $groups = <$fd>;
close $fd;
chomp $groups;
@groups = split ",", $groups;
}
if (@groups == 0) {
if ($::human) {
openmap_die(get_lang(2)."\n");
}
else {
openmap_die(get_lang(2)."\n");
}
}
# first of all, remove the repositories that are not in the groups list.
if (opendir my $dirfd, $datapath) {
my @repentries = readdir $dirfd;
closedir $dirfd;
foreach my $repentry (@repentries) {
if ($repentry !~ /^\./) {
if ($repentry ne "home") { # never delete the home drives
if (-d $datapath."/".$repentry) {
my $found = 0;
foreach my $groupentry (@groups) {
if ($groupentry eq $repentry) {
$found = 1;
}
}
if (!$found) {
if ($::human) {
print get_lang(18)." $repentry!\n";
}
else {
print "[018] $repentry\n";
}
system("rm", "-rf", ${datapath}."/".$repentry);
}
}
}
}
}
}
foreach my $groupname (@groups) {
my $url = "$::serverurl/svn/repos/om_${groupname}/trunk";
if ($::human) {
print get_lang(3)." $groupname\n";
}
else {
print "[003] $groupname\n";
}
if (-d $datapath."/".$groupname) {
print get_lang(4)."\n";
cleanup($datapath."/".$groupname);
}
print get_lang(5)."\n";
if (update($datapath."/".$groupname, $url, $::username, $::password)) {
if (handle_conflicts($datapath."/".$groupname)) {
print get_lang(6)."\n";
print get_lang(5)."\n";
update($datapath."/".$groupname, $url, $::username, $::password);
}
print get_lang(7)."\n";
if (!commit($datapath."/".$groupname)) {
if ($::human) {
print get_lang(991)."\n";
}
handle_conflicts($datapath."/".$groupname, 1);
print get_lang(6)."\n";
print get_lang(5)."\n";
update($datapath."/".$groupname, $url, $::username, $::password);
print get_lang(7)."\n";
if (!commit($datapath."/".$groupname)) {
$commitok = 0;
}
}
}
else {
openmap_die(get_lang(2)."\n");
}
if (!$commitok) {
if ($::human) {
print get_lang(990);
<STDIN>;
}
else {
print get_lang(8)."\n";
}
}
else {
print get_lang(9)."\n";
}
}
# now synchronize the calendar data
my $calendardir = $home."/.openmap/data/home/".$settings{username}."/.blync";
if (-d $calendardir) {
my @entries;
if (opendir my $fd, $calendardir) {
@entries = readdir $fd;
closedir $fd;
}
if (@entries > 2) {
print get_lang(16)."\n";
if (!synchronize_calendar($calendardir)) {
print get_lang(17)."\n";
}
}
# remove old dates
prunecal($calendardir."/calendar.ics", 100);
}
if (-e $::pidfile) {
unlink($::pidfile);
}
}
elsif ($command eq "revert") {
$::human = 1;
if ($::opt_machine) {
$::human = 0;
}
if ($::human) {
print get_lang(899)."\n";
}
my $decision = 1;
if ($::human) {
$decision = 0;
}
while (!$decision) {
print get_lang(898);
my $answer = <STDIN>;
chomp $answer;
if ($answer eq "JA" || $answer eq "YES") {
$decision = 1;
}
elsif ($answer eq "NEIN" || $answer eq "NO") {
$decision = -1;
}
else {
print get_lang(897)."\n";
}
}
if ($decision == 1) {
if ($::human) {
print get_lang(896)."\n";
}
my $path = $home."/.openmap/data";
if (opendir my $fd, $path) {
my @entries = readdir $fd;
closedir $fd;
foreach my $entry (@entries) {
if ($entry !~ /^\./) {
_svnctx()->revert($path."/".$entry, 1);
my $statuscallback = sub {
my $path = shift;
my $status = shift;
if ($status->text_status() == $SVN::Wc::Status::unversioned) {
system("/bin/rm -rf $path");
}
};
_svnctx()->status($path."/".$entry, 'HEAD', $statuscallback, 1, 0, 0, 0);
}
}
}
}
else {
if ($::human) {
print get_lang(895)."\n";
sleep(5);
}
exit(0);
}
}
elsif ($command eq "prunecal") {
my %settings;
$::human = !$::opt_machine;
load_configuration(\%settings);
if (!defined($settings{username})) {
if ($::human) {
print "No credentials defined!\n";
}
exit(2);
}
my $calendarfile = $home."/.openmap/data/home/".$settings{username}."/.blync/calendar.ics";
if (-e $calendarfile) {
if ($::human) {
print "Pruning $calendarfile...\n";
}
if ($ARGV[1] !~ /^\d+$/) {
if ($::human) {
print "Please specify (in number of days) how old an entry may be to be kept\n";
}
exit(1);
}
else {
prunecal($calendarfile, $ARGV[1]);
}
}
else {
if ($::human) {
print "No calendar file found.\n";
exit(0);
}
}
}
elsif ($command eq "mount") {
if (is_linux()) {
my $waiting_period = 3600;
my $srcdir = $::ENV{HOME}."/.openmap/data";
my $mntdir = $mountpath;
my $pidfile = $::ENV{HOME}."/.openmap/.mntpid";
if (-e $pidfile) {
my $pid;
if (open my $fd, $pidfile) {
$pid = <$fd>;
close $fd;
}
if (defined($pid)) {
chomp $pid;
if (!kill 0, $pid) {
die get_lang(894)."\n";
}
}
unlink($pidfile);
}
# now update the root certificates
if (-e '/etc/openmap/ca.pem') {
update_root_certificates('/etc/openmap/ca.pem', $::ENV{HOME}."/.thunderbird", $::ENV{HOME}."/.mozilla/firefox");
}
if (!fork()) {
setsid();
$::SIG{TERM} = sub {
system("openmap", "cancel", "&>", "/dev/null");
exit(0);
};
if (open my $fd, ">".$pidfile) {
print $fd $$;
close $fd;
}
while (1) {
sleep($waiting_period); # wait for 1 hour
my $logfile = $::ENV{HOME}."/.openmap/lastsync.log";
system("openmap sync &> '$logfile'");
}
exit(0);
}
if (opendir my $fd, $srcdir) {
my @entries = readdir $fd;
closedir $fd;
foreach my $entry (@entries) {
if ($entry !~ /^\./) {
system("svn", "cleanup", $srcdir."/".$entry);
}
}
}
if (! -d $mntdir) {
mkdir $mntdir;
}
system("openmapdrive", $srcdir, $mntdir);
}
}
elsif ($command eq "unmount") {
if (is_linux()) {
my $mntdir = $mountpath;
my $pidfile = $::ENV{HOME}."/.openmap/.mntpid";
if (-e $pidfile) {
my $pid;
if (open my $fd, $pidfile) {
$pid = <$fd>;
close $fd;
}
if (defined($pid)) {
chomp $pid;
system("openmap", "cancel");
kill 'TERM', $pid;
}
unlink($pidfile);
}
system("fusermount", "-u", $mntdir);
}
}
elsif ($command eq "status") {
my $baselength;
my $all = 0;
if ($::opt_all) {
$all = 1;
}
my $statuscallback = sub {
my $path = shift;
my $status = shift;
print substr($path,$baselength)."\n";
};
$baselength = length($basepath."/data");
# the settings
my %settings;
load_configuration(\%settings);
$::username = $settings{username};
$::password = $settings{password};
if (-d $basepath."/data") {
if (opendir my $dirfd, $basepath."/data") {
my @entries = readdir $dirfd;
closedir $dirfd;
foreach my $entry (@entries) {
if ($entry !~ /^\./) {
my $wcpath = $basepath."/data/".$entry;
_svnctx()->status($wcpath, 'HEAD', $statuscallback, 1, 0, $all, 0);
}
}
}
}
}
elsif ($command eq "wipe") {
$::human = 1;
if ($::opt_machine) {
$::human = 0;
}
if ($::human) {
print get_lang(899);
}
my $decision = 1;
if ($::human) {
$decision = 0;
}
while (!$decision) {
print get_lang(898);
my $answer = <STDIN>;
chomp $answer;
if ($answer eq "JA" || $answer eq "YES") {
$decision = 1;
}
elsif ($answer eq "NEIN" || $answer eq "NO") {
$decision = -1;
}
else {
print get_lang(897)."\n";
}
}
if ($decision == 1) {
if ($::human) {
print get_lang(896)."\n";
}
my $path = $home."/.openmap/data";
system("/bin/rm -rf '$path'");
}
else {
if ($::human) {
print get_lang(895)."\n";
sleep(5);
}
exit(0);
}
}
elsif ($command eq "cancel") {
# cancel any running sync process
my $pid = getpid();
if (defined($pid)) {
kill 'TERM', $pid;
}
exit(0);
}
elsif ($command eq "log") {
for (my $i = 1; $i < @ARGV; $i++) { # $ARGV[0] contains the command
do_log($ARGV[$i]);
}
}
elsif ($command eq "cat") {
my $file = $ARGV[1];
my $revision = $ARGV[2];
do_cat($file, $revision);
}
elsif ($command eq "tag") {
my $tagcommand = $ARGV[1];
my @arguments;
for (my $i = 2; $i < @ARGV; $i++) {
push @arguments, ($ARGV[$i]);
}
do_tag($tagcommand, @arguments);
}
else {
print "Usage: openmap <command> [options]\n";
print " where <command> is one of:\n";
print " sync\n";
print " revert\n";
print " mount\n";
print " unmount\n";
print " status\n";
print " wipe\n";
print " cancel\n";
print " prunecal\n";
print " log <filename>\n";
print " cat <filename> <revision|yyyy-mm-ddThh:mm:ss>\n";
print " tag <set|get|find|known> [<arguments...>]\n";
exit(1);
}
exit(0);
sub compute_real_path {
my $path = shift;
# STHTODO: make this work on Windows!
# STHTODO: check whether the path is safe ('..' etc.)
if ($path =~ /^\//) { # an absolute path
my $real_path = $basepath."/data".$path;
if (-e $real_path) {
return $real_path;
}
}
else { # a relative path
my $cwd = cwd();
if (substr($cwd, 0, length($mountpath)) eq $mountpath) {
my $real_path = $basepath."/data".substr($cwd, length($mountpath))."/".$path;
if (-e $real_path) {
return $real_path;
}
}
}
return undef;
}
sub do_cat {
my $file = shift;
my $revision = shift;
if (!defined($file)) {
die(get_lang(23)."\n");
}
my $path = compute_real_path($file);
if (!defined($path)) {
die (get_lang(23)."\n");
}
if (-d $path) {
die(get_lang(25)."\n");
}
if (!defined($revision)) {
die(get_lang(24)."\n");
}
if ($revision !~ /^\d+$/) {
if ($revision !~ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})$/) {
die(get_lang(24)."\n");
}
else {
$revision = "{".$revision."}";
}
}
my %settings;
load_configuration(\%settings);
$::username = $settings{username};
$::password = $settings{password};
_svnctx()->cat(\*STDOUT, $path, $revision);
}
sub valid_tag {
my $tag = shift;
if ($tag =~ /^[-_a-zA-Z0-9,]+$/) {
return 1;
}
return 0;
}
sub do_tag {
my $command = shift;
my @arguments = @_;
if ($command eq "set") {
my $tag = shift @arguments;
if (!valid_tag($tag)) {
die(get_lang(27).": ".$tag."\n");
}
foreach my $entry (@arguments) {
my $file = compute_real_path($entry);
if (-e $file) {
set_tag($file, $tag);
}
else {
print (get_lang(28).": ".$entry."\n");
}
}
}
elsif ($command eq "get") {
my $file = compute_real_path($arguments[0]);
if (-e $file) {
print get_tag($file,0)."\n";
}
else {
print (get_lang(28).": ".$arguments[0]."\n");
}
}
elsif ($command eq "find") {
my $tags = shift @arguments;
my @tags = split ",", $tags;
foreach my $tag (@tags) {
if (!valid_tag($tag)) {
die (get_lang(27).": ".$tag."\n");
}
}
if (@arguments == 0) {
if (opendir my $dir, $basepath."/data") {
my @entries = readdir $dir;
closedir $dir;
foreach my $entry (@entries) {
push @arguments, ("/".$entry);
}
}
}
foreach my $file (@arguments) {
my $path = compute_real_path($file);
if (-e $path) {
find_tags_recursively(\@tags, $path);
}
}
}
elsif ($command eq "known") {
if (opendir my $dir, $basepath."/data") {
my @entries = readdir $dir;
closedir $dir;
my %tags;
foreach my $entry (@entries) {
if ($entry =~ /^[a-zA-Z]/ && -d $basepath."/data/".$entry && -e $basepath."/data/".$entry."/.svn") {
collect_tags_recursively(\%tags, $basepath."/data/".$entry);
}
}
foreach my $tag (keys(%tags)) {
print $tag."\n";
}
}
}
else {
die(get_lang(26)."\n");
}
}
sub find_tags_recursively {
my $tags = shift;
my $path = shift;
if (-e $path) {
my $proplist = get_tag($path, 1);
foreach my $props (@$proplist) {
my $tag = $props->prop_hash()->{omtags};
if (defined($tag) && length($tag) > 0) {
my @tags = split ",", $tag;
my $found = 1;
foreach my $t (@$tags) {
if ($found) {
my $localfound = 0;
foreach my $reft (@tags) {
if ($t eq $reft) {
$localfound = 1;
}
}
if (!$localfound) {
$found = 0;
}
}
else {
last;
}
}
if ($found) {
print substr($props->node_name(), length($basepath."/data"))."\n";
}
}
}
}
}
sub collect_tags_recursively {
my $tags = shift;
my $path = shift;
my $proplist = get_tag($path, 1);
foreach my $props (@$proplist) {
my $tag = $props->prop_hash()->{omtags};
if (defined($tag) && length($tag) > 0) {
my @tags = split ",", $tag;
foreach my $t (@tags) {
$tags->{$t} = 1;
}
}
}
}
sub get_tag {
my $file = shift;
my $recurse = shift;
if (!$recurse) {
my $props = _svnctx()->proplist($file, undef, 0);
if (@$props > 0) {
my $properties = $props->[0]->prop_hash();
if (defined($properties)) {
if (defined($properties->{omtags})) {
return $properties->{omtags};
}
else {
return "";
}
}
}
}
else {
return _svnctx()->proplist($file, undef, 1);
}
}
sub set_tag {
my $file = shift;
my $tag = shift;
if (-e $file) {
_svnctx()->propset("omtags", $tag, $file, $::recursive);
}
}
sub do_log {
my $file = shift;
my %settings;
load_configuration(\%settings);
$::username = $settings{username};
$::password = $settings{password};
if (!$::opt_machine) {
print get_lang(21)." ".$file.":\n";
}
else {
print "[021]".$file."\n";
}
my $path = compute_real_path($file);
if (defined($path)) {
my $svnctx = _svnctx();
my $log_msg = sub {
my $changed_paths = shift;
my $revision = shift;
my $author = shift;
my $date = shift;
my $message = shift;
my $pool = shift;
if (!defined($author)) {
$author = "N/A";
}
if (!defined($date)) {
$date = 0;
}
else {
#2013-10-17T14:43:56.489383Z
if ($date =~/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/) {
my $datetime = DateTime->new(year => $1,
month => $2,
day => $3,
hour => $4,
minute => $5,
second => $6);
$date = $datetime->epoch();
}
}
if (!defined($message)) {
$message = "";
}
if (!$::opt_machine) {
print " ".$revision.": ".$author.", ".localtime($date)."\n";
}
else {
print "[022]${revision}:${author}:${date}\n";
}
};
#$svnctx->log_msg($log_msg);
$svnctx->log([$path], 1, 'HEAD', 0, 0, $log_msg);
}
}
sub openmap_die {
my $text = shift;
my $pid = getpid();
if (defined($pid)) {
if ($pid == $$) {
unlink($::pidfile);
}
}
die $text;
}
sub getpid {
my $pid;
if (-e $::pidfile) {
if (open my $fd, $::pidfile) {
my $temppid = <$fd>;
close $fd;
if ($temppid =~ /\d+/) {
if (kill 0, ($temppid)) {
$pid = $temppid;
}
}
else {
unlink($::pidfile);
}
}
}
return $pid;
}
sub synchronize_calendar {
my $blynchome = shift;
if (-d $blynchome) {
# Search files to be merged
my $filename1 = 'calendar.ics';
my $filename2pattern = 'calendar-.*\.ics';
my $filename2;
my $file1found = 0;
my $file2found = 0;
my $dirfd;
if (opendir($dirfd, $blynchome)) {
my @files = readdir($dirfd);
close $dirfd;
foreach (@files) {
if ($_ eq $filename1) {
$file1found = 1;
}
elsif ($_ =~ $filename2pattern) {
$filename2 = $_;
$file2found = 1;
}
}
if (!$file1found) {
return 1;
}
if (!$file2found) {
return 1;
}
my @calendar;
my %components;
my @uids;
# read input calendars
parse_ics($blynchome."/".$filename1, 1, \@calendar, \%components, \@uids);
parse_ics($blynchome."/".$filename2, 0, \@calendar, \%components, \@uids);
# write merged calendar
if (open (my $outfd, ">".$blynchome."/".$filename1)) {
for (my $count = 0; $count < @calendar - 1; $count++) {
print $outfd $calendar[$count]."\n";
}
foreach (@uids) {
my @component = @{$components{$_}};
foreach (@component) {
print $outfd $_."\n";
}
}
print $outfd "END:VCALENDAR\n";
close ($outfd);
#unlink($filename2);
_delete($blynchome."/".$filename2);
return 1;
}
else {
return 0;
}
}
else {
return 0;
}
}
else {
return 1;
}
}
sub parse_ics {
my $filename = shift;
my $read_calendar = shift;
my $calendar = shift;
my $components = shift;
my $uids = shift;
my @component = ();
my $uid = "";
my $uid_index = -1;
my $is_component = 0;
if (open (my $fdin, $filename)) {
while (<$fdin>) {
chomp;
my @entry = split(":", $_, 2);
if ($entry[0] eq "BEGIN" &&
($entry[1] eq "VEVENT\r" ||
$entry[1] eq "VTODO\r" ||
$entry[1] eq "VJOURNAL\r" ||
$entry[1] eq "VFREEBUSY\r")) {
$is_component = 1;
$uid = "";
$uid_index = -1;
}
if (!$is_component) {
if ($read_calendar) {
push(@$calendar, $_);
}
}
else {
my $index = push(@component, @entry[0].":".@entry[1]) - 1;
if ($entry[0] eq "UID") {
$uid = $entry[1];
$uid_index = $index;
}
if ($entry[0] eq "END") {
my $ignore = 0;
if (exists($components->{$uid})) {
if (arrays_equal([@component], $components->{$uid})) {
$ignore = 1;
}
else {
my $counter = 1;
do {
$uid = "ch.openmap.".$counter++;
} while (exists($components->{$uid}));
$component[$uid_index] = "UID:$uid";
}
}
if (!$ignore) {
$components->{$uid} = [@component];
push(@$uids, $uid);
}
$is_component = 0;
@component = ();
}
}
}
close ($fdin);
}
}
sub arrays_equal {
my ($xref, $yref) = @_;
return unless @$xref == @$yref;
for (my $count = 0; $count < @$xref; $count++) {
return unless $xref->[$count] eq $yref->[$count];
}
return 1;
}
sub update {
my $path = shift;
my $url = shift;
my $username = shift;
my $password = shift;
my $ok = eval {
if (!-d $path) {
# checkout
# flock $lockfd, 2;
_svnctx()->checkout(${url}, ${path}, 'HEAD', 1);
# flock $lockfd, 8;
}
else {
# update
# flock $lockfd, 2;
_svnctx()->update(${path}, 'HEAD', 1);
# flock $lockfd, 8;
}
};
if (!$ok) {
if ($::cancelall) {
print get_lang(20)."\n";
}
}
return $ok;
}
sub _svnctx {
my $authprovider = sub {
my $cred = shift;
my $realm = shift;
my $default_username = shift;
my $may_save = shift;
my $pool = shift;
$cred->username($::username);
$cred->password($::password);
};
my $sslprovider;
if ($::checkssl) {
$sslprovider = sub {
my $cred = shift;
my $realm = shift;
my $failure = shift;
my $certinfo = shift;
my $maysave = shift;
my $pool = shift;
#print $realm."\n";
#print $failure."\n";
#print $certinfo."\n";
#print "---\n";
#print $SVN::Auth::SSL::NOTYETVALID."\n";
#print $SVN::Auth::SSL::EXPIRED."\n";
#print $SVN::Auth::SSL::CNMISMATCH."\n";
#print $SVN::Auth::SSL::UNKNOWNCA."\n";
#print $SVN::Auth::SSL::OTHER."\n";
$cred->accepted_failures(0);
}
}
else {
$sslprovider = sub {
my $cred = shift;
my $realm = shift;
my $failure = shift;
my $certinfo = shift;
my $maysave = shift;
my $pool = shift;
$cred->accepted_failures($failure);
};
}
my $config = SVN::Core::config_get_config(undef);
$config->{'config'}->set("miscellany", "use-commit-times", "yes");
$config->{'servers'}->set("global", "ssl-authority-files", "/etc/openmap/ca.pem");
#print $config->{'config'}->get("miscellany", "use-commit-times", "no")."\n";
my $cancel = sub {
if ($::cancelall) {
print get_lang(20)."\n";
}
else {
return 0;
}
};
my $svnctx = new SVN::Client (
auth => [SVN::Client::get_simple_prompt_provider($authprovider, 2), SVN::Client::get_ssl_server_trust_prompt_provider($sslprovider)],
config => $config,
cancel => $cancel
);
return $svnctx;
}
sub cleanup {
my $datapath = shift;
# flock $lockfd, 2;
_svnctx()->cleanup(${datapath});
# flock $lockfd, 8;
}
sub get_status {
my $status = shift;
my %status_text = ($SVN::Wc::Status::none => 'none (Does not exist)',
$SVN::Wc::Status::unversioned => 'unversioned (Is not a versioned node in this working copy)',
$SVN::Wc::Status::normal => 'normal (Exists, but uninteresting)',
$SVN::Wc::Status::added => 'added (Is scheduled for addition)',
$SVN::Wc::Status::missing => 'missing (Under version control but missing)',
$SVN::Wc::Status::deleted => 'deleted (Scheduled for deletion)',
$SVN::Wc::Status::replaced => 'replaced (Was deleted and then re-added)',
$SVN::Wc::Status::modified => 'modified (Text or props have been modified)',
$SVN::Wc::Status::merged => 'merged (Local mods received repos mods)',
$SVN::Wc::Status::conflicted => 'conflicted (Local mods received conflicting mods)',
$SVN::Wc::Status::ignored => 'ignored (A node marked as ignored)',
$SVN::Wc::Status::obstructed => 'obstructed (An unversioned resource is in the way of the versioned resource)',
$SVN::Wc::Status::external => 'external (An unversioned path populated by an svn:externals property)',
$SVN::Wc::Status::incomplete => 'incomplete (A directory doesn\'t contain a complete entries list)');
return $status_text{$status->text_status()}."/".$status_text{$status->repos_text_status()};
}
sub prunecal {
my $calendarfile = shift;
my $numdays = shift;
if (open my $fd, $calendarfile) {
my $inevent = 0;
my $output = "";
my $thisevent = "";
my $keepevent = 0;
my $date = time()-24*3600*$numdays;
my @date = localtime($date);
my $lowestdate = sprintf("%4d%02d%02d",($date[5]+1900),($date[4]+1),$date[3]);
#if ($::human) {
# print "Lowest date is $lowestdate\n";
#}
while (my $line = <$fd>) {
if ($line =~ /^BEGIN:VEVENT/) {
$inevent = 1;
$thisevent .= $line;
}
elsif ($line =~ /^END:VEVENT/) {
$inevent = 0;
if ($keepevent) {
$thisevent .= $line;
$output .= $thisevent;
}
$thisevent = "";
$keepevent = 0;
}
else {
if ($inevent) {
$thisevent .= $line;
if ($line =~ /^DTSTART/) {
if ($line =~ /(20\d{6})/) {
if ($1 >= $lowestdate) {
$keepevent = 1;
}
}
}
}
else {
$output .= $line;
}
}
}
close $fd;
if (open $fd, ">".$calendarfile) {
print $fd $output;
close $fd;
}
}
}
sub handle_conflicts {
my $wcpath = shift;
my $commitdone = shift;
my $conflictfound = 0;
my $statuscallback = sub {
my $path = shift;
my $status = shift;
if ($status->text_status() == $SVN::Wc::Status::conflicted) {
$conflictfound = 1;
if ($::human) {
print " * ".get_lang(10).": $path (".get_status($status).")\n";
}
else {
print "[010] $path\n";
}
# create a recursive copy of this whole directory under a new name
# revert the current directory
my $newpath = get_unique_name($path, $status->entry()->revision());
_copy($path, $newpath);
_revert($path);
}
elsif ($commitdone && ($status->text_status() == $SVN::Wc::Status::added)) {
$conflictfound = 1;
if ($::human) {
print " * ".get_lang(10).": $path (".get_status($status).")\n";
}
else {
print "[010] $path\n";
}
# the file was deleted in the repository and changed locally,
# so we basically will have to re-add it.
my $newpath = get_unique_name($path, $status->entry()->revision());
_move($path, $newpath);
_revert($path);
_move($newpath, $path);
_add($path, 1);
}
elsif ($commitdone && ($status->text_status() == $SVN::Wc::Status::deleted)) {
# probably a file was add on the server below this path; don't delete it!
if ($::human) {
print " * ".get_lang(10).": $path (".get_status($status).")\n";
}
else {
print "[010] $path\n";
}
_revert($path);
}
elsif ($commitdone && ($status->text_status() == $SVN::Wc::Status::replaced)) {
# probably a file was added on one side and a directory with the same name on the other side
my $newpath = get_unique_name($path, $status->entry()->revision());
if ($::human) {
print " * ".get_lang(10).": $path (".get_status($status).")\n";
}
else {
print "[010] $path\n";
}
_copy($path, $newpath);
_revert($path);
}
elsif ($status->text_status() == $SVN::Wc::Status::missing) {
# we deleted the file, the server has a newer revision than ours. Restore the file and keep it.
if ($::human) {
print " * ".get_lang(10).": $path (".get_status($status).")\n";
}
else {
print "[010] $path\n";
}
_revert($path);
}
elsif ($commitdone) {
# if the file in question does not exist (anymore), just declare it resolved
if (! -e $path) {
if ($::human) {
print " * ".get_lang(10).": $path (".get_status($status).")\n";
}
else {
print "[010] $path\n";
}
_resolved($path);
}
}
};
# flock $lockfd, 2;
_svnctx()->status($wcpath, 'HEAD', $statuscallback, 1, 0, 1, 0);
# flock $lockfd, 8;
return $conflictfound;
}
sub _move {
my $src = shift;
my $dst = shift;
system("mv", $src, $dst);
}
sub _copy {
my $src = shift;
my $dst = shift;
if (! -e $dst) {
if (-d $src) {
mkdir $dst;
_add($dst);
if (opendir my $dirfd, $src) {
my @entries = readdir $dirfd;
closedir $dirfd;
foreach my $entry (@entries) {
if ($entry ne "." && $entry ne ".." && $entry ne ".svn") {
_copy($src."/".$entry, $dst."/".$entry);
}
}
}
}
else { # let's just assume it's a regular file
link $src, $dst;
_add($dst);
}
}
}
sub _delete {
my $file = shift;
if (-f $file) {
# flock $lockfd, 2;
_svnctx()->delete($file, 1);
# flock $lockfd, 8;
}
}
sub _add {
my $file = shift;
my $recursive = shift;
if (!defined($recursive)) {
$recursive = 0;
}
# flock $lockfd, 2;
_svnctx()->add($file, $recursive);
if (! -d $file) {
_svnctx()->propset("svn:mime-type", "application/octet-stream", $file, 0);
}
# flock $lockfd, 8;
}
sub _resolved {
my $file = shift;
# flock $lockfd, 2;
_svnctx()->resolved($file, 1);
# flock $lockfd, 8;
}
sub _revert {
my $file = shift;
# flock $lockfd, 2;
_svnctx()->revert($file, 1);
# flock $lockfd, 8;
}
sub get_unique_name {
my $path = shift;
my $revision = shift;
$path =~ /([^\/]+)$/;
my $file = $1;
my $namelength = length($file);
my $suffix = "";
if ($file =~ /\.([^\.]+)$/) {
$suffix = $1;
$file = substr($file, 0, length($file)-length($suffix)-1);
}
my $base = substr($path,0,-$namelength);
my $name = $base.$file."-".$::username.".r".$revision;
if (length($suffix) > 0) {
$name .= ".".$suffix
}
my $counter = 1;
while (-e $name) {
if (length($suffix) > 0) {
$name = $base.$file."-".$::username.".r".$revision.".v".$counter.".".$suffix;
}
else {
$name = $base.$file."-".$::username.".r".$revision.".v".$counter;
}
$counter++;
}
return $name;
}
sub commit {
my $datapath = shift;
# we need to do a commit for each entry, since some drives (directories) might not be writable on the server
my $allok = 1;
if (opendir my $dirfd, $datapath) {
my @entries = readdir $dirfd;
closedir $dirfd;
foreach my $entry (@entries) {
if ($entry !~ /^\./) {
if ($::human) {
print " ".$entry."...";
}
else {
print "[011] $entry\n";
}
my $ok = eval {
# flock $lockfd, 2;
_svnctx()->commit(${datapath}."/".$entry, 0);
# flock $lockfd, 8;
};
if ($ok) {
if ($::human) {
print "OK\n";
}
else {
print "[012] OK\n";
}
}
else {
if ($::cancelall) {
openmap_die(get_lang(20)."\n");
}
$allok = 0;
print get_lang(13)."\n";
}
}
}
}
return $allok;
}
# the config file needs to contain the following settings (replace the Xs):
#
# username = XXXXXX
# password = XXXXXX
# groupname = XXXXXX
sub load_configuration {
my $settings = shift;
my $configfile = $basepath."/.config";
if (open my $fd, $configfile) {
while (my $line = <$fd>) {
chomp $line;
if ($line !~ /^ *\#/) {
if ($line =~ /^ *([^= ]+) *= *(.+)/) {
$settings->{$1} = $2;
}
}
}
close $fd;
}
else {
openmap_die(get_lang(14)."\n");
}
}
sub is_linux {
if (-d '/boot') {
return 1;
}
return 0;
}
sub update_root_certificates {
my $certificate = shift;
foreach my $dir (@_) {
if (-d $dir) {
if (opendir my $fd, $dir) {
my @entries = readdir $fd;
closedir $fd;
foreach my $entry (@entries) {
if ($entry !~ /^\./) {
if (-d $dir."/".$entry) {
if (-e $dir."/".$entry."/cert8.db") {
system("certutil", "-A", "-n", "OpenMAP", "-t", "C,C,C", "-d", $dir."/".$entry, "-i", $certificate);
}
}
}
}
}
}
}
}