de.burlov.amazon.s3.dirsync.DirSync.java Source code

Java tutorial

Introduction

Here is the source code for de.burlov.amazon.s3.dirsync.DirSync.java

Source

/*
 * Copyright 2008 Paul Burlov
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package de.burlov.amazon.s3.dirsync;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.URI;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.filefilter.FileFilterUtils;
import org.apache.commons.io.input.CountingInputStream;
import org.apache.commons.io.output.NullOutputStream;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bouncycastle.crypto.BlockCipher;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.engines.SerpentEngine;
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
import org.bouncycastle.crypto.params.KeyParameter;
import org.jets3t.service.S3Service;
import org.jets3t.service.S3ServiceException;
import org.jets3t.service.impl.rest.httpclient.RestS3Service;
import org.jets3t.service.model.S3Bucket;
import org.jets3t.service.model.S3Object;
import org.jets3t.service.security.AWSCredentials;

import de.burlov.amazon.s3.RegExpUtil;
import de.burlov.amazon.s3.S3Utils;
import de.burlov.amazon.s3.dirsync.datamodel.v1.FileInfo;
import de.burlov.amazon.s3.dirsync.datamodel.v1.Folder;
import de.burlov.amazon.s3.dirsync.datamodel.v1.MainIndex;
import de.burlov.bouncycastle.io.CryptInputStream;
import de.burlov.bouncycastle.io.CryptOutputStream;

/**
 * Primaere s3dirsync Klasse mit High Level API
 * 
 * @author paul
 * 
 */
public class DirSync {
    static final private byte[] salt = new byte[] { (byte) 89, (byte) 43, (byte) 94, (byte) 02, (byte) 20,
            (byte) 45, (byte) 123, (byte) 1, (byte) 0, (byte) 204 };
    static final private int ITERATION_COUNT = 30000;
    static final private String SYS_DATA_PREFIX = "system";
    static final private String FILES_PREFIX = "data";
    static final private String MAIN_INDEX_KEY = "main-index";
    static final private String DELIMITER = "/";
    // static final private String USER_METADATA_PREFIX = "x-amz-meta-";
    // static final private String METADATA_FILE = USER_METADATA_PREFIX +
    // "file";
    // static final private String METADATA_FOLDER = USER_METADATA_PREFIX +
    // "folder";

    private String bucket;
    private String location;
    private S3Service s3Service;
    private byte[] pbeKey;
    private BlockCipher cipher;
    private MainIndex mainIndex;
    private Log log = LogFactory.getLog(DirSync.class);
    private Map<String, Folder> folderCache = new HashMap<String, Folder>();
    private int deletedFiles;
    private int transferredFiles;
    private long transferredData;
    private String accessKey;
    private String secretKey;
    private MessageDigest shaDigest;
    private MessageDigest md5Digest;
    private List<Pattern> excludePatterns = new LinkedList<Pattern>();
    private List<Pattern> includePatterns = new LinkedList<Pattern>();

    public DirSync(String accessKey, String secretKey, String bucket, String location, char[] encPassword)
            throws DirSyncException {
        super();
        try {
            shaDigest = MessageDigest.getInstance("SHA-1");
            md5Digest = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            throw new DirSyncException(e.getMessage());
        }
        this.accessKey = accessKey;
        this.secretKey = secretKey;
        if (encPassword == null) {
            throw new IllegalArgumentException("encryption password is null");
        }
        this.bucket = bucket;
        this.location = location;
        if (StringUtils.equalsIgnoreCase(location, S3Bucket.LOCATION_EUROPE)) {
            this.location = S3Bucket.LOCATION_EUROPE;
        } else {
            this.location = null;
        }
        if (StringUtils.isBlank(this.bucket)) {
            this.bucket = accessKey + ".dirsync";
        }
        cipher = new SerpentEngine();
        pbeKey = generatePbeKey(encPassword);
        try {
            s3Service = new RestS3Service(new AWSCredentials(accessKey, secretKey));
        } catch (S3ServiceException e1) {
            throw new DirSyncException("Connecting to S3 service failed", e1);
        }
    }

    /**
     * 
     * @param autocreate
     *        'true' wenn fehlende Index/bucket automatisch erstellt werden sollen
     * @throws DirSyncException
     */
    private void connect(boolean autocreate) throws DirSyncException {
        if (mainIndex != null) {
            return;
        }
        boolean bucketExists = false;
        try {
            bucketExists = s3Service.isBucketAccessible(bucket);
        } catch (S3ServiceException e1) {
            throw new DirSyncException("Internal error: " + e1.getMessage());
        }
        if (!bucketExists) {
            if (autocreate) {
                /*
                 * In 'up' Modus benoetigte Bucket erstellen falls er noch nicht vorhanden ist
                 */
                try {
                    s3Service.createBucket(bucket, location);
                } catch (S3ServiceException e2) {
                    throw new DirSyncException(
                            "Creating bucket '" + bucket + "' failed: " + e2.getLocalizedMessage());
                }
            } else {
                throw new DirSyncException("Bucket not found: " + bucket);
            }
        }
        try {
            mainIndex = (MainIndex) downloadObject(getMainIndexKey(), pbeKey);
        } catch (IOException e) {
            /*
             * Lesen des Indexes fehlgeschlagen, falsches Passwort?
             */
            throw new DirSyncException("Reading main index failed. Are password and S3 login data valid?", e);
        }
        if (mainIndex == null) {
            if (autocreate) {
                /*
                 * Noch keine Daten auf dem Server
                 */
                mainIndex = new MainIndex();
                /*
                 * Schluessel fuer Datenverschluesselung generieren
                 */
                SecureRandom srnd = new SecureRandom();
                srnd.setSeed(pbeKey);
                byte[] dataKey = new byte[32];
                srnd.nextBytes(dataKey);
                mainIndex.setEncryptionKey(dataKey);
            } else {
                /*
                 * Kein Index gefunden, also keine Daten zum Runterladen
                 */
                throw new DirSyncException("No data found");
            }
        }
    }

    /**
     * Sensible Daten in Hauptspeicher explizit ueberschreiben
     */
    public void close() {
        if (pbeKey != null) {
            Arrays.fill(pbeKey, (byte) 0);
        }
        if (mainIndex != null) {
            Arrays.fill(mainIndex.getEncryptionKey(), (byte) 0);
        }
        accessKey = null;
        secretKey = null;
    }

    /**
     * Synchronisiert Daten im lokalen Verzeichnis und auf dem Server
     * 
     * @param baseDir
     * @param folderName
     * @param up
     *        Synchronisationsrichtung. Wenn 'true' dann werden Daten von lokalen Verzeichnis auf
     *        den Server geschrieben. Wenn 'false' Dann werden Daten von Server auf lokalen
     *        Verzeichnis geschrieben
     * @param snapShot
     *        Wenn 'true' dann wird Synchronisierung in 'Abbild' Modus duchgefuehrt. D.h.
     *        Zielverzeichnis wird auf den gleichen Stand wie Quelle gebracht: neue Dateien werde
     *        geloescht, geloeschte wiederherstellt und modifiezierte upgedated. Wenn 'false' dann
     *        werden neue Dateien hinzugefuegt und modifizierte upgedated aber auf keinen Fall
     *        irgendetwas geloescht.
     * @throws DirSyncException
     */
    public void syncFolder(File baseDir, String folderName, boolean up, boolean snapShot) throws DirSyncException {
        connect(up);
        deletedFiles = 0;
        transferredFiles = 0;
        transferredData = 0;
        if (!baseDir.isDirectory()) {
            throw new DirSyncException("Invalid directory: " + baseDir.getAbsolutePath());
        }
        Folder folder = getFolder(folderName);
        if (folder == null) {
            if (!up) {
                log.warn("No such folder " + folderName);
                //System.out.println("No such folder " + folderName);
                return;
            }
            folder = new Folder(folderName, Long.toHexString(mainIndex.getNextId()));
        }
        Collection<LocalFile> files;
        try {
            files = generateChangedFileList(baseDir, folder);
        } catch (Exception e) {
            throw new DirSyncException(e);
        }
        if (up) {
            syncUp(folder, files, snapShot);
        } else {
            syncDown(folder, baseDir, files, snapShot);
        }
        log.info("Transferred data: " + FileUtils.byteCountToDisplaySize(transferredData));
        log.info("Transferred files: " + transferredFiles);
        log.info("Deleted/Removed files: " + deletedFiles);
    }

    public void deleteFolder(String folderName) throws DirSyncException {
        connect(false);
        Folder folder = getFolder(folderName);
        if (folder == null) {
            return;
        }
        for (Map.Entry<String, FileInfo> info : folder.getIndexData().entrySet()) {
            try {
                deleteRemoteFile(info.getValue().getStorageId());
            } catch (S3ServiceException e) {
                log.error("Unable to delete file " + info.getKey(), e);
            }
        }
        mainIndex.getFolders().remove(folderName);
        try {
            saveMainIndex();
        } catch (S3ServiceException e) {
            throw new DirSyncException("Unable to delete main index entry", e);
        }
        folderCache.remove(folderName);
        try {
            s3Service.deleteObject(bucket, getFolderKey(folder.getStorageId()));
        } catch (S3ServiceException e) {
            throw new DirSyncException("Unable to delete folder entry", e);
        }

    }

    /**
     * Methode setzt Passwort auf neuen Wert. Beim naechsten connet() Aufruf muss schon neues
     * Passwort mitgegeben werden.
     * 
     * @param newPassword
     * @throws DirSyncException
     */
    public void changePassword(char[] newPassword) throws DirSyncException {
        connect(false);
        pbeKey = generatePbeKey(newPassword);
        try {
            /*
             * MainIndex mit neuen PBE-Key verschlusselt speichern. Daten-Schluessel bleibt
             * unangetastet.
             */
            saveMainIndex();
        } catch (S3ServiceException e) {
            throw new DirSyncException(e);
        }
    }

    private byte[] generatePbeKey(char[] password) {
        PKCS5S2ParametersGenerator pgen = new PKCS5S2ParametersGenerator();
        pgen.init(PKCS5S2ParametersGenerator.PKCS5PasswordToBytes(password), salt, ITERATION_COUNT);
        CipherParameters params = pgen.generateDerivedParameters(256);
        byte[] ret = ((KeyParameter) params).getKey();
        return ret;
    }

    private void saveFolder(Folder folder) throws DirSyncException {
        try {
            /*
             * Neuen Folder-Objekt konstruieren und speichern
             */
            Folder newFolder = new Folder(folder.getName(), Long.toHexString(mainIndex.getNextId()));
            newFolder.setLastModified(System.currentTimeMillis());
            newFolder.setIndexData(folder.getIndexData());
            uploadObject(SYS_DATA_PREFIX + "/" + newFolder.getStorageId(), getDataEncryptionKey(), newFolder);

            /*
             * Main Index mit neuem Folder sichern
             */
            mainIndex.getFolders().put(newFolder.getName(), newFolder.getStorageId());
            saveMainIndex();
            folderCache.put(newFolder.getName(), newFolder);

            /*
             * Alten Folder-Objekt loeschen
             */
            s3Service.deleteObject(bucket, getFolderKey(folder.getStorageId()));

        } catch (Exception e) {
            throw new DirSyncException("Save folder description failed", e);
        }
    }

    private void saveMainIndex() throws S3ServiceException {
        uploadObject(getMainIndexKey(), pbeKey, mainIndex);
    }

    /**
     * Methode laedt lokale Dateien die als geaendert erkannt wurden auf den Server hoch
     * 
     * @param folder
     * @param files
     * @param snapShot
     * @throws DirSyncException
     */
    private void syncUp(Folder folder, Collection<LocalFile> files, boolean snapShot) throws DirSyncException {
        boolean dirty = false;
        long uploadedBytes = 0;
        for (LocalFile item : files) {
            File file = item.getLocalFile();
            if (file.exists()) {
                /*
                 * Den Hashwert der lokalen Datei berechnen und versuchen zuerst eine bereits
                 * hochgeladene Dateie mit diesem Hash zu finden. Es kann je sein, dass lokale Datei
                 * nur umbenannt oder kopiert wurde, dann braucht man nicht die Daten noch mal
                 * hochzuladen.
                 */
                byte[] hash;
                try {
                    hash = digestFile(file, shaDigest);
                } catch (IOException e1) {
                    throw new DirSyncException("Unable to hash file: " + file.getAbsolutePath(), e1);
                }
                FileInfo info = folder.getFileInfo(hash);
                if (info != null) {
                    /*
                     * Eine Datei existiert bereits mit so einem Hashwert. Keine Daten hochladen
                     * sondern nur bereits hochgeladene Datei zusaetzlich mit neuem Dateinamen
                     * verknuepfen
                     */
                    folder.getIndexData().put(item.getRelativeName(), info);
                    log.info("Link file with uploaded data: " + item.getRelativeName());
                    info.setLastModified(file.lastModified());
                    dirty = true;
                } else {
                    /*
                     * Noch kein Datenobjekt auf dem Server mit so einem Hashwert. Daten muessen
                     * hochgeladen werden. Hochgeladene Objekte immer unter neuem Key speichern.
                     * Somit alte Daten auf jeden Fall erhalten bleiben bis Folder-Index hochgeladen
                     * wurde
                     */
                    info = new FileInfo(file.lastModified(), file.length(),
                            Long.toHexString(mainIndex.getNextId()));
                    try {
                        log.info("Uploading " + file.getAbsolutePath());
                        /*
                         * Neue/geaenderte Datei uploaden
                         */
                        uploadFile(file, getFileKey(info.getStorageId()));
                        dirty = true;
                        uploadedBytes += file.length();
                        transferredData += file.length();
                        transferredFiles++;
                        info.setHash(hash);
                    } catch (Exception e) {
                        throw new DirSyncException(
                                "File upload '" + file.getAbsolutePath() + "' failed. " + e.getLocalizedMessage());
                    }
                    folder.getIndexData().put(item.getRelativeName(), info);
                    if (uploadedBytes > 10000000) {
                        /*
                         * In bestimmten Intervalen die Indexinformationen auf dem Server
                         * aktualisieren, damit schon hochgeladenen Daten nicht verloren gehen
                         */
                        saveFolder(folder);
                        dirty = false;
                        uploadedBytes = 0;
                    }
                }
            } else if (snapShot) {
                /*
                 * Lokale Datei wurde geloescht, in 'snap shot' Modus auch auf dem Server loeschen
                 */
                if (folder.getIndexData().remove(item.getRelativeName()) != null) {
                    log.info("Remove file " + item.getRelativeName());
                    deletedFiles++;
                    dirty = true;

                }
            }
        }
        /*
         * Jetzt Folder-Index speichern und nicht mehr referenzierte Objekte loschen
         */
        Set<String> idsToDelete = folder.syncFileHashIndex();
        if (dirty) {
            saveFolder(folder);
        }
        try {
            for (String id : idsToDelete) {
                deleteRemoteFile(id);
            }
        } catch (S3ServiceException e) {
            /*
             * Fehler ist nicht schwerwiegend. Es werden hochstens verwaiste Objekte uebrigbleiben
             * die mit 'cleanup' Befehle geloescht werden koennen
             */
            log.error("Unable to delete remote file. " + e.getLocalizedMessage(), e);
        }
    }

    private void syncDown(Folder folder, File baseDir, Collection<LocalFile> files, boolean snapShot) {
        for (LocalFile item : files) {
            FileInfo info = folder.getIndexData().get(item.getRelativeName());
            File file = item.getLocalFile();
            if (info != null) {
                /*
                 * Vermissten oder geanderte Datei runterladen
                 */
                try {
                    /*
                     * Zuerst Hashwert berechnen und vergleichen. Villeicht ist das Download gar
                     * nicht noetig
                     */
                    byte[] hash = null;
                    if (file.exists()) {
                        hash = digestFile(file, shaDigest);
                    }
                    if (info.getHash() == null || !Arrays.equals(info.getHash(), hash)) {
                        log.info("Downloading " + item.getRelativeName());
                        downloadFile(file, getFileKey(info.getStorageId()));
                        transferredFiles++;
                        transferredData += file.length();
                    }
                    file.setLastModified(info.getLastModified());
                } catch (Exception e) {
                    log.error("File download '" + item.getRelativeName() + "' failed. " + e.getLocalizedMessage());
                }
            } else if (snapShot) {
                /*
                 * neue lokale Dateien in 'snap shot' Modus loeschen
                 */
                log.info("Delete " + file.getAbsolutePath());
                if (!file.delete()) {
                    log.warn("Unable to delete file: " + file.getAbsolutePath());
                } else {
                    deletedFiles++;
                }
                /*
                 * Wenn Verzeichnis keine Dateien mehr enthaelt, muss er auch geloescht werden
                 */
                deleteEmptyFolder(baseDir, file.getParentFile());
            }
        }
    }

    /**
     * Loescht rekursiv leere Verzeichnisse
     * 
     * @param lowerDir
     *        unterste Verzeichnis, der nicht geloscht werden darf
     * @param dir
     *        Verzeichnis zum loeschen
     */
    private void deleteEmptyFolder(final File lowerDir, File dir) {
        if (dir != null && dir.exists() && dir.isDirectory() && !lowerDir.equals(dir)) {
            File[] children = dir.listFiles();
            if (children == null || children.length == 0) {
                dir.delete();
                dir = dir.getParentFile();
                deleteEmptyFolder(lowerDir, dir);
            }
        }
    }

    private void uploadFile(File file, String key) throws IOException, S3ServiceException {
        file = prepareFileForUpload(file, key);
        /*
         * MD5 der fuer hochladen preparierter Datei berechnen damit bei putten Amazon die
         * Richtigkeit der uebertragenen Daten verifizieren kann
         */
        byte[] digest = digestFile(file, md5Digest);
        try {
            S3Object obj = new S3Object(key);
            obj.setDataInputFile(file);
            obj.setMd5Hash(digest);
            obj.setContentLength(file.length());
            s3Service.putObject(bucket, obj);
        } finally {
            /*
             * Datei anschliessend loeschen
             */
            file.delete();
        }
    }

    /**
     * Liefert Schluessel fuer primaere Datenverschluesselung
     * 
     * @return
     */
    private byte[] getDataEncryptionKey() {
        assert mainIndex != null;
        assert mainIndex.getEncryptionKey() != null && mainIndex.getEncryptionKey().length >= 32;
        return mainIndex.getEncryptionKey();
    }

    /**
     * Prepariert eine Datei zum Hochladen. Sie wird komprimiert und verschluesselt
     * 
     * @param source
     * @return
     * @throws IOException
     */
    private File prepareFileForUpload(File source, String s3key) throws IOException {
        File tmp = File.createTempFile("dirsync", ".tmp");
        tmp.deleteOnExit();
        InputStream in = null;
        OutputStream out = null;
        try {
            in = new FileInputStream(source);
            out = new DeflaterOutputStream(
                    new CryptOutputStream(new FileOutputStream(tmp), cipher, getDataEncryptionKey()));
            IOUtils.copy(in, out);
            in.close();
            out.close();
            return tmp;
        } finally {
            IOUtils.closeQuietly(in);
            IOUtils.closeQuietly(out);
        }
    }

    private void downloadFile(File target, String s3key) throws IOException, S3ServiceException {
        InputStream in = downloadData(s3key);
        if (in == null) {
            throw new IOException("No data found");
        }
        in = new InflaterInputStream(new CryptInputStream(in, cipher, getDataEncryptionKey()));
        File temp = File.createTempFile("dirsync", null);
        FileOutputStream fout = new FileOutputStream(temp);
        try {
            IOUtils.copy(in, fout);
            if (target.exists()) {
                target.delete();
            }
            IOUtils.closeQuietly(fout);
            IOUtils.closeQuietly(in);
            FileUtils.moveFile(temp, target);
        } catch (IOException e) {
            fetchStream(in);
            throw e;
        } finally {
            IOUtils.closeQuietly(fout);
            IOUtils.closeQuietly(in);
        }
    }

    private void fetchStream(InputStream in) {
        /*
         * HTTP-Streams muessen zu Ende gelesen werden
         */
        byte[] buf = new byte[1024];
        try {
            while (in.read(buf) > 0) {
            }
        } catch (IOException e) {
            return;
        }
    }

    private void upload(InputStream in, String s3key, long length) throws S3ServiceException {
        S3Object so = new S3Object(s3key);
        so.setDataInputStream(in);
        so.setContentLength(length);
        s3Service.putObject(bucket, so);
    }

    private void deleteRemoteFile(String id) throws S3ServiceException {
        s3Service.deleteObject(bucket, getFileKey(id));
    }

    /**
     * Generiert Liste mit geanderten Dateien. Fehlende oder neue Dateien werden auch hinzugefuegt.
     * List wird unter berucksichtigung der 'exclude' und/oder 'include' Patterns erstellt
     * 
     * @param baseDir
     * @param folder
     * @return
     * @throws IOException
     */
    @SuppressWarnings("unchecked")
    private List<LocalFile> generateChangedFileList(File baseDir, Folder folder) throws Exception {
        HashSet<String> localFiles = new HashSet<String>();
        List<LocalFile> ret = new LinkedList<LocalFile>();
        for (File file : (Collection<File>) FileUtils.listFiles(baseDir, FileFilterUtils.trueFileFilter(),
                FileFilterUtils.trueFileFilter())) {
            if (!file.isFile()) {
                /*
                 * Ordner ignorieren
                 */
                continue;
            }
            String filename = computeRelativeName(baseDir, file);
            if (!shouldIncludeFile(filename)) {
                /*
                 * Laut exclude/include Regeln soll die Datei ignoriert werden
                 */
                continue;
            }
            localFiles.add(filename);

            FileInfo info = folder.getFileInfo(filename);
            if (info == null) {
                /*
                 * Datei ist neu und wurde noch nicht hochgeladen, bei synchronisierung abhaengig
                 * von der Richtung, 'up' oder 'down' wird sie entweder hochgeladen oder geloescht
                 */
                ret.add(new LocalFile(file, filename));
            } else {
                if (isFileChanged(file, info)) {
                    ret.add(new LocalFile(file, filename));
                }
            }
        }
        /*
         * Jetzt evt entfernte Dateien hinzufuegen
         */
        for (String filename : folder.getIndexData().keySet()) {
            if (!shouldIncludeFile(filename)) {
                /*
                 * Laut exclude/include Regeln soll die Datei ignoriert werden
                 */
                continue;
            }
            if (!localFiles.contains(filename)) {
                /*
                 * Datei wurde lokal geloescht
                 */
                ret.add(new LocalFile(new File(baseDir, filename), filename));
            }
        }
        return ret;
    }

    /**
     * Methode testet ob gegebene Dateiename laut vorhandenen exclude/include Regeln in Liste der zu
     * bearbeitenden Datein eingeschlossen werden soll
     * 
     * @param filename
     * @return
     */
    private boolean shouldIncludeFile(String filename) {
        if (!includePatterns.isEmpty()) {
            boolean include = false;
            /*
             * Wenn Include-Regel nicht leer sind, dann jede Datei die nicht darunter faellt
             * excludieren
             */
            for (Pattern p : includePatterns) {
                if (p.matcher(filename).matches()) {
                    include = true;
                    break;
                }
            }
            if (!include) {
                return false;
            }

        }
        for (Pattern p : excludePatterns) {
            if (p.matcher(filename).matches()) {
                return false;
            }
        }
        return true;
    }

    /**
     * Testet ob eine Datei sich geaendert hat.
     * 
     * @param file
     * @param info
     * @return
     * @throws Exception
     */
    private boolean isFileChanged(File file, FileInfo info) throws Exception {
        if (file.length() != info.getLength()) {
            return true;
        }
        if (file.lastModified() != info.getLastModified()) {
            return true;
        }
        return false;
    }

    private byte[] digestFile(File file, MessageDigest digest) throws IOException {
        DigestInputStream in = new DigestInputStream(new FileInputStream(file), digest);
        IOUtils.copy(in, new NullOutputStream());
        in.close();
        return in.getMessageDigest().digest();
    }

    /**
     * Methode berechnet relative Pfad einer Datei bezueglich eines Basisordners
     * 
     * @param baseDir
     * @param file
     * @return
     */
    private String computeRelativeName(File baseDir, File file) {
        URI relUri = baseDir.toURI().relativize(file.toURI());
        return relUri.getPath();
        // String ret = StringUtils.substringAfter(file.getAbsolutePath(),
        // baseDir.getAbsolutePath());
        // /*
        // * Jetzt Dateiname in UNIX Form bringen, falls Programm auf Windows
        // laeuft
        // */
        // ret = ret.replace('\\', '/');
        // return ret;
    }

    /**
     * Liefert Folder OBjekt mit dem gegebenen Namen. Wenn Folder Objekt schon runtergeladen wurde,
     * dann wird die lokale Version zuruckgegeben. Anderfalls wird zuerst Folder Objekt vom Server
     * runtergeladen
     * 
     * @param name
     * @return
     * @throws DirSyncException
     */
    public Folder getFolder(String name) throws DirSyncException {
        connect(false);
        String id = mainIndex.getFolders().get(name);
        if (id == null) {
            return null;
        }
        try {
            return getFolderIntern(id);
        } catch (Exception e) {
            throw new DirSyncException("Reading folder description failed", e);
        }
    }

    private Folder getFolderIntern(String id) throws IOException, S3ServiceException {
        Folder ret = folderCache.get(id);
        if (ret == null) {
            ret = downloadFolder(id);
            if (ret != null) {
                folderCache.put(ret.getName(), ret);
            }
        }
        return ret;
    }

    private Folder downloadFolder(String id) throws IOException, S3ServiceException {
        Folder folder = (Folder) downloadObject(getFolderKey(id), getDataEncryptionKey());
        folder.initFileHashIndex();
        return folder;
    }

    private void uploadObject(String s3key, byte[] encKey, Serializable obj) throws S3ServiceException {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        try {
            ObjectOutputStream oout = new ObjectOutputStream(
                    new DeflaterOutputStream(new CryptOutputStream(bout, cipher, encKey)));
            oout.writeObject(obj);
            oout.close();
        } catch (IOException e) {
            /*
             * sollte eigentlich nie vorkommen
             */
            throw new RuntimeException(e);
        }
        byte[] data = bout.toByteArray();
        upload(new ByteArrayInputStream(data), s3key, data.length);
        transferredData += data.length;
    }

    private Object downloadObject(String s3key, byte[] encKey) throws IOException {
        InputStream in = downloadData(s3key);
        if (in == null) {
            return null;
        }
        CountingInputStream cin = new CountingInputStream(
                new InflaterInputStream(new CryptInputStream(in, cipher, encKey)));
        ObjectInputStream oin = new ObjectInputStream(cin);
        try {
            Object o = oin.readObject();
            transferredData += cin.getByteCount();
            return o;
        } catch (ClassNotFoundException e) {
            /*
             * sollte eigentlich nie vorkommen
             */
            throw new IOException(e.getLocalizedMessage());
        } finally {
            IOUtils.closeQuietly(in);
        }
    }

    private InputStream downloadData(String key) {
        try {
            S3Object obj = null;
            obj = s3Service.getObject(new S3Bucket(bucket), key);
            if (obj == null || obj.getDataInputStream() == null) {
                return null;
            }
            return obj.getDataInputStream();
        } catch (S3ServiceException e) {
            /*
             * Hoechstwahrscheinlich ist der Objekt nicht vorhanden
             */
            // log.warn(e.getLocalizedMessage());
        }
        return null;
    }

    /**
     * Methode loescht alle Objekte aus einem Bucket
     * 
     * @throws DirSyncException
     */
    public void cleanBucket() throws DirSyncException {
        try {
            for (S3Object so : s3Service.listObjects(new S3Bucket(bucket))) {
                try {
                    s3Service.deleteObject(bucket, so.getKey());
                } catch (S3ServiceException e) {
                    log.error("Deleting of object '" + so.getKey() + "' failed: " + e.getLocalizedMessage());
                }
            }
        } catch (S3ServiceException e) {
            throw new DirSyncException("Cleaning bucket failed: " + e.getLocalizedMessage());
        }
    }

    /**
     * Methode findet nicht mehr referenzierte Objekte in S3 und loescht sie
     * 
     * @throws DirSyncException
     */
    public void cleanUp() throws DirSyncException {
        connect(false);
        /*
         * Zuerst liste mit Objekten erstellen die referenziert sind
         */
        Set<String> referencedKeys = getAllUsedObjects();
        int removedCount = 0;
        try {
            for (String key : S3Utils.listObjects(accessKey, secretKey, bucket)) {
                if (!referencedKeys.remove(key)) {
                    removedCount++;
                    s3Service.deleteObject(bucket, key);
                }
            }
        } catch (Exception e) {
            throw new DirSyncException(e);
        }

        log.info("Objects deleted: " + removedCount);
    }

    /**
     * Methode liefrt Auflistung mit S3 Keys aller aktuell vom Programm benutzten und gespeicherten
     * Objekte (Daten als auch Verwaltungsinformationen)
     * 
     * @return
     * @throws DirSyncException
     */
    private Set<String> getAllUsedObjects() throws DirSyncException {
        HashSet<String> referencedKeys = new HashSet<String>();
        /*
         * MainIndex
         */
        referencedKeys.add(getMainIndexKey());
        /*
         * Folders
         */
        for (String id : mainIndex.getFolders().values()) {
            referencedKeys.add(getFolderKey(id));
        }
        /*
         * Dateien aus Folders
         */
        for (String name : mainIndex.getFolders().keySet()) {
            Folder folder = getFolder(name);
            if (folder == null) {
                continue;
            }
            for (FileInfo info : folder.getIndexData().values()) {
                referencedKeys.add(getFileKey(info.getStorageId()));
            }
        }
        return referencedKeys;
    }

    /**
     * Methode gibt Zusammenfassung der auf dem Server liegenenden Daten
     * 
     * @throws DirSyncException
     */
    public void printStorageSummary() throws DirSyncException {
        int storedObjects = 0;
        try {
            if (!S3Utils.bucketExists(accessKey, secretKey, bucket)) {
                System.out.println("No such bucket");
                return;
            }
            System.out.println("Summary for bucket " + bucket);
            for (String str : S3Utils.listObjects(accessKey, secretKey, bucket)) {
                storedObjects++;
            }
        } catch (Exception e) {
            throw new DirSyncException(e.getMessage());
        }
        connect(false);
        int usedObjects = getAllUsedObjects().size();
        System.out.println("Total objects in use: " + usedObjects);
        System.out.println("Total stored objects: " + storedObjects);
        if (usedObjects < storedObjects) {
            System.out.println(
                    (storedObjects - usedObjects) + " orphaned objects found. '-cleanup' command recommended");
        }
        System.out.println("---------------------------------------------------------------------------------");
        long totalSize = 0;
        for (Map.Entry<String, String> entry : mainIndex.getFolders().entrySet()) {
            System.out.println("Folder: " + entry.getKey());
            Folder folder = getFolder(entry.getKey());
            long folderSize = 0;
            if (folder != null) {
                for (FileInfo fi : folder.getIndexData().values()) {
                    folderSize += fi.getLength();
                }
                System.out.println("Files: " + folder.getIndexData().size());
                System.out.println("Size: " + FileUtils.byteCountToDisplaySize(folderSize));
                System.out.println();
            }
            totalSize += folderSize;
        }
        System.out.println("---------------------------------------------------------------------------------");
        System.out.println("Total folders: " + mainIndex.getFolders().size());
        System.out.println("Total size: " + FileUtils.byteCountToDisplaySize(totalSize));
    }

    /**
     * Methode setzt Filterregel fuer Dateien die aus Prozess ausgeschlossen werden sollen. Exclude
     * Regeln haben hoehere Prioritaet als Include Regel.
     * 
     * @param excludes
     *        Filterregel mit * und ?
     */
    public void setExcludePatterns(Collection<String> excludes) {
        excludePatterns = new ArrayList<Pattern>(excludes.size());
        for (String str : excludes) {
            String exp = RegExpUtil.convertSimpleRegexpToJava(str);
            excludePatterns.add(Pattern.compile(exp));
        }
    }

    /**
     * Methode setzt Filterregel fuer Dateien die in das Prozess eingeschlossen werden sollen.
     * Exclude Regeln haben hoehere Prioritaet als Include Regel.
     * 
     * @param includes
     *        Filterregel mit * und ?
     */
    public void setIncludePatterns(Collection<String> includes) {
        includePatterns = new ArrayList<Pattern>(includes.size());
        for (String str : includes) {
            String exp = RegExpUtil.convertSimpleRegexpToJava(str);
            includePatterns.add(Pattern.compile(exp));
        }
    }

    static private String getMainIndexKey() {
        return SYS_DATA_PREFIX + DELIMITER + MAIN_INDEX_KEY;
    }

    static private String getFolderKey(String id) {
        return SYS_DATA_PREFIX + DELIMITER + id;
    }

    static private String getFileKey(String id) {
        return FILES_PREFIX + DELIMITER + id;
    }
}