#!/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 /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$path/$linkName"; print "$sourceDir -> $destDir\n"; if ( ! -d "${linktreeRootDir}/$tag$path" ) { $ret=mkpath("${linktreeRootDir}/$tag$path", 0, 0755); } if( !$ret ){ die "Cannot mkdir \"${linktreeRootDir}/$tag$path\"\n"; } }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 | -A \n", " -d \n", " [-u ] \n", " [-p ] \n", " [-t ] \n", " [-r ] \n", " [-H|-f|-a|-v|-C|e|M] \n", " -l path to directory where the tag directory structure with \n", " links to the original images should be created \n", " -d full path to the digikam database file \n", " -r 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 (used for mysql)\n", " -p Password (used for mysql)\n", " -t Type of database: SQLite and mysql\n", " -A 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 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", " and regenerate the original folder (album) structure for each tag \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 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 Include tags of comma separated list 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 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() { 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 . 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();