Blame contrib/organize/organize.cpp

Packit 01d647
// ***************************************************************** -*- C++ -*-
Packit 01d647
/*
Packit 01d647
 * Copyright (C) 2009 Brad Schick <schickb@gmail.com>
Packit 01d647
 *
Packit 01d647
 * This file is part of the organize tool.
Packit 01d647
 *
Packit 01d647
 * This program is free software; you can redistribute it and/or
Packit 01d647
 * modify it under the terms of the GNU General Public License
Packit 01d647
 * as published by the Free Software Foundation; either version 2
Packit 01d647
 * of the License, or (at your option) any later version.
Packit 01d647
 *
Packit 01d647
 * This program is distributed in the hope that it will be useful,
Packit 01d647
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
Packit 01d647
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
Packit 01d647
 * GNU General Public License for more details.
Packit 01d647
 *
Packit 01d647
 * You should have received a copy of the GNU General Public License
Packit 01d647
 * along with this program; if not, write to the Free Software
Packit 01d647
 * Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA.
Packit 01d647
 */
Packit 01d647
/*
Packit 01d647
 File:      organize.cpp
Packit 01d647
 Author(s): Brad Schick (brad) <schickb@gmail.com>
Packit 01d647
 History:   19-Jan-09, brad: created
Packit 01d647
*/
Packit 01d647
// *****************************************************************************
Packit 01d647
Packit 01d647
#include <boost/program_options.hpp>
Packit 01d647
#include <boost/regex.hpp>
Packit 01d647
#include <boost/array.hpp>
Packit 01d647
#include <boost/algorithm/string.hpp>
Packit 01d647
#include <boost/lexical_cast.hpp>
Packit 01d647
#include <exiv2/image.hpp>
Packit 01d647
#include <exiv2/error.hpp>
Packit 01d647
#include <exiv2/basicio.hpp>
Packit 01d647
#include <iostream>
Packit 01d647
#include <iomanip>
Packit 01d647
#include <cassert>
Packit 01d647
#include <limits>
Packit 01d647
#include "MD5.h"
Packit 01d647
#include "helpers.hpp"
Packit 01d647
Packit 01d647
typedef Exiv2::byte md5digest[16];
Packit 01d647
Packit 01d647
namespace po = boost::program_options;
Packit 01d647
Packit 01d647
bool g_verbose = false;
Packit 01d647
bool g_neednewline = false;
Packit 01d647
Packit 01d647
// Array size should match number of SLOTs
Packit 01d647
boost::array<int,4> g_run_order = {{-1, -1, -1, -1}};
Packit 01d647
const int EXIF_SLOT = 0;
Packit 01d647
const int IPTC_SLOT = 1;
Packit 01d647
const int XMP_SLOT  = 2;
Packit 01d647
const int FILE_SLOT = 3;
Packit 01d647
Packit 01d647
const unsigned DOT_EVERY = 55;
Packit 01d647
Packit 01d647
struct Pattern {
Packit 01d647
    std::string pat;
Packit 01d647
    std::string desc;
Packit 01d647
    pfunc funcs[4];  // order should always be exif, iptc, xmp, file
Packit 01d647
};
Packit 01d647
Packit 01d647
struct PathPart {
Packit 01d647
    std::string pre;
Packit 01d647
    const Pattern *pat;
Packit 01d647
    std::string post;
Packit 01d647
    PathPart(std::string pre_, const Pattern *pat_, std::string post_)
Packit 01d647
         : pre(pre_), pat(pat_), post(post_) {}
Packit 01d647
};
Packit 01d647
Packit 01d647
std::vector<PathPart> g_path_parts;
Packit 01d647
Packit 01d647
// Instead of making these all global
Packit 01d647
struct ProcessParams {
Packit 01d647
    const fs::path &dest_dir;
Packit 01d647
    const bool dry_run;
Packit 01d647
    const bool ignore_dups;
Packit 01d647
    const bool ignore_unsorted;
Packit 01d647
    const bool force;
Packit 01d647
    const bool rename;
Packit 01d647
    const bool symlink;
Packit 01d647
    const bool verify;
Packit 01d647
    const bool move;
Packit 01d647
    const long limit_depth;
Packit 01d647
    const fs::path &dups_dir;
Packit 01d647
    const fs::path &unsorted_dir;
Packit 01d647
    const std::vector<std::string> &excludes;
Packit 01d647
    unsigned dups_count;
Packit 01d647
    unsigned unsorted_count;
Packit 01d647
    unsigned dir_err_count;
Packit 01d647
    unsigned file_err_count;
Packit 01d647
    unsigned ok_count;
Packit 01d647
    unsigned dups_ignored_count;
Packit 01d647
    unsigned unsorted_ignored_count;
Packit 01d647
    unsigned dir_ex_count;
Packit 01d647
    unsigned file_ex_count;
Packit 01d647
};
Packit 01d647
Packit 01d647
void process_directory(const fs::path &directory, const long depth, 
Packit 01d647
    ProcessParams &params);
Packit 01d647
Packit 01d647
const Pattern g_patterns[] = {
Packit 01d647
    {"@date", "date captured (2009-01-19)", 
Packit 01d647
        {exif_date, iptc_date, NULL, file_date} },
Packit 01d647
    {"@year", "year captured (2009)",
Packit 01d647
        {exif_year, iptc_year, NULL, file_year} },
Packit 01d647
    {"@month", "month captured (01)",
Packit 01d647
        {exif_month, iptc_month, NULL, file_month} },
Packit 01d647
    {"@day", "day captured (19)",
Packit 01d647
        {exif_day, iptc_day, NULL, file_day} },
Packit 01d647
    {"@time", "time captured (14-35-27)", 
Packit 01d647
        {exif_time, iptc_time, NULL, file_time} },
Packit 01d647
    {"@hour", "hour captured (14)",
Packit 01d647
        {exif_hour, iptc_hour, NULL, file_hour} },
Packit 01d647
    {"@min", "minute captured (35)",
Packit 01d647
        {exif_minute, iptc_minute, NULL, file_minute} },
Packit 01d647
    {"@sec", "second captured (27)",
Packit 01d647
        {exif_second, iptc_second, NULL, file_second} },
Packit 01d647
    {"@dim", "pixel dimension (2272-1704)",
Packit 01d647
        {exif_dimension, NULL, NULL, file_dimension} },
Packit 01d647
    {"@x", "pixel width (2272)",
Packit 01d647
        {exif_width, NULL, NULL, file_width} },
Packit 01d647
    {"@y", "pixel height (1704)",
Packit 01d647
        {exif_height, NULL, NULL, file_height} },
Packit 01d647
    {"@make", "device make (Canon)",
Packit 01d647
        {exif_make, NULL, NULL, NULL} },
Packit 01d647
    {"@model", "device model (Canon PowerShot S40)",
Packit 01d647
        {exif_model, NULL, NULL, NULL} },
Packit 01d647
    {"@speed", "shutter speed (1-60)",
Packit 01d647
        {exif_speed, NULL, NULL, NULL} },
Packit 01d647
    {"@aper", "aperture (F3.2)",
Packit 01d647
        {exif_aperture, NULL, NULL, NULL} },
Packit 01d647
    {"@iso", "iso speed (400)",
Packit 01d647
        {exif_iso, NULL, NULL, NULL} },
Packit 01d647
    {"@focal", "focal length (8.6 mm)",
Packit 01d647
        {exif_focal, NULL, NULL, NULL} },
Packit 01d647
    {"@dist", "subject distance (1.03 m)",
Packit 01d647
        {exif_distance, NULL, NULL, NULL} },
Packit 01d647
    {"@meter", "meter mode (multi-segment)",
Packit 01d647
        {exif_meter, NULL, NULL, NULL} },
Packit 01d647
    {"@macro", "macro mode (Off)",
Packit 01d647
        {exif_macro, NULL, NULL, NULL} },
Packit 01d647
    {"@orient", "orientation (top_left)",
Packit 01d647
        {exif_orientation, NULL, NULL, NULL} },
Packit 01d647
    {"@lens", "lens name (Tamron 90mm f-2.8)",
Packit 01d647
        {exif_lens, NULL, NULL, NULL} },
Packit 01d647
    {"@key", "first keyword (Family)",
Packit 01d647
        {exif_keyword, iptc_keyword, NULL, NULL} },
Packit 01d647
Packit 01d647
    {"", "", {NULL, NULL, NULL, NULL} }
Packit 01d647
};
Packit 01d647
Packit 01d647
Packit 01d647
// Check that 'opt1' and 'opt2' are not specified at the same time. 
Packit 01d647
void conflicting(const po::variables_map& vm, 
Packit 01d647
    const char* opt1, const char* opt2)
Packit 01d647
{
Packit 01d647
    if (vm.count(opt1) && !vm[opt1].defaulted() 
Packit 01d647
        && vm.count(opt2) && !vm[opt2].defaulted()) {
Packit 01d647
        throw std::logic_error(std::string("conflicting options '") 
Packit 01d647
            + opt1 + "' and '" + opt2 + "'");
Packit 01d647
    }
Packit 01d647
}
Packit 01d647
Packit 01d647
// Check that 'required' is present
Packit 01d647
void required(const po::variables_map& vm, const char* required)
Packit 01d647
{
Packit 01d647
    if (!vm.count(required) || vm[required].defaulted()) {
Packit 01d647
        throw std::logic_error(std::string("required parameter '") + required
Packit 01d647
            + "' is missing");
Packit 01d647
    }
Packit 01d647
}
Packit 01d647
Packit 01d647
void info(const std::string &msg)
Packit 01d647
{
Packit 01d647
    if(g_verbose) {
Packit 01d647
        std::cout << msg << "\n";
Packit 01d647
        g_neednewline = false;
Packit 01d647
    }
Packit 01d647
}
Packit 01d647
Packit 01d647
void error(const std::exception &e, const std::string &msg)
Packit 01d647
{
Packit 01d647
    if(g_neednewline) {
Packit 01d647
        std::cout << "\n";
Packit 01d647
        g_neednewline = false;
Packit 01d647
    }
Packit 01d647
    std::cerr << e.what() << "\n";
Packit 01d647
    std::cerr << msg << std::endl;
Packit 01d647
}
Packit 01d647
Packit 01d647
void usage_header(const char* exname)
Packit 01d647
{
Packit 01d647
    std::cout << "Usage: " << exname << " [options] source-dir dest-dir pattern\n";
Packit 01d647
}
Packit 01d647
Packit 01d647
void usage_full(const po::options_description &options, const char* exname)
Packit 01d647
{
Packit 01d647
    usage_header(exname);
Packit 01d647
    std::cout << "\n  Creates groups of files in new directories defined by a metadata 'pattern'.\n" <<
Packit 01d647
        "  Files are copied, moved, or linked from 'source-dir' to 'dest-dir'.\n" <<
Packit 01d647
        "  The destination directory should not be within the source directory.\n\n";
Packit 01d647
    std::cout << options;
Packit 01d647
Packit 01d647
    std::cout << "\nPattern values:\n";
Packit 01d647
    for( const Pattern *pattern = g_patterns; pattern->pat.length(); ++pattern) {
Packit 01d647
        std::cout << "  " << std::setw(8) << std::left << pattern->pat;
Packit 01d647
        std::cout << pattern->desc << "\n";
Packit 01d647
    }
Packit 01d647
Packit 01d647
    std::cout << "\nExamples:\n";
Packit 01d647
    std::cout << "  `" <<  exname << " -m mess clean @year-@month'\n";
Packit 01d647
    std::cout << "     Moves files from 'mess' into directories of 'clean' according to\n" <<
Packit 01d647
                 "     year-month the file was captured (clean/2006-11/...)\n\n";
Packit 01d647
    std::cout << "  `" <<  exname << " -o ie source find width-@x/height-@y'\n";
Packit 01d647
    std::cout << "     Copies files into directories according first to pixel width then pixel\n" <<
Packit 01d647
                 "     height. Check iptc then exif metadata (find/width-2272/height-1704/...)\n\n";
Packit 01d647
    std::cout << "  `" <<  exname << " -lf source find @aper/@hour'\n";
Packit 01d647
    std::cout << "     Force create symlinks in directories according first to aperture then\n" <<
Packit 01d647
                 "     hour captured (find/F3.2/15/...)\n";
Packit 01d647
Packit 01d647
    std::cout << std::endl;
Packit 01d647
}
Packit 01d647
Packit 01d647
void version()
Packit 01d647
{
Packit 01d647
    std::cout << "organized 0.1\n" <<
Packit 01d647
        "Copyright (C) 2009 Brad Schick. <schickb@gmail.com>\n\n" <<
Packit 01d647
        "This program is free software; you can redistribute it and/or\n"
Packit 01d647
        "modify it under the terms of the GNU General Public License\n"
Packit 01d647
        "as published by the Free Software Foundation; either version 2\n"
Packit 01d647
        "of the License, or (at your option) any later version.\n"
Packit 01d647
        "\n"
Packit 01d647
        "This program is distributed in the hope that it will be useful,\n"
Packit 01d647
        "but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
Packit 01d647
        "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n"
Packit 01d647
        "GNU General Public License for more details.\n"
Packit 01d647
        "\n"
Packit 01d647
        "You should have received a copy of the GNU General Public\n"
Packit 01d647
        "License along with this program; if not, write to the Free\n"
Packit 01d647
        "Software Foundation, Inc., 51 Franklin Street, Fifth Floor,\n"
Packit 01d647
        "Boston, MA 02110-1301 USA" << std::endl;
Packit 01d647
}
Packit 01d647
Packit 01d647
// Returns empty string if the destination subdirectory could not be determined
Packit 01d647
// for the supplied source file.
Packit 01d647
std::string build_dest(const fs::path &source_file) 
Packit 01d647
{
Packit 01d647
    std::string dest;
Packit 01d647
Packit 01d647
    Exiv2::Image::AutoPtr image;
Packit 01d647
    try {
Packit 01d647
        image = Exiv2::ImageFactory::open(source_file.string());
Packit 01d647
        image->readMetadata();
Packit 01d647
    } 
Packit 01d647
    catch(const Exiv2::AnyError&) {
Packit 01d647
        // No metadata, let things continue to try file info
Packit 01d647
    }
Packit 01d647
Packit 01d647
    std::vector<PathPart>::iterator iter = g_path_parts.begin();
Packit 01d647
    std::vector<PathPart>::iterator end = g_path_parts.end();
Packit 01d647
    for( ; iter != end; ++iter) {
Packit 01d647
        dest += iter->pre;
Packit 01d647
        std::string result;
Packit 01d647
Packit 01d647
        const Pattern *pat = iter->pat;
Packit 01d647
        for(unsigned fx = 0; fx < g_run_order.size(); ++fx) {
Packit 01d647
            if(g_run_order[fx] != -1 && pat->funcs[g_run_order[fx]]) {
Packit 01d647
                if(g_run_order[fx] == FILE_SLOT) {
Packit 01d647
                    // Always run file operations
Packit 01d647
                    result = pat->funcs[g_run_order[fx]](image.get(), source_file);
Packit 01d647
                }
Packit 01d647
                else if(image.get()) {
Packit 01d647
                    // No point in running metadata operations without an image
Packit 01d647
                    result = pat->funcs[g_run_order[fx]](image.get(), source_file);
Packit 01d647
                }
Packit 01d647
                if(result.length())
Packit 01d647
                    break;
Packit 01d647
            }
Packit 01d647
        }
Packit 01d647
        // If we found no data, even for part of pattern, give up and 
Packit 01d647
        // return no destination
Packit 01d647
        if(!result.length())
Packit 01d647
            return result;
Packit 01d647
    
Packit 01d647
        dest += (result + iter->post);
Packit 01d647
    }
Packit 01d647
    return dest;
Packit 01d647
}
Packit 01d647
Packit 01d647
bool md5sum(const fs::path &path, md5digest &digest)
Packit 01d647
{
Packit 01d647
    try {
Packit 01d647
        Exiv2::FileIo io(path.string());
Packit 01d647
        if (io.open() != 0)
Packit 01d647
            return false;
Packit 01d647
        Exiv2::IoCloser closer(io);
Packit 01d647
Packit 01d647
        Exiv2::byte buff[4096];
Packit 01d647
        MD5_CTX context;
Packit 01d647
        MD5Init(&context);
Packit 01d647
Packit 01d647
        long read_count = io.read(buff, 4096);
Packit 01d647
        while(read_count) {
Packit 01d647
            MD5Update(&context, buff, read_count);
Packit 01d647
            read_count = io.read(buff, 4096);
Packit 01d647
        }
Packit 01d647
        MD5Final(digest, &context);
Packit 01d647
        return true;
Packit 01d647
    }
Packit 01d647
    catch (std::exception& ) {
Packit 01d647
        return false;
Packit 01d647
    }
Packit 01d647
}
Packit 01d647
Packit 01d647
Packit 01d647
int main(int argc, char* argv[])
Packit 01d647
{
Packit 01d647
    po::options_description options("Options");
Packit 01d647
    // Don't use default values because the help print it ugly and too wide
Packit 01d647
    options.add_options()
Packit 01d647
        ("move,m", "move files rather than copy")
Packit 01d647
        ("symlink,s", "symlink files rather than copy (posix only)")
Packit 01d647
        ("order,o", po::value<std::string>(), 
Packit 01d647
            "order and types of metadata to read\ne=exif, i=iptc, f=file (default: eif)")
Packit 01d647
        ("unsorted,u", po::value<std::string>(), 
Packit 01d647
            "special directory to store unsorted files (default: unsorted)")
Packit 01d647
        ("dups,d", po::value<std::string>(), 
Packit 01d647
            "special directory to store files with duplicate names (default: duplicates)")
Packit 01d647
        ("force,f", "overwrite duplicate files instead of using special directory")
Packit 01d647
        ("rename,r", "rename duplicate files instead of using special directory")
Packit 01d647
        ("ignore,i", "ignore both unsorted and duplicate files instead of using special directories")
Packit 01d647
        ("ignore-unsorted", "ignore unsorted files instead of using special directory")
Packit 01d647
        ("ignore-dups", "ignore duplicate files instead of using special directory")
Packit 01d647
        ("verify", "verify copied or moved files and exit if incorrect")
Packit 01d647
        ("exclude,x", po::value< std::vector<std::string> >(), 
Packit 01d647
            "exclude directories and files that contain arg (case sensitive on all platforms)")
Packit 01d647
        ("limit-depth,l", po::value<long>(), 
Packit 01d647
            "limit recursion to specified depth (0 disables recursion)")
Packit 01d647
        ("verbose,v", "prints operations as they happen")
Packit 01d647
        ("dry-run,n", "do not make actual changes (implies verbose)")
Packit 01d647
        ("help,h", "show this help message then exit")
Packit 01d647
        ("version,V", "show program version then exit")
Packit 01d647
        ;
Packit 01d647
Packit 01d647
    po::options_description hidden("Hidden Options");
Packit 01d647
    hidden.add_options()
Packit 01d647
        ("source-dir", po::value< std::string >(), "directory of files to organize, may end in file wildcard")
Packit 01d647
        ("dest-dir", po::value< std::string >(), "designation directory for files, may not be within source-dir")
Packit 01d647
        ("pattern", po::value< std::string >(), "subdirectory pattern for grouping files within dest-dir")
Packit 01d647
        ;
Packit 01d647
Packit 01d647
    po::options_description cmdline;
Packit 01d647
    cmdline.add(options).add(hidden);
Packit 01d647
Packit 01d647
    po::positional_options_description positional;
Packit 01d647
    positional.add("source-dir", 1);
Packit 01d647
    positional.add("dest-dir", 1);
Packit 01d647
    positional.add("pattern", 1);
Packit 01d647
Packit 01d647
    try {
Packit 01d647
        po::variables_map vm;
Packit 01d647
        po::store(po::command_line_parser(argc, argv).
Packit 01d647
        options(cmdline).positional(positional).run(), vm);
Packit 01d647
        po::notify(vm);
Packit 01d647
Packit 01d647
        if (vm.count("help")) {
Packit 01d647
            usage_full(options, argv[0]);
Packit 01d647
            return 0;
Packit 01d647
        }
Packit 01d647
    
Packit 01d647
        if (vm.count("version")) {
Packit 01d647
            version();
Packit 01d647
            return 0;
Packit 01d647
        }
Packit 01d647
    
Packit 01d647
        conflicting(vm, "verify", "symlink");
Packit 01d647
        conflicting(vm, "move", "symlink");
Packit 01d647
        conflicting(vm, "unsorted", "ignore");
Packit 01d647
        conflicting(vm, "unsorted", "ignore-unsorted");
Packit 01d647
        conflicting(vm, "dups", "ignore");
Packit 01d647
        conflicting(vm, "dups", "ignore-dups");
Packit 01d647
        conflicting(vm, "force", "ignore");
Packit 01d647
        conflicting(vm, "force", "ignore-dups");
Packit 01d647
        conflicting(vm, "force", "rename");
Packit 01d647
        conflicting(vm, "rename", "ignore");
Packit 01d647
        conflicting(vm, "rename", "ignore-dups");
Packit 01d647
        required(vm, "source-dir");
Packit 01d647
        required(vm, "dest-dir");
Packit 01d647
        required(vm, "pattern");
Packit 01d647
    
Packit 01d647
        const bool dry_run = vm.count("dry-run") != 0;
Packit 01d647
        g_verbose = (vm.count("verbose") != 0 || dry_run);
Packit 01d647
    
Packit 01d647
        std::string order = "eif";
Packit 01d647
        if(vm.count("order")) {
Packit 01d647
            order = vm["order"].as<std::string>();
Packit 01d647
    
Packit 01d647
            boost::to_lower(order);
Packit 01d647
            if(order.length() > 3) {
Packit 01d647
                throw std::logic_error(std::string("order is longer than 4 characters"));
Packit 01d647
            }
Packit 01d647
        }
Packit 01d647
    
Packit 01d647
        unsigned i = 0;
Packit 01d647
        std::string::iterator end = order.end();
Packit 01d647
        for(std::string::iterator iter = order.begin(); iter != end && i < 4; ++iter, ++i) {
Packit 01d647
            switch(*iter) {
Packit 01d647
                case 'e': 
Packit 01d647
                    g_run_order[i] = EXIF_SLOT;
Packit 01d647
                    break;
Packit 01d647
                case 'i': 
Packit 01d647
                    g_run_order[i] = IPTC_SLOT;
Packit 01d647
                    break;
Packit 01d647
                case 'x': 
Packit 01d647
                    throw std::logic_error(std::string("xmp not implemented yet '") + 
Packit 01d647
                        *iter + "'");
Packit 01d647
                    break;
Packit 01d647
                case 'f': 
Packit 01d647
                    g_run_order[i] = FILE_SLOT;
Packit 01d647
                    break;
Packit 01d647
                default:
Packit 01d647
                    throw std::logic_error(std::string("unknown order character '") + 
Packit 01d647
                        *iter + "'");
Packit 01d647
            }
Packit 01d647
        }
Packit 01d647
    
Packit 01d647
        const fs::path source_dir( vm["source-dir"].as<std::string>() );
Packit 01d647
        if( !exists(source_dir) || !is_directory(source_dir) ) {
Packit 01d647
            throw std::logic_error(std::string("source '") + 
Packit 01d647
                source_dir.string() + "' must exist and be a directory");
Packit 01d647
        }
Packit 01d647
    
Packit 01d647
        const fs::path dest_dir( vm["dest-dir"].as<std::string>() );
Packit 01d647
        if( exists(dest_dir) && !is_directory(dest_dir) ) {
Packit 01d647
            throw std::logic_error(std::string("destination '") + 
Packit 01d647
                dest_dir.string() + "' must be a directory");
Packit 01d647
        }
Packit 01d647
    
Packit 01d647
        // Boost doesn't seem to have a way to get a canonical path, so this
Packit 01d647
        // simple test is easy to confuse with some ../../'s in the paths. Oh
Packit 01d647
        // well, this is good enough for now.
Packit 01d647
        fs::path test_dest(dest_dir);
Packit 01d647
        for(; !test_dest.empty(); test_dest = test_dest.parent_path()) {
Packit 01d647
            if(fs::equivalent(source_dir, test_dest)) {
Packit 01d647
                throw std::logic_error(std::string("dest-dir must not be within source-dir"));
Packit 01d647
            }
Packit 01d647
        }
Packit 01d647
    
Packit 01d647
        // Disect the pattern
Packit 01d647
        std::string pattern = vm["pattern"].as<std::string>();
Packit 01d647
        boost::regex regex( "([^@]*)(@[[:alpha:]]+)([^@]*)");
Packit 01d647
        boost::sregex_iterator m_iter = make_regex_iterator(pattern, regex);
Packit 01d647
        boost::sregex_iterator m_end;
Packit 01d647
        for( ; m_iter != m_end; ++m_iter) {
Packit 01d647
            const boost::smatch &match = *m_iter;
Packit 01d647
            const std::string &pre = match[1];
Packit 01d647
            const std::string &pat = match[2];
Packit 01d647
            const std::string &post = match[3];
Packit 01d647
    
Packit 01d647
            // Should put this in a map, but there aren't that many options now
Packit 01d647
            bool found = false;
Packit 01d647
            for( const Pattern *pattern = g_patterns; pattern->pat.length(); ++pattern) {
Packit 01d647
                if(pattern->pat == pat) {
Packit 01d647
                    PathPart part(pre, pattern, post);
Packit 01d647
                    g_path_parts.push_back(part);
Packit 01d647
                    found = true;
Packit 01d647
                    break;
Packit 01d647
                }
Packit 01d647
            }
Packit 01d647
        
Packit 01d647
            if(!found) {
Packit 01d647
                throw std::logic_error(std::string("unknown pattern '") + pat + "'");
Packit 01d647
            }
Packit 01d647
        }
Packit 01d647
    
Packit 01d647
        // Assign defaults to params that need them
Packit 01d647
        const bool ignore = vm.count("ignore") != 0;
Packit 01d647
        std::vector<std::string> excludes;
Packit 01d647
        if(vm.count("exclude"))
Packit 01d647
            excludes = vm["exclude"].as< std::vector<std::string> >();
Packit 01d647
        long limit_depth = LONG_MAX;
Packit 01d647
        if(vm.count("limit-depth")) {
Packit 01d647
            limit_depth = vm["limit-depth"].as<long>();
Packit 01d647
            // Boost program_options doesn't work with unsigned, so do it manually
Packit 01d647
            if( limit_depth < 0 )
Packit 01d647
                throw std::logic_error(std::string("recursion depth limit must be positive"));
Packit 01d647
        }
Packit 01d647
        std::string dups = "duplicates";
Packit 01d647
        if(vm.count("dups"))
Packit 01d647
            dups = vm["dups"].as<std::string>();
Packit 01d647
        const fs::path dups_dir = dest_dir / dups;
Packit 01d647
    
Packit 01d647
        std::string unsorted = "unsorted";
Packit 01d647
        if(vm.count("unsorted"))
Packit 01d647
            unsorted = vm["unsorted"].as<std::string>();
Packit 01d647
        const fs::path unsorted_dir = dest_dir / unsorted;
Packit 01d647
    
Packit 01d647
        ProcessParams params = {
Packit 01d647
            dest_dir, 
Packit 01d647
            dry_run,
Packit 01d647
            (vm.count("ignore-dups") != 0 || ignore), 
Packit 01d647
            (vm.count("ignore-unsorted") != 0 || ignore), 
Packit 01d647
            vm.count("force") != 0,
Packit 01d647
            vm.count("rename") != 0,
Packit 01d647
            vm.count("symlink") != 0, 
Packit 01d647
            vm.count("verify") != 0, 
Packit 01d647
            vm.count("move") != 0, 
Packit 01d647
            limit_depth, 
Packit 01d647
            dups_dir, 
Packit 01d647
            unsorted_dir, 
Packit 01d647
            excludes,
Packit 01d647
            0, 0, 0, 0, 0, 0, 0, 0, 0
Packit 01d647
        };
Packit 01d647
    
Packit 01d647
        process_directory(source_dir, 0, params);
Packit 01d647
    
Packit 01d647
        std::string op = "copied";
Packit 01d647
        if(params.symlink)
Packit 01d647
            op = "linked";
Packit 01d647
        else if(params.move)
Packit 01d647
            op = "moved";
Packit 01d647
    
Packit 01d647
        if(dry_run)
Packit 01d647
            op = std::string("would be ") + op;
Packit 01d647
    
Packit 01d647
        if(g_neednewline)
Packit 01d647
            std::cout << "\n";
Packit 01d647
    
Packit 01d647
        std::cout << "\n" << params.ok_count << " files " << op << "\n";
Packit 01d647
        std::cout << "   " << params.dups_count << " duplicates\n";
Packit 01d647
        std::cout << "   " << params.unsorted_count << " unsorted\n";
Packit 01d647
        if(params.dups_ignored_count)
Packit 01d647
            std::cout << params.dups_ignored_count << " duplicates ignored\n";
Packit 01d647
        if(params.unsorted_ignored_count)
Packit 01d647
            std::cout << params.unsorted_ignored_count << " unsorted ignored\n";
Packit 01d647
        if(params.dir_ex_count)
Packit 01d647
            std::cout << params.dir_ex_count << " directories excluded\n";
Packit 01d647
        if(params.file_ex_count)
Packit 01d647
            std::cout << params.file_ex_count << " files excluded\n";
Packit 01d647
        if(params.dir_err_count)
Packit 01d647
            std::cout << params.dir_err_count << " directory errors\n";
Packit 01d647
        if(params.file_err_count)
Packit 01d647
            std::cout << params.file_err_count << " file errors\n";
Packit 01d647
    
Packit 01d647
        return 0;
Packit 01d647
    }
Packit 01d647
    catch (Exiv2::AnyError& e) {
Packit 01d647
        error(e, std::string("Aborting"));
Packit 01d647
        return -1;
Packit 01d647
    }
Packit 01d647
    catch(std::logic_error& e) {
Packit 01d647
        error(e, "");
Packit 01d647
        usage_header(argv[0]);
Packit 01d647
        std::cout << argv[0] << " -h    for more help" << std::endl;
Packit 01d647
        return -2;
Packit 01d647
    }
Packit 01d647
    catch(std::exception& e) {
Packit 01d647
        error(e, "Aborting");
Packit 01d647
        return -3;
Packit 01d647
    }
Packit 01d647
}
Packit 01d647
Packit 01d647
boost::regex uregex("(.*?)\\(([[:digit:]]{1,2})\\)$");
Packit 01d647
Packit 01d647
fs::path uniquify(const fs::path &dest)
Packit 01d647
{
Packit 01d647
    std::string ext = dest.extension().string();
Packit 01d647
    std::string fname = dest.stem().string();
Packit 01d647
    fs::path parent = dest.parent_path();
Packit 01d647
Packit 01d647
    unsigned number = 1;
Packit 01d647
    std::string newfname;
Packit 01d647
    fs::path newdest;
Packit 01d647
Packit 01d647
    boost::smatch match;
Packit 01d647
    if(boost::regex_search(fname, match, uregex)) {
Packit 01d647
        // Matches are indexes into fname, so don't change it while reading values
Packit 01d647
        newfname = match[1];
Packit 01d647
        number = boost::lexical_cast<short>(match[2]);
Packit 01d647
        fname = newfname;
Packit 01d647
    }
Packit 01d647
Packit 01d647
    do { 
Packit 01d647
        newfname = fname + "(" + boost::lexical_cast<std::string>(++number) + ")" + ext;
Packit 01d647
        newdest = parent / newfname;
Packit 01d647
    } while(fs::exists(newdest));
Packit 01d647
Packit 01d647
    return newdest;
Packit 01d647
}
Packit 01d647
Packit 01d647
void process_directory(const fs::path &directory, const long depth, 
Packit 01d647
    ProcessParams &params)
Packit 01d647
{
Packit 01d647
    // Exclude entire directories
Packit 01d647
    bool exclude = false;
Packit 01d647
    std::vector<std::string>::const_iterator x_iter = params.excludes.begin();
Packit 01d647
    std::vector<std::string>::const_iterator x_end = params.excludes.end();
Packit 01d647
    for( ; x_iter != x_end; ++x_iter ) {
Packit 01d647
        if(boost::contains(directory.string(), *x_iter)) {
Packit 01d647
            exclude = true;
Packit 01d647
            break;
Packit 01d647
        }
Packit 01d647
    }
Packit 01d647
    if(exclude) {
Packit 01d647
        info(std::string("excluding directory: ") + directory.string() +
Packit 01d647
            " matched: " + *x_iter);
Packit 01d647
        ++params.dir_ex_count;
Packit 01d647
        return;
Packit 01d647
    }
Packit 01d647
Packit 01d647
    try {
Packit 01d647
        fs::directory_iterator p_iter(directory), p_end; 
Packit 01d647
        for( ; p_iter != p_end; ++p_iter) {
Packit 01d647
            if( is_directory(*p_iter) ) {
Packit 01d647
                // recurse if we haven't hit the limit
Packit 01d647
                if(depth < params.limit_depth)
Packit 01d647
                    process_directory(p_iter->path(), depth + 1, params);
Packit 01d647
                else {
Packit 01d647
                    info(std::string("depth reached, skipping: ") +
Packit 01d647
                        p_iter->path().string());
Packit 01d647
                }
Packit 01d647
            }
Packit 01d647
            else if( is_regular_file(*p_iter) ) {
Packit 01d647
        
Packit 01d647
                // Check again for excluding file names
Packit 01d647
                exclude = false;
Packit 01d647
                x_iter = params.excludes.begin();
Packit 01d647
                for( ; x_iter != x_end; ++x_iter ) {
Packit 01d647
                    if(boost::contains(p_iter->path().string(), *x_iter)) {
Packit 01d647
                        exclude = true;
Packit 01d647
                        break;
Packit 01d647
                    }
Packit 01d647
                }
Packit 01d647
                if(exclude) {
Packit 01d647
                    info(std::string("excluding file: ") + p_iter->path().string() +
Packit 01d647
                        " matched: " + *x_iter);
Packit 01d647
                    ++params.file_ex_count;
Packit 01d647
                    continue;
Packit 01d647
                }
Packit 01d647
            
Packit 01d647
                try {
Packit 01d647
                    const fs::path dest_subdir = build_dest(*p_iter);
Packit 01d647
                    fs::path dest_file;
Packit 01d647
                    if(!dest_subdir.empty())
Packit 01d647
                        dest_file = params.dest_dir / dest_subdir;
Packit 01d647
                    else if(params.ignore_unsorted) {
Packit 01d647
                        info(std::string("ignoring unsorted: ") + p_iter->path().string());
Packit 01d647
                        ++params.unsorted_ignored_count;
Packit 01d647
                        continue;
Packit 01d647
                    }
Packit 01d647
                    else {
Packit 01d647
                        info(std::string("unsorted file (missing metadata): ") + p_iter->path().string());
Packit 01d647
                        dest_file = params.unsorted_dir;
Packit 01d647
                        ++params.unsorted_count;
Packit 01d647
                    }
Packit 01d647
            
Packit 01d647
                    dest_file /= p_iter->path().filename();
Packit 01d647
                
Packit 01d647
                    if(fs::exists(dest_file)) {
Packit 01d647
                        if(params.ignore_dups) {
Packit 01d647
                            info(std::string("ignoring: ") + p_iter->path().string() +
Packit 01d647
                                " duplicates: " +  dest_file.string());
Packit 01d647
                            ++params.dups_ignored_count;
Packit 01d647
                            continue;
Packit 01d647
                        }
Packit 01d647
                        else {
Packit 01d647
                            if(params.force) {
Packit 01d647
                                info(std::string("force removing: ") + dest_file.string() + " for: "
Packit 01d647
                                    + p_iter->path().string());
Packit 01d647
                                if(!params.dry_run)
Packit 01d647
                                    fs::remove(dest_file);
Packit 01d647
                            }
Packit 01d647
                            else if(params.rename) {
Packit 01d647
                                info(std::string("renaming: ") + p_iter->path().string() +
Packit 01d647
                                    " duplicates: " +  dest_file.string());
Packit 01d647
                                dest_file = uniquify(dest_file);
Packit 01d647
                            }
Packit 01d647
                            else {
Packit 01d647
                                info(std::string("duplicate file: ") + p_iter->path().string() +
Packit 01d647
                                    " of: " +  dest_file.string());
Packit 01d647
                                dest_file = params.dups_dir / dest_subdir / p_iter->path().filename();
Packit 01d647
                                // Ugh, more dup possibilities
Packit 01d647
                                if(fs::exists(dest_file)) {
Packit 01d647
                                    info(std::string("renaming: ") + p_iter->path().string() +
Packit 01d647
                                        " duplicates: " +  dest_file.string());
Packit 01d647
                                    dest_file = uniquify(dest_file);
Packit 01d647
                                }
Packit 01d647
                            }
Packit 01d647
                            ++params.dups_count;
Packit 01d647
                        }
Packit 01d647
                    }
Packit 01d647
                
Packit 01d647
                    if(!params.dry_run)
Packit 01d647
                        fs::create_directories(dest_file.parent_path());
Packit 01d647
                
Packit 01d647
                    if(params.symlink) {
Packit 01d647
                        info(std::string("linking from: ") + p_iter->path().string() + 
Packit 01d647
                            " to: " + dest_file.string());
Packit 01d647
                        if(!params.dry_run) {
Packit 01d647
                            // The target of a symlink must be either absolute (aka complete) or
Packit 01d647
                            // relative to the location of the link. Easiest solution is to make
Packit 01d647
                            // a complete path.
Packit 01d647
                            fs::path target;
Packit 01d647
                            if(p_iter->path().is_complete()) 
Packit 01d647
                                target = p_iter->path();
Packit 01d647
                            else 
Packit 01d647
                                target = fs::initial_path() / p_iter->path();
Packit 01d647
                            fs::create_symlink(target, dest_file);
Packit 01d647
                        }
Packit 01d647
                    }
Packit 01d647
                    else {
Packit 01d647
                        info(std::string("copying from: ") + p_iter->path().string() +
Packit 01d647
                            " to: " + dest_file.string());
Packit 01d647
                        if(!params.dry_run) {
Packit 01d647
                            // Copy the file and restore its write time (needed for posix)
Packit 01d647
                            std::time_t time = fs::last_write_time(*p_iter);
Packit 01d647
                            fs::copy_file(*p_iter, dest_file);
Packit 01d647
                            fs::last_write_time(dest_file, time);
Packit 01d647
                            if(params.verify) {
Packit 01d647
                                md5digest src_digest, dst_digest;
Packit 01d647
                                bool ok = md5sum(p_iter->path(), src_digest);
Packit 01d647
                                if(ok)
Packit 01d647
                                    ok = md5sum(dest_file, dst_digest);
Packit 01d647
                                if(ok)
Packit 01d647
                                    ok = (memcmp(src_digest,dst_digest, sizeof(md5digest))==0);
Packit 01d647
                                if(!ok) {
Packit 01d647
                                    // Should probably find a more appropriate exception for this
Packit 01d647
                                    throw std::runtime_error(std::string("File verification failed: '") 
Packit 01d647
                                        + p_iter->path().string() + "' differs from '" + 
Packit 01d647
                                        dest_file.string() + "'");
Packit 01d647
                                } 
Packit 01d647
                                else {
Packit 01d647
                                    info(std::string("verification passed"));
Packit 01d647
                                }
Packit 01d647
                            }
Packit 01d647
                        }
Packit 01d647
                    }
Packit 01d647
                    if(params.move) {
Packit 01d647
                        info(std::string("removing: ") + p_iter->path().string());
Packit 01d647
                        if(!params.dry_run)
Packit 01d647
                            fs::remove(*p_iter);
Packit 01d647
                    }
Packit 01d647
                
Packit 01d647
                    if(!g_verbose && (params.ok_count % DOT_EVERY)==0) {
Packit 01d647
                        std::cout << "." << std::flush;
Packit 01d647
                        g_neednewline = true;
Packit 01d647
                    }
Packit 01d647
                    ++params.ok_count;
Packit 01d647
                }
Packit 01d647
                catch(fs::filesystem_error& e) {
Packit 01d647
                    error(e, std::string("skipping file: " + p_iter->path().string()));
Packit 01d647
                    ++params.file_err_count;
Packit 01d647
                }
Packit 01d647
            }
Packit 01d647
        }
Packit 01d647
    }
Packit 01d647
    catch(fs::filesystem_error& e) {
Packit 01d647
        error(e, std::string("skipping directory: " + directory.string()));
Packit 01d647
        ++params.dir_err_count;
Packit 01d647
    }
Packit 01d647
}
Packit 01d647