diff options
-rwxr-xr-x | digitaglinktree-1.8.4beta | 1116 |
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(); |