Package com.alexkasko.delta

Source Code of com.alexkasko.delta.DirDeltaPatcher

package com.alexkasko.delta;

import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.nothome.delta.GDiffPatcher;
import com.nothome.delta.RandomAccessFileSeekableSource;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.LineIterator;
import org.apache.commons.lang.UnhandledException;

import java.io.*;
import java.lang.reflect.Type;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import static com.alexkasko.delta.HashUtils.hex;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static org.apache.commons.io.FileUtils.openInputStream;
import static org.apache.commons.io.FileUtils.openOutputStream;
import static com.alexkasko.delta.IndexEntry.State.*;
import static com.alexkasko.delta.HashUtils.computeSha1;

/**
* Patches directory with provided ZIP file or stream.
* Patches are applied in fail-fast mode, application will be aborted on first wrong hash-sum or IO error.
* <ul>
<li>takes directory to patch and patch file (or stream)</li>
<li>reads '.index' file and using it for futher steps:</li>
<li>checks hash sums for 'unchanged' files</li>
<li>reads from stream 'added' files, puts them into directory checking hash sums</li>
<li>check hash sums for 'updated' files</li>
<li>reads '.gdiff' patches from stream, applies them, checks hash sums for applied files</li>
<li>checks hash sums for 'deleted' files</li>
<li>deletes 'deleted' files</li>
</ul>
*
* @author alexkasko
* Date: 11/19/11
*/
public class DirDeltaPatcher {

    /**
     * Applies patch file to directory
     *
     * @param dir target directory
     * @param patch ZIP patch file
     * @throws IOException on any io or consistency problem
     */
    public void patch(File dir, File patch) throws IOException {
        ZipInputStream zis = null;
        try {
            zis = new ZipInputStream(openInputStream(patch));
            patch(dir, zis);
        } finally {
            IOUtils.closeQuietly(zis);
        }
    }

    /**
     * Applies patch stream to directory
     *
     * @param dir target directory
     * @param patch ZIP patch stream
     * @throws IOException on any io or consistency problem
     */
    public void patch(File dir, ZipInputStream patch) throws IOException {
        DeltaIndex index = readIndex(patch);
        check(index.unchanged, dir);
        create(index.created, dir, patch);
        update(index.updated, dir, patch);
        delete(index.deleted, dir);
    }

    private DeltaIndex readIndex(ZipInputStream patch) throws IOException {
        ZipEntry indexEntry = patch.getNextEntry();
        checkArgument(indexEntry.getName().startsWith(".index"), "Unexpected index file name: '{}', must start with '.index'");
        LineIterator iter = IOUtils.lineIterator(patch, "UTF-8");
        List<? extends IndexEntry> entries = ImmutableList.copyOf(Iterators.transform(iter, new IndexEntryMapper()));
        patch.closeEntry();
        ImmutableList<IndexEntry.Unchanged> unchanged = ImmutableList.copyOf(Iterables.filter(entries, IndexEntry.Unchanged.class));
        ImmutableList<IndexEntry.Created> created = ImmutableList.copyOf(Iterables.filter(entries, IndexEntry.Created.class));
        ImmutableList<IndexEntry.Updated> updated = ImmutableList.copyOf(Iterables.filter(entries, IndexEntry.Updated.class));
        ImmutableList<IndexEntry.Deleted> deleted = ImmutableList.copyOf(Iterables.filter(entries, IndexEntry.Deleted.class));
        return new DeltaIndex(created, deleted, updated, unchanged);
    }

    private void check(List<IndexEntry.Unchanged> index, File dir) throws IOException {
        for (IndexEntry.Unchanged en : index) {
            File file = new File(dir, en.path);
            if(!(file.exists() && file.isFile())) throw new FileNotFoundException(file.toString());
            String sha1 = computeSha1(file);
            if(!sha1.equals(en.oldSha1)) throw new IOException("UNCHANGED file check failed for file: " + file);
        }
    }

    private void create(List<IndexEntry.Created> index, File dir, ZipInputStream patch) throws IOException {
        for (IndexEntry.Created en : index) {
            ZipEntry entry = patch.getNextEntry();
            checkState(en.path.equals(entry.getName()), "Index and zipstream unsynchronized, index: " + en.path + ", zipstream: " + entry.getName());
            File file = new File(dir, en.path);
            if (file.exists()) throw new IOException("CREATED file already exists: " + file);
            String sha1 = copyStreamToFileWithDigest(patch, file);
            if (!sha1.equals(en.newSha1)) throw new IOException("CREATED file check failed for file: " + file);
            patch.closeEntry();
        }
    }

    private void update(List<IndexEntry.Updated> index, File dir, ZipInputStream patch) throws IOException {
        for (IndexEntry.Updated en : index) {
            ZipEntry entry = patch.getNextEntry();
            checkState((en.path + ".gdiff").equals(entry.getName()), "Index and zipstream unsynchronized, index: " + en.path + ", zipstream: " + entry.getName());
            File file = new File(dir, en.path);
            if (!file.exists()) throw new IOException("UPDATED file doesn't exist: " + file);
            String sha1old = computeSha1(file);
            if(!sha1old.equals(en.oldSha1)) throw new IOException("UPDATED file check failed for old file: " + file);
            File patched = new File(dir, en.path + UUID.randomUUID().toString());
            patch(file, patch, patched);
            String sha1new = computeSha1(patched);
            if(!sha1new.equals(en.newSha1)) throw new IOException("UPDATED file check failed for new file: " + file);
            boolean deleted = file.delete();
            checkState(deleted, "Delete file unsuccessful: " + file);
            boolean renamed = patched.renameTo(file);
            checkState(renamed, "Rename to file unsuccessful: " + file);
            patch.closeEntry();
        }
    }

    private void delete(List<IndexEntry.Deleted> index, File dir) throws IOException {
        for (IndexEntry.Deleted en : index) {
            File file = new File(dir, en.path);
            if (!file.exists()) throw new IOException("DELETED file doesn't exist: " + file);
            String sha1old = computeSha1(file);
            if(!sha1old.equals(en.oldSha1)) throw new IOException("DELETED file check failed old file: " + file);
            boolean deleted = file.delete();
            checkState(deleted, "Cannot delete file: " + file);
        }
    }

    private String copyStreamToFileWithDigest(InputStream stream, File file) throws IOException {
        DigestOutputStream output = null;
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            OutputStream outputStream = new BufferedOutputStream(openOutputStream(file));
            output = new DigestOutputStream(outputStream, md);
            IOUtils.copyLarge(stream, output);
            output.flush();
            byte[] bytes = output.getMessageDigest().digest();
            return hex(bytes);
        } catch (NoSuchAlgorithmException e) {
            throw new UnhandledException(e);
        } finally {
            IOUtils.closeQuietly(output);
        }
    }

    private void patch(File file, InputStream patch, File patched) throws IOException {
        OutputStream out = null;
        RandomAccessFile raf = null;
        try {
            raf = new RandomAccessFile(file, "r");
            RandomAccessFileSeekableSource source = new RandomAccessFileSeekableSource(raf);
            out = new BufferedOutputStream(new FileOutputStream(patched));
            new GDiffPatcher().patch(source, patch, out);
        } finally {
            IOUtils.closeQuietly(out);
            IOUtils.closeQuietly(raf);
        }
    }

    private class IndexEntryMapper implements Function<String, IndexEntry> {
        private final Gson gson = new Gson();

        @Override
        public IndexEntry apply(String input) {
            Type mapType = new TypeToken<HashMap<String, String>>() {}.getType();
            Map<String, String> map = gson.fromJson(input, mapType);
            String state = map.get("state");
            String path = map.get("path");
            String oldSha1 = map.get("oldSha1");
            String newSha1 = map.get("newSha1");
            if(UNCHANGED.name().equals(state)) return new IndexEntry.Unchanged(path, oldSha1, newSha1);
            if(CREATED.name().equals(state)) return new IndexEntry.Created(path, oldSha1, newSha1);
            if(UPDATED.name().equals(state)) return new IndexEntry.Updated(path, oldSha1, newSha1);
            if(DELETED.name().equals(state)) return new IndexEntry.Deleted(path, oldSha1, newSha1);
            throw new IllegalStateException("Cannot parse index entry from line: " + input);
        }
    }
}
TOP

Related Classes of com.alexkasko.delta.DirDeltaPatcher

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.