summaryrefslogtreecommitdiffstats
path: root/digitaglinktree-1.8.4beta
diff options
context:
space:
mode:
Diffstat (limited to 'digitaglinktree-1.8.4beta')
-rwxr-xr-xdigitaglinktree-1.8.4beta1116
1 files changed, 1116 insertions, 0 deletions
diff --git a/digitaglinktree-1.8.4beta b/digitaglinktree-1.8.4beta
new file mode 100755
index 0000000..1ebcdb6
--- /dev/null
+++ b/digitaglinktree-1.8.4beta
@@ -0,0 +1,1116 @@
+#!/usr/bin/perl
+
+use DBI;
+use URI::Escape;
+
+$version="1.8.4beta";
+# Author: Rainer Krienke, krienke@uni-koblenz.de
+#
+# Description:
+#
+# This script will create a linktree for all photos in a digikam
+# database that have tags set on them. Tags are used in digikam to
+# create virtual folders containing images that all have one or more tags.
+# Since other programs do not know about these virtual folders this script
+# maps these virtual folders into the filesystem by creating a directory
+# for each tag name and then linking from the tag dir to the tagged image in the
+# digikam photo directory.
+#
+# Call this script like:
+# digitaglinktree -r /home/user/photos -l /home/user/photos/tags \
+# -d /home/user/photos/digikam.db
+
+
+# Changes
+#
+# 1.1->1.2:
+# Support for hierarchical tags was added. The script can either create
+# a hierarchical directory structure for these tags (default) or treat them
+# as tags beeing on the same level resulting in a flat dir structure (-f)
+#
+# 1.2->1.3
+# Added support for multiple tags assigned to one photo. Up to now only
+# one tag (the last one found) was used.
+#
+# 1.3->1.4
+# Bug fix, links with same name in one tag directory were no resolved
+# which led to "file exists" errors when trying to create the symbolic link.
+#
+# 1.4-> 1.6, 2006/08/03
+# Added an option (-A) to archive photos and tag structure in an extra directory
+# 1.6-> 1.6.1 2006/08/15
+# Added an option (-C) for archive mode (-A). Added more information to
+# the manual page.
+# 1.6.1-> 1.6.2 2008/11/24 (Peter Muehlenpfordt, muehlenp@gmx.de)
+# Bug fix, subtags with the same name in different branches were merged
+# into one directory.
+# 1.6.2 -> 1.6.3 2008/11/25 Rainer Krienke, krienke@uni-koblenz.de
+# Fixed a bug that shows in some later perl interpreter versions when
+# accessing array references with $# operator. If $r is a ref to an array
+# then $#{@r} was ok in the past but now $#{r] is correct, perhaps it was
+# always correct and the fact that $#{@r} worked was just good luck ....
+# 1.6.3 -> 1.7.0 2008/11/27 Rainer Krienke, krienke@uni-koblenz.de
+# Adapted script to handle digikams v0.10 new database scheme as well
+# as the older ones. Version is determined automatically.
+# 1.7.0 -> 1.8.0 2012/02/07 Cyril Raphanel, cyril.raphanel@gmail.com
+# Added (-r) option for new digikam database as root directory for a set of albums below same file system
+# Added (-Y) option to add "year" directory below each tag
+# 1.8.0 -> 1.8.1 2012/02/08 krienke@uni-koblenz.de
+# added support to find albumroots on iremovable media described by uuid
+# in digikams database
+# 1.8.1 -> 1.8.2 2012/03/28 Cyril Raphanel, cyril.raphanel@gmail.com
+# Added (-i) option to include only some tags - default 'all'
+# Added (-e) option to exclude some tags - default ''
+# Added (-M) option to create directory hierarchy base on tags selection, number of hierarchy level to be specified
+# Added (-V) option to enable verbose mode
+# 1.8.2 -> 1.8.3 2013/10/18 Harald Heigl, hh-developing@heigl-online.at
+# Added (-u -p -t) option for user, password and type of database
+# changed to dbi-perl-system
+# adjusted for use with sqlite and mysql
+# 1.8.2 -> 1.8.3 2013/11/27 Rainer Krienke, krienke@uni-koblenz.de
+# Made mapping from filesystem uuid to mountpoints containing photos more robust using systemtool blkid
+# Changed default for -i from "all to "none" for -M mode. -i is now
+# mandatory for -M
+# 1.8.3 -> 1.8.4 2013/12/3 Rainer Krienke, krienke@uni-koblenz.de
+# Merge of the two forked versions 1.8.3
+# by hh-developing@heigl-online.at and 1.8.3
+# by krienke@uni-koblenz.de. Basically this merge incorporates MYSQL
+# support contributed by hh-developing@heigl-online.at
+
+
+
+# ---------------------------------------------------------------------------
+# LICENSE:
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+#
+# 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; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+#
+# ---------------------------------------------------------------------------
+
+
+use Getopt::Std;
+use File::Spec;
+use File::Path;
+use URI::Escape;
+
+# Please adapt the sqlite version to your digikam version.
+# Do not forget to also install the correct sqlite version:
+# Digikam 0.7x needs sqlite2
+# Digikam 0.8x needs sqlite3
+
+$archiveDirPhotos="Photos";
+$archiveDirLinks="Tags";
+
+#
+$scriptAuthor="krienke@uni-koblenz.de";
+
+#
+# check if sqlite can open the database
+#
+sub checkVersion{
+ my($database, $checkVersionCommand)=@_;
+ my($version);
+
+ $version="-1";
+ (my $sqlCmd = $checkVersionCommand) =~ s/'{name}'/'TagsTree'/g;
+ my $sth = $database->prepare($sqlCmd);
+ $sth->execute;
+ $sth->fetchall_arrayref();
+
+ if ($sth->err)
+ {
+ print "Error running SQL-Command \"$sqliteCmd\" on database \n";
+ exit(1);
+ }
+
+ if( $sth->rows == 0 ){
+ $version="7"; # digikam version 0.7
+ }else{
+ $version="8"; # digikam version 0.8
+ }
+ # Now check if we have digikam 0.10
+
+ (my $sqlCmd = $checkVersionCommand) =~ s/'{name}'/'AlbumRoots'/g;
+ my $sth = $database->prepare($sqlCmd);
+ $sth->execute;
+ $sth->fetchall_arrayref();
+
+ if ($sth->err)
+ {
+ print "Error running SQL-Command \"$sqliteCmd\" on database \n";
+ exit(1);
+ }
+
+ if( $sth->rows != 0 ){
+ $version="10"; # digikam version 0.10 KDE4
+ }
+ return($version);
+}
+
+
+#
+# Get all images that have tags from digikam database
+# refPathNames will contain all photos including path having tags
+# The key in refPathNames is the path, value is photos name
+# rootdir is the relative album path in which photos are stored
+# in digikam <0.10 this is the path given by the user via -r option
+# starting with digikam 0.10 rootDir may be any of the entries of table
+# AlbumRoots column specificPath, albumId is the corresponding entry
+# of column id.
+#
+sub getTaggedImages{
+ my($refImg, $database, $rootDir, $albumId, $refPaths, $refPathNames, $vers, $tage,$tagi)=@_;
+ my($i, $image, $path, $tag, $status, $id, $pid, $tmp);
+ my($sqliteCmd, @data, @tags, %tagPath, $tagCount, $refTag);
+
+ # Get tag information from database
+ $sqliteCmd="select id,pid,name from Tags;";
+ my $sth = $database->prepare($sqliteCmd);
+ $sth->execute;
+
+ if ($sth->err)
+ {
+ print "Error running SQL-Command \"$sqliteCmd\" on database \n";
+ exit(1);
+ }
+
+ # Note columns start at 1 (not 0).
+ $sth->bind_col(1, \$id);
+ $sth->bind_col(2, \$pid);
+ $sth->bind_col(3, \$tag);
+
+
+ while ($sth->fetch) { # retrieve one row
+ # map tag id data into array
+ $tags[$id]->{"tag"}="$tag";
+ $tags[$id]->{"pid"}="$pid";
+ }
+
+ # Now build up the path for tags having parents
+ # Eg: a tag people might have a subtag friends: people
+ # |->friends
+ # We want to find out the complete path for the subtag. For friends this
+ # would be the path "people/friends". Images with tag friends would then be
+ # linked in the directory <tagsroot>/people/friends/
+ for($i=0; $i<=$#tags; $i++){
+ $pid=$tags[$i]->{"pid"}; # Get parent tag id of current tag
+ $tag=$tags[$i]->{"tag"}; # Start constructing tag path
+ if( $pid == 0 ){
+ $tagPath{$i}=$tag;
+ next;
+ }
+
+ while( $pid != 0){
+ $tag=$tags[$pid]->{"tag"} . "/$tag"; # add parents tag name to path
+ $pid=$tags[$pid]->{"pid"}; # look if parent has another parent
+ }
+ # Store path constructed
+ $tagPath{$i}=$tag;
+ #warn "+ $tag \n";
+ }
+
+
+ if( $vers==7 ){
+ # SQL to get all tagged images from digikam DB with names of tags
+ $sqliteCmd="select ImageTags.name,Albums.Url,ImageTags.tagid from " .
+ " ImageTags, Albums,Tags " .
+ " where Albums.id = ImageTags.dirid; ";
+
+ }elsif( $vers == 8 ){
+ # SQL to get all tagged images from digikam DB with names of tags
+ $sqliteCmd="select Images.name,Albums.url,ImageTags.tagid from" .
+ " Images,Albums,ImageTags where " .
+ " Images.dirid=Albums.id AND Images.id=ImageTags.imageid; ";
+ }elsif( $vers == 10 ){
+ # SQL to get all tagged images from digikam DB with names of tags
+ # Only those images are considered that belong to the album in question
+ $sqliteCmd="select Images.name,Albums.relativePath,ImageTags.tagid, ImageInformation.digitizationDate, Images.id from" .
+ " Images,Albums,ImageTags,ImageInformation " .
+ " where Images.album=Albums.id AND" .
+ " Images.id=ImageTags.imageid AND " .
+ " ImageInformation.imageid=ImageTags.imageid AND " .
+ " Albums.albumRoot=$albumId "
+ }else{
+ warn "Unsupported digikam database version. Contact $scriptAuthor \n\n";
+ exit(1);
+ }
+
+ my $sth = $database->prepare($sqliteCmd);
+ $sth->execute;
+ if ($sth->err)
+ {
+ print "Error running SQL-Command \"$sqliteCmd\" on database \n";
+ exit(1);
+ }
+
+ # Note columns start at 1 (not 0).
+ $sth->bind_col(1, \$image);
+ $sth->bind_col(2, \$path);
+ $sth->bind_col(3, \$tag);
+ $sth->bind_col(4, \$imgdate);
+ $sth->bind_col(5, \$imgid);
+
+ while ($sth->fetch) {
+ ($imgYear,$foo)=split(/\-/,$imgdate);
+ if( $imgYear eq "" ){
+ $imgYear="No_Date";
+ }
+
+ $path=~s/\/$// if( $path =~ /\/$/ ); # remove a trailing /
+ $refPaths->{"$rootDir$path"}=1;
+ $refPathNames->{"$rootDir$path/$image"}=1;
+
+ #print "-- $rootDir$path/$image \n";
+
+ $tmp="$path";
+ #warn "- $rootDir, $path, $image \n";
+ # Enter all subpaths in $path as defined too
+ do{
+ $tmp=~s/\/[^\/]+$//;
+ $refPaths->{"$rootDir$tmp"}=1;
+ $tmp=~s/^\/$//; # Finally delete a "/" standing alone
+ } while (length($tmp));
+
+ $refTag=$refImg->{"$rootDir/$path/$image"}->{"tag"};
+ #
+ # One image can have several tags assigned. So we manage
+ # a list of tags for each image
+ $tagCount=$#{$refTag}+1;
+
+ # Check if tag not in the including/excluding list
+ # By default included
+ $tagok=1;
+ # check excluding list
+ if ($tage eq 'all') {
+ $tagok=0;
+ }else{
+ foreach $item (split(/,/, $tage)) {
+
+ if ($tagPath{$tag} =~ /($item)/) {
+ $tagok=0;
+ }
+
+ }
+ }
+ # check including list
+ if ($tagi eq 'all') {
+ $tagok=1;
+ }else{
+ foreach $item (split(/,/, $tagi)) {
+
+ if ($tagPath{$tag} =~ /($item)/) {
+ $tagok=1;
+ }
+
+ }
+ }
+
+ if ($tagok){
+ # The tags name
+ $refImg->{"$rootDir/$path/$image"}->{"tag"}->[$tagCount]=$tag;
+ # tags path for sub(sub+)tags
+ # print "TAG -- $tag -- PATH $path\n";
+ $refImg->{"$rootDir/$path/$image"}->{"tagpath"}->[$tagCount]=$tagPath{$tag};
+ $refImg->{"$rootDir/$path/$image"}->{"path"}=$path;
+ $refImg->{"$rootDir/$path/$image"}->{"image"}=$image;
+ $refImg->{"$rootDir/$path/$image"}->{"rootDir"}=$rootDir;
+ $refImg->{"$rootDir/$path/$image"}->{"imgYear"}=$imgYear;
+ $refImg->{"$rootDir/$path/$image"}->{"imgid"}=$imgid;
+ }
+
+ #print "$path/$image -> $tagPath($tag) \n";
+ }
+ return($status);
+}
+
+#
+# Create a directory with tag-subdirectories containing links to photos having
+# tags set. Links can be absolute or relative as well as hard or soft.
+#
+sub createLinkTree{
+ my($refImages)=shift;
+ my($fakeRoot, $linktreeRootDir, $opt_flat, $opt_absolute, $linkMode,$dirDate)=@_;
+ my( $i, $j, $cmd, $path, $image, $tag, $qpath, $qtag, $qimage, $linkName,
+ $tmp, $ret, $sourceDir, $destDir, $photoRootDir);
+ my($qtagPath, $relPath, $count);
+
+ if( -d "$linktreeRootDir" ){
+ # Remove old backuplinktree
+ system("rm -rf ${linktreeRootDir}.bak");
+ if( $? ){
+ die "Cannot run \"rm -rf ${linktreeRootDir}.bak\"\n";
+ }
+
+ # rename current tree to .bak
+ $ret=rename("$linktreeRootDir", "${linktreeRootDir}.bak" );
+ if( !$ret ){
+ die "Cannot rename \"$linktreeRootDir\" to \"${linktreeRootDir}.bak\"\n";
+ }
+ }
+
+ # Create new to level directory to hold linktree
+ $ret=mkdir("$linktreeRootDir");
+ if( !$ret ){
+ die "Cannot mkdir \"$linktreeRootDir\"\n";
+ }
+ # Iterate over all tagged images and create links
+ $count=0;
+ foreach $i (keys(%$refImages)){
+ $path=$refImages->{$i}->{"path"};
+ $image=$refImages->{$i}->{"image"};
+
+ #print "-> $photoRootDir, $path, $image, $fakeRoot\n";
+ if( ! length($fakeRoot) ){
+ $photoRootDir=$refImages->{$i}->{"rootDir"};
+ }else{
+ $photoRootDir=$fakeRoot;
+ }
+ #$qimage=quotemeta($image);
+ #$qpath=quotemeta($path);
+
+ # Check that the images in the linktrees themselves are
+ # ignored in the process of link creation. If we do not do this
+ # we might get into trouble when the linktree root is inside the
+ # digikam root dir and so the image links inside the liktree directory
+ # are indexed by digikam.
+ next if( -l "${photoRootDir}$path/$image" );
+ # $refImages always contains all images from all albums. For Mode -A
+ # in digikam 0.10 it may happen that some albums are not on the same filesystem
+ # like the photoRoot. in this case this album is not cloned to another
+ # directory buth neverless
+ # refImages contains also data about photos in this album. But since it is
+ # not cloned, the cloned root # does not exits and so the photo therin does
+ # not exist. Thats what we check here.
+ next if( ! -r "${photoRootDir}$path/$image" );
+ $count++;
+
+ # Handle all of the tags for the current photo
+ for( $j=0; $j<=$#{$refImages->{$i}->{"tag"}}; $j++){
+ $tag=$refImages->{$i}->{"tagpath"}->[$j];
+ $imgYear=$refImages->{$i}->{"imgYear"};
+ #$qtagPath=quotemeta($tagPath);
+
+ # For tags that have subtags there is a path defined in qtagPath
+ # describing the parentrelationship as directory path like "friends/family/brothers"
+ # If it is defined we want to use this path instead of the flat tag name like "brothers"
+ # Doing so results in a directory hirachy that maps the tags parent relationships
+ # into the filesystem.
+ if( $opt_flat ){
+ # For flat option just use the last subdirectory
+ $tag=~s/^.+\///;
+ }
+ # Create subdirectories needed
+ if( ! -d "${linktreeRootDir}/$tag" ){
+ $ret=mkpath("${linktreeRootDir}/$tag", 0, 0755);
+
+ if( !$ret ){
+ die "Cannot mkdir \"${linktreeRootDir}/$tag\"\n";
+ }
+ }
+ # Create subdirectories needed for date
+ if( $dirDate eq "year" ){
+ if( ! -d "${linktreeRootDir}/$tag/$imgYear" ){
+ $ret=mkpath("${linktreeRootDir}/$tag/$imgYear", 0, 0755);
+
+ if( !$ret ){
+ die "Cannot mkdir \"${linktreeRootDir}/$tag/$imgYear\"\n";
+ }
+ }
+ $tag=$tag."/".$imgYear;
+ }
+ # Avoid conflicts for images with the same name in one tag directory
+ $linkName=$image;
+ $tmp=$image;
+ while( -r "${linktreeRootDir}/$tag/$tmp" ){
+ $linkName="_" . $linkName;
+ $tmp="_" . $tmp;
+ }
+
+ if(length($opt_absolute)){
+ # Create link
+ $sourceDir="${photoRootDir}$path/$image";
+ $destDir="${linktreeRootDir}/$tag/$linkName";
+ }else{
+ # Get relative path from absolute one
+ # $rel=File::Spec->abs2rel( $path, $base ) ;
+ $relPath="";
+ $relPath=File::Spec->abs2rel("${photoRootDir}$path", "${linktreeRootDir}/$tag" ) ;
+ if( !length($relPath)){
+ die "Cannot create relative path from \"${linktreeRootDir}/$tag\" to \"${photoRootDir}$path\" . Abort.\n";
+ }
+ # Create link
+ #print "-> $relPath\n";
+ $sourceDir="$relPath/$image";
+ $destDir="${linktreeRootDir}/$tag/$linkName";
+ }
+
+ if( $linkMode eq "symbolic" ){
+ $ret=symlink($sourceDir, $destDir);
+ if( !$ret ){
+ $ret="$!";
+ warn "Warning: Failed symbolic linking \"$sourceDir\" to \"$destDir\": ",
+ "\"$ret\"\n";
+ }
+ }elsif( $linkMode eq "hard" ){
+ $ret=link($sourceDir, $destDir);
+ if( !$ret ){
+ $ret="$!";
+ warn "Warning: Failed hard linking \"$sourceDir\" to \"$destDir\": ",
+ "\"$ret\"\n";
+ }
+ }else{
+ die "$0: Illegal linkMode: \"$linkMode\". Abort.\n";
+ }
+
+ }# for
+ }
+ return($count);
+}
+
+sub createLinkTreeMultiBranch{
+ my($tag_list,$linktreeRootDir,$photoRootDir,$counttag,$countmax,$dirDate)=@_;
+ my($j,$tag);
+ my $path=$tag_list->{"path"};
+ my $image=$tag_list->{"image"};
+ my $imgid=$tag_list->{"imgid"};
+ my $imgYear=$tag_list->{"imgYear"};
+ if ($counttag<$countmax){
+ for( $j=0; $j<=$#{${tag_list}->{"tag"}}; $j++){
+ $tag=$tag_list->{"tagpath"}->[$j];
+ # Check if we have not tried the path before
+ if(${linktreeRootDir} !~ m/($tag)(\/|$)/) {
+ # Create subdirectories needed
+ if( ! -d "${linktreeRootDir}/$tag/_all" ){
+ $ret=mkpath("${linktreeRootDir}/$tag/_all", 0, 0755);
+
+ if( !$ret ){
+ die "Cannot mkdir \"${linktreeRootDir}/$tag/_all\"\n";
+ }
+ }
+ # Avoid conflicts for images with the same name in one tag directory
+ $linkName=$imgid."_".$image;
+
+ # Get relative path from absolute one
+ $relPath="";
+ $relPath=File::Spec->abs2rel("${photoRootDir}$path", "${linktreeRootDir}/$tag/_all" ) ;
+ if( !length($relPath)){
+ die "Cannot create relative path from \"${linktreeRootDir}/$tag/_all\" to \"${photoRootDir}$path\" . Abort.\n";
+ }
+ # Create link
+ $sourceDir="$relPath/$image";
+ $destDir="${linktreeRootDir}/$tag/_all/$linkName";
+
+ if (!-e $destDir){
+ $ret=symlink($sourceDir, $destDir);
+ if( !$ret ){
+ $ret="$!";
+ warn "Warning: Failed symbolic linking \"$sourceDir\" to \"$destDir\": ",
+ "\"$ret\"\n";
+ }
+ }
+
+ # Manage date information
+ if( $dirDate eq "year" ){
+ # Check if we have not tried the path before
+ if(${linktreeRootDir} !~ /(Date\/$imgYear)/) {
+ if( ! -d "${linktreeRootDir}/$tag/Date/$imgYear/_all" ){
+ $ret=mkpath("${linktreeRootDir}/$tag/Date/$imgYear/_all", 0, 0755);
+
+ if( !$ret ){
+ die "Cannot mkdir \"${linktreeRootDir}/$tag/Date/$imgYear/_all\"\n";
+ }
+ }
+
+ # Get relative path from absolute one
+ $relPath="";
+ $relPath=File::Spec->abs2rel("${photoRootDir}$path", "${linktreeRootDir}/$tag/Date/$imgYear/_all" ) ;
+ if( !length($relPath)){
+ die "Cannot create relative path from \"${linktreeRootDir}/$tag/Date/$imgYear/_all\" to \"${photoRootDir}$path\" . Abort.\n";
+ }
+ # Create link
+ $sourceDir="$relPath/$image";
+ $destDir="${linktreeRootDir}/$tag/Date/$imgYear/_all/$linkName";
+
+ if (!-e $destDir){
+ $ret=symlink($sourceDir, $destDir);
+ if( !$ret ){
+ $ret="$!";
+ warn "Warning: Failed symbolic linking \"$sourceDir\" to \"$destDir\": ",
+ "\"$ret\"\n";
+ }
+ }
+
+ # create subdir
+ createLinkTreeMultiBranch($tag_list,"${linktreeRootDir}/$tag/Date/$imgYear",$photoRootDir,$counttag,$countmax,$dirDate);
+ }
+ }
+
+ # Manage subdir
+ # create subdir
+ createLinkTreeMultiBranch($tag_list,"${linktreeRootDir}/$tag",$photoRootDir,$counttag+1,$countmax,$dirDate);
+
+ }
+ }
+ }
+
+}
+
+#
+# Create a directory with tag-subdirectories containing links to photos having
+# tags set. Links can be absolute or relative as well as hard or soft.
+#
+sub createLinkTreeMulti{
+ my($refImages)=shift;
+ my($fakeRoot, $linktreeRootDir, $opt_flat, $opt_absolute, $linkMode,$dirDate,$countmax,$verbose)=@_;
+ my( $i, $j, $cmd, $path, $image, $tag, $qpath, $qtag, $qimage, $linkName,
+ $tmp, $ret, $sourceDir, $destDir, $photoRootDir);
+ my($qtagPath, $relPath, $count);
+
+ if( -d "$linktreeRootDir" ){
+ # Remove old backuplinktree
+ system("rm -rf ${linktreeRootDir}.bak");
+ if( $? ){
+ die "Cannot run \"rm -rf ${linktreeRootDir}.bak\"\n";
+ }
+
+ # rename current tree to .bak
+ $ret=rename("$linktreeRootDir", "${linktreeRootDir}.bak" );
+ if( !$ret ){
+ die "Cannot rename \"$linktreeRootDir\" to \"${linktreeRootDir}.bak\"\n";
+ }
+ }
+
+ # Create new to level directory to hold linktree
+ $ret=mkdir("$linktreeRootDir");
+ if( !$ret ){
+ die "Cannot mkdir \"$linktreeRootDir\"\n";
+ }
+ # Iterate over all tagged images and create links
+ $count=0;
+ foreach $i (keys(%$refImages)){
+ $path=$refImages->{$i}->{"path"};
+ $image=$refImages->{$i}->{"image"};
+ $refImages->{$i}->{"imgid"};
+ $imgYear=$refImages->{$i}->{"imgYear"};
+
+ #print "-> $photoRootDir, $path, $image, $fakeRoot\n";
+ if( ! length($fakeRoot) ){
+ $photoRootDir=$refImages->{$i}->{"rootDir"};
+ }else{
+ $photoRootDir=$fakeRoot;
+ }
+ #$qimage=quotemeta($image);
+ #$qpath=quotemeta($path);
+
+ # Check that the images in the linktrees themselves are
+ # ignored in the process of link creation. If we do not do this
+ # we might get into trouble when the linktree root is inside the
+ # digikam root dir and so the image links inside the liktree directory
+ # are indexed by digikam.
+ next if( -l "${photoRootDir}$path/$image" );
+ # $refImages always contains all images from all albums. For Mode -A
+ # in digikam 0.10 it may happen that some albums are not on the same filesystem
+ # like the photoRoot. in this case this album is not cloned to another
+ # directory buth neverless
+ # refImages contains also data about photos in this album. But since it is
+ # not cloned, the cloned root # does not exits and so the photo therin does
+ # not exist. Thats what we check here.
+ next if( ! -r "${photoRootDir}$path/$image" );
+ $count++;
+
+
+ ## BEGIN ##
+ if( $dirDate eq "year" ){
+ # create root for Date tree
+ if( ! -d "${linktreeRootDir}/Date/$imgYear/_all" ){
+ $ret=mkpath("${linktreeRootDir}/Date/$imgYear/_all", 0, 0755);
+
+ if( !$ret ){
+ die "Cannot mkdir \"${linktreeRootDir}/Date/$imgYear/_all\"\n";
+ }
+ }
+
+ # Get relative path from absolute one
+ $relPath="";
+ $relPath=File::Spec->abs2rel("${photoRootDir}$path", "${linktreeRootDir}/Date/$imgYear/_all" ) ;
+ if( !length($relPath)){
+ die "Cannot create relative path from \"${linktreeRootDir}/Date/$imgYear/_all\" to \"${photoRootDir}$path\" . Abort.\n";
+ }
+ # Create link
+ $sourceDir="$relPath/$image";
+ $destDir="${linktreeRootDir}/Date/$imgYear/_all/$linkName";
+
+ if (!-e $destDir){
+ $ret=symlink($sourceDir, $destDir);
+ if( !$ret ){
+ $ret="$!";
+ warn "Warning: Failed symbolic linking \"$sourceDir\" to \"$destDir\": ",
+ "\"$ret\"\n";
+ }
+ }
+ push @imgdir,"Date/$imgYear";
+ }
+
+ if ($verbose) {
+ # print trace
+ print localtime().":IMAGE:COUNT $count:IMAGE $image:IMAGEID $imageid \n";
+ }
+
+ # Manage tags
+ createLinkTreeMultiBranch($refImages->{$i},${linktreeRootDir},$photoRootDir,0,$countmax,$dirDate);
+
+ }
+ return($count);
+}
+
+
+
+#
+# Clone a directory into another directory.
+# source and destination have to exist
+# Files in $srcDir will symbolically or hard linked
+# to the $cloneDir according to $cloneMode which can
+# be symbolic for symbolic links or hard for hardlinks
+# refPathNames contains path for all photos having tags set
+#
+sub cloneDir{
+ local($srcDir, $cloneDir, $linkMode, $cloneMode, $refPaths, $refPathNames)=@_;
+ local(@entries, $ki, $path, $name, $ret);
+
+ # Read directory entries
+ opendir( DIR, $srcDir ) || warn "Cannot read directory $srcDir \n";
+ local(@entries)=readdir(DIR);
+ closedir(DIR);
+
+ foreach $k (@entries){
+ next if( $k eq '.' );
+ next if( $k eq '..');
+ next if( $k =~ /digikam.*\.db/o );
+
+ #warn "-> $srcDir, $k, $cloneDir\n";
+ # Recurse into directories
+ if( -d "$srcDir/$k" ){
+ # if cloneMode is "minimal" then only clone directories and files
+ # with photos that have tags set.
+ if( $cloneMode ne "minimal" ||
+ defined($refPaths->{"$srcDir/$k"}) ){
+ #warn "* $srcDir/$k defined \n";
+ mkdir("$cloneDir/$k", 0755);
+ if( $? ){
+ die "Cannot run \"mkdir $cloneDir/$k\"\n";
+ }
+ cloneDir("$srcDir/$k", "$cloneDir/$k", $linkMode,
+ $cloneMode, $refPaths, $refPathNames );
+ }
+ }else{
+ # if cloneMode is "minimal" then only clone directories and files
+ # with photos that have tags set.
+ #print "+ refPathNames, $srcDir/$k , ",$refPathNames->{"$srcDir/$k"}, " \n";
+ if( $cloneMode ne "minimal" ||
+ defined($refPathNames->{"$srcDir/$k"}) ){
+ # link files
+ if( $linkMode eq "symbolic" ){
+ $ret=symlink( "$srcDir/$k", "$cloneDir/$k");
+ if( !$ret ){
+ $ret="$!";
+ warn "Warning: In cloning failed symbolic linking \"$srcDir/$k\" to \"$cloneDir/$k\": ",
+ "\"$ret\"\n";
+ }
+ }elsif( $linkMode eq "hard" ){
+ $ret=link( "$srcDir/$k", "$cloneDir/$k");
+
+ if( !$ret ){
+ $ret="$!";
+ warn "Warning: In cloning failed hard linking \"$srcDir/$k\" to \"$cloneDir/$k\": ",
+ "\"$ret\"\n";
+ }
+ }else{
+ die "$0: Illegal cloneLinkMode: $linkMode set. Aborting.\n";
+ }
+ }
+ }
+ }
+}
+
+
+#
+# Create directory for cloning photos when running with -A
+#
+sub createCloneDir{
+ my( $cloneDir, $photoDir)=@_;
+ my($ret);
+
+ if( -d "$cloneDir" ){
+ # Remove old backuplinktree
+ system("rm -rf ${cloneDir}.bak");
+ if( $? ){
+ die "Cannot run \"rm -rf ${cloneDir}.bak\"\n";
+ }
+
+ # rename current tree to .bak
+ $ret=rename("$cloneDir", "${cloneDir}.bak" );
+ if( !$ret ){
+ die "Cannot rename \"$cloneDir\" to \"${cloneDir}.bak\"\n";
+ }
+ }
+ $ret=mkdir("$cloneDir", 0755);
+ if( !$ret ){
+ die "Cannot run \"mkdir $cloneDir\"\n";
+ }
+ $ret=mkdir("$cloneDir/$photoDir", 0755);
+ if( !$ret ){
+ die "Cannot run \"mkdir $cloneDir/$photoDir\"\n";
+ }
+}
+
+#
+# Manage cloning of a complete directory. If the clone directory exists
+# a .bak will be created
+# Files will be linked (hard/symbolic) according to cloneLinkMode which
+# should be "symbolic" or "hard"
+# cloneMode can "minimal" or anything else. minimal means only to clone
+# those files and directories that have tags set. Anything else means
+# to clone the whole photo tree with all files
+#
+sub runClone{
+ my($rootDir, $cloneDir, $photoDir, $cloneMode, $cloneLinkMode,
+ $refPaths, $refPathNames)=@_;
+ my($ret);
+ my($dev_r,$dev_A, $ino,$mode,$nlink,$uid,$gid,$rdev,$size,
+ $atime,$mtime,$ctime,$blksize,$blocks);
+
+ # Check if root and archive dir are on same filesystem
+ ($dev_r,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
+ $atime,$mtime,$ctime,$blksize,$blocks) = stat($rootDir);
+ ($dev_A,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
+ $atime,$mtime,$ctime,$blksize,$blocks) = stat($cloneDir);
+ #
+ if( $dev_r != $dev_A ){
+ warn "Warning: \"$rootDir\" and \"$cloneDir\" are not on the same filesystem. Please select ",
+ "an archive directory on the same filesystem like digikams root directory! Skipped.\n";
+ return(-1);
+ }
+
+ # Create clone of all entries in $rootDir into $clonedir
+ cloneDir($rootDir, "$cloneDir/$photoDir", $cloneLinkMode,
+ $cloneMode, $refPaths, $refPathNames);
+
+ return(0);
+}
+
+
+#
+# print out usage
+#
+sub usage{
+ print " -l <taglinkdir> | -A <archivedir> \n",
+ " -d <digikamdatabasefile> \n",
+ " [-u <user>] \n",
+ " [-p <password>] \n",
+ " [-t <typeofdatabase>] \n",
+ " [-r <rootdir>] \n",
+ " [-H|-f|-a|-v|-C|e|M] \n",
+ " -l <tagdir> path to directory where the tag directory structure with \n",
+ " links to the original images should be created \n",
+ " -d <dbfile> full path to the digikam database file \n",
+ " -r <rootdir> path to digikams root directory containing all photos.\n",
+ " For digikam vers >= 0.10 this is usually not needed and should only be used\n",
+ " in case that an album root directory cannot be found.\n",
+ " Option is mandatory for earlier digikam versions!\n",
+ " -u <user> User (used for mysql)\n",
+ " -p <pwd> Password (used for mysql)\n",
+ " -t <type> Type of database: SQLite and mysql\n",
+ " -A <ardir> Create selfcontained archive of photos and tags in directory\n",
+ " ardir. rootdir and arDir have to be on the *same* filesystem!\n",
+ " -C If -A <archdir> was given this option will put hardlinks of all\n",
+ " photos in the \"$archiveDirPhotos\" directory not only of those with tags.\n",
+ " -a Create absolute symbolic links instead of relative ones \n",
+ " -H Use hard links instead of symbolic links in linktree. \n",
+ " -Y Add Year directory below each tag directory \n",
+ " -f If there are hierarchical tags (tags that have subtags) \n",
+ " create a flat tag directory hierarchy. So directories for\n",
+ " subtags are at the same directory level like their parent tags\n",
+ " -M <level> Make multi-level directory using tag classification included in each\n",
+ " image. Mandatory parameter:specifies the maximum tree level number.\n",
+ " You have to use -i with at least one tag to be processed (see below)\n",
+ " !Options -A -C -a -H -f not tested with this option!\n",
+ " -i <taglist> Include tags of comma separated list <taglist> in multi-level\n",
+ " mode containing given tag string in their tag path.\n",
+ " Use all if really all tags are needed.\n",
+ " Default: none\n",
+ " -e <taglist> Excluding tags having specific string in their tag path. \n",
+ " Default: none\n",
+ " Example Digikam,People excludes all tags having Digikam or People in their path\n",
+ " -V Verbose mode \n",
+ " -h Print this help \n",
+ " -v Print scripts version number \n";
+ print "Exit.\n\n";
+ exit 1;
+}
+
+
+#
+# get Album roots if digikam is 0.10 or higher
+#
+sub getAlbumRoots{
+ my($database, $refAlbumRoots)=@_;
+ my($ret, @data, $sql, $i, $id, $path);
+ my($identifier, $uuid, $uuiddev, $ucuuiddev, $ucuuid, $device, $mountpoint);
+ my(@more);
+
+ my $sth = $database->prepare('select id, identifier,specificPath from AlbumRoots');
+ $sth->execute;
+
+ if($sth->err){
+ warn "$0: Cannot execute $sql on database $database \n";
+ exit(1);
+ }
+
+ $sth->bind_col(1, \$id);
+ $sth->bind_col(2, \$identifier);
+ $sth->bind_col(3, \$path);
+
+
+ while ($sth->fetch) { # retrieve one row
+ # check identifier. It might be simply volumeid:?, then specificPath is the absolute
+ # path.
+ # if its volumeid:\?uuid followed by a uuid of a device, then specificPath
+ # is relative to the mountpoint of this device
+ if( $identifier =~/volumeid.*uuid/ ){
+ $uuid=$identifier;
+ $uuid=~s/^.*uuid=//i;
+ $ucuuid=uc($uuid); # make uuid upper case because sometimes it is upper case in /dev
+ if( -e "/dev/disk/by-uuid/$uuid" ){
+ $uuiddev=`blkid -U $uuid`;
+ die "Error: Cannot find system binary \"blkid\". Please install to continue.\n" if( $? != 0 );
+ }elsif(-e "/dev/disk/by-uuid/$ucuuid" ){
+ $uuiddev=`blkid -U $ucuuid`;
+ die "Error: Cannot find system binary \"blkid\". Please install to continue.\n" if( $? != 0 );
+ }else{
+ print "Cannot find device /dev/disk/by-uuid/$uuid or ..../$ucuuid. Skipped.\n";
+ next; # uuid not found in /dev/disk/by-uuid, so skip entry
+ }
+ chomp($uuiddev);
+ # print "UUID: $uuid,>$uuiddev< \n";
+ if( -e $uuiddev ){
+ if( open(IN, "/proc/mounts" ) ){
+ while(<IN>) {
+ chomp;
+ ($device, $mountpoint, @more)=split( /\s+/ );
+ #print "Device: $device, $uuiddev\n";
+ if( $device eq $uuiddev ){
+ # print "Device: $device, $mountpoint/$path \n";
+ $refAlbumRoots->{$id}="$mountpoint/$path"; # Enter Album Data
+ $refAlbumRoots->{$id}=~s/\/$//o; # remove possible trailing /
+ $refAlbumRoots->{$id}=~s/\/\//\//go; # replace // by /
+ }
+ }
+ close(IN);
+ }else{
+ print "Cannot open /proc/mounts to find mountpoint for dev: $uuiddev\n";
+ }
+ }else{
+ print "Cannot find album mountpoint for identifier $identifier . Skipped.\n";
+ }
+ }elsif ( $identifier =~/volumeid.*path/ ){
+ $path=$identifier;
+ $path=~s/volumeid:\?path=//o;
+ $path=~s/|$//o;
+ $refAlbumRoots->{$id}=$path; # Enter Album Data
+ }elsif ( $identifier =~/networkshareid.*mountpath/ ){
+ $mountpath=$identifier;
+ $mountpath=~s/^.*mountpath=//i;
+ $mountpath=uri_unescape($mountpath);
+ $refAlbumRoots->{$id}=$mountpath;
+ }else{
+ $refAlbumRoots->{$id}=$path; # Enter Album Data
+ }
+ }
+}
+
+# ------------------------------------------------------------------
+# main
+# ------------------------------------------------------------------
+$ret=getopts('VYHh:Car:M:i:e:l:d:A:fvu:p:t:');
+
+if( defined($opt_h) ){
+ usage(0);
+}
+
+if( defined($opt_v) ){
+ print "Version: $version\n";
+ exit 0;
+}
+
+$verbose=0;
+if( defined($opt_V) ){
+ $verbose=1;
+}
+
+$dirDate='none';
+if( defined($opt_Y) ){
+ $dirDate='year';
+}
+
+$tage='';
+if( defined($opt_e) ){
+ $tage=$opt_e;
+}
+
+$tagi='none';
+if( defined($opt_i) ){
+ $tagi=$opt_i;
+}
+
+$user='';
+if( defined($opt_u) ){
+ $user=$opt_u;
+}
+
+$password='';
+if( defined($opt_p) ){
+ $password=$opt_p;
+}
+
+$type='SQLite';
+if( defined($opt_t) ){
+ $type=$opt_t;
+}
+
+usage if ((!length($opt_l) && ! length($opt_A))
+ || !length($opt_d) );
+
+$opt_f=0 if( ! length($opt_f) );
+my $database;
+my $checkVersionCommand;
+# Remove trailing /
+$opt_r=~s/\/$//;
+$opt_l=~s/\/$//;
+#
+$rootDir=$opt_r;
+$linkDir=$opt_l;
+
+if( length($opt_C) ){
+ $cloneMode="full"; # Clone all files/dirs
+}else{
+ $cloneMode="minimal"; # Only clone dirs and files haviong tags not all files
+}
+
+if( length($opt_H) ){
+ $linkMode="hard"; # type of link from linktree to phototree
+}else{
+ $linkMode="symbolic"; # type of link from linktree to phototree
+}
+
+# Check digikam Database "version"
+if ($type eq 'SQLite')
+{
+ if( ! -f $opt_d ){
+ print "Digikam database \"$opt_d\" not found. Exit.\n";
+ exit 1;
+ }
+ $database = DBI->connect("dbi:SQLite:dbname=".$opt_d, "", "")
+ or die "Can't connect to database: $DBI::errstr\n";
+
+ $checkVersionCommand = "SELECT * FROM sqlite_master WHERE type='table' and name='{name}'";
+}
+elsif ($type eq 'mysql')
+{
+ $database = DBI->connect("dbi:mysql:".$opt_d, $opt_u, $opt_p, {mysql_enable_utf8 => 1} )
+ or die "Can't connect to database: $DBI::errstr\n";
+ $checkVersionCommand = "SELECT table_name FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = 'digikam' and table_name='{name}'";
+
+}
+else
+{
+ print "Type of database \"$type\" unsupported!\n";
+ exit 1;
+}
+
+$vers=checkVersion($database, $checkVersionCommand); # Find out version of digikam
+
+if( ! -d $opt_r && $vers < 10 ){
+ print "Invalid/empty digikam photoroot directory \"$opt_r\" . Use -r to give correct one. Exit.\n";
+ exit 1;
+}
+
+# For version .10 of digikam and newer get root albumpaths
+$ret=0;
+if( $vers >= 10 ){
+ getAlbumRoots($database, \%albumRoots);
+ foreach $i (keys(%albumRoots)) {
+ #print "Album roots -> $i:", $albumRoots{$i}, "\n";
+ # Extract data needed from database
+ # Interprete -r as base for all albums
+ $albumRoots{$i}="$opt_r$albumRoots{$i}";
+ $ret+=getTaggedImages(\%images, $database, uri_unescape($albumRoots{$i}), $i, \%paths, \%pathNames, $vers, $tage, $tagi);
+ }
+}else{
+ $ret=getTaggedImages(\%images, $database, $opt_r, -1, \%paths, \%pathNames, $vers, $tage, $tagi);
+}
+
+if( $ret != 0 ){
+ warn "$0: Error looking up tag information in database. Exit.\n\n";
+ exit 1;
+}
+
+# Handle Archiving photos and tag structure
+# digikams photo dir will be cloned as a whole by using
+# either soft or hard links ($cloneMode)
+$fakeRoot="";
+if( length($opt_A) ){
+ #
+ # Create a new empty directory for coling, rename old if existing
+ $ret=createCloneDir($opt_A, $archiveDirPhotos);
+ if( $vers >= 10 ){
+ foreach $i (keys(%albumRoots)) {
+ #print "$i, $albumRoots{$i}\n";
+ $ret+=runClone($albumRoots{$i}, $opt_A, $archiveDirPhotos, $cloneMode, "hard", \%paths, \%pathNames);
+ }
+
+ }else{
+ $ret=runClone($opt_r, $opt_A, $archiveDirPhotos, $cloneMode, "hard", \%paths, \%pathNames);
+
+ }
+ $fakeRoot="$opt_A/$archiveDirPhotos";
+ $linkDir="$opt_A/$archiveDirLinks";
+}
+
+if( ( length($opt_l) || length($opt_A)) ){
+ # When doing hard links we always use absolute linking
+ if( $linkMode eq "hard" ){
+ warn "Will use absolute hard links for linktree in archive mode...\n" if( !length($opt_a) );
+ $opt_a="1";
+ }
+
+ # Create the link trees for all tagged images
+ if( defined($opt_M) ){
+ if ($opt_M !~ /^.*\d+$/){
+ print "Please specify number of directory level after -M\n";
+ exit 1;
+ }
+ if( $tagi eq "none" ){
+ print "No tags included. Use option -i <taglist>. See help (-h) for more information.\n";
+ exit 1;
+ }
+ $countmax=$opt_M;
+ $count=createLinkTreeMulti(\%images, $fakeRoot, $linkDir, $opt_f, $opt_a, $linkMode,$dirDate,$countmax,$verbose);
+ } else {
+ $count=createLinkTree(\%images, $fakeRoot, $linkDir, $opt_f, $opt_a, $linkMode,$dirDate);
+ }
+ print "Processed $count photos. \n";
+}
+
+$database->disconnect();