├── .gitignore ├── delta-patch ├── src │ ├── test │ │ ├── resources │ │ │ └── diff.zip │ │ └── java │ │ │ └── com │ │ │ └── alexkasko │ │ │ └── delta │ │ │ └── PatchTest.java │ └── main │ │ └── java │ │ └── com │ │ └── alexkasko │ │ └── delta │ │ ├── PatchLauncher.java │ │ └── DirDeltaPatcher.java └── pom.xml ├── delta-common ├── src │ └── main │ │ └── java │ │ └── com │ │ └── alexkasko │ │ └── delta │ │ ├── NullOutputStream.java │ │ ├── DeltaIndex.java │ │ ├── HashUtils.java │ │ ├── NoCloseOutputStream.java │ │ └── IndexEntry.java └── pom.xml ├── LICENSE ├── assembly.xml ├── delta-diff ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── alexkasko │ │ │ └── delta │ │ │ ├── DiffLauncher.java │ │ │ └── DirDeltaCreator.java │ └── test │ │ └── java │ │ └── com │ │ └── alexkasko │ │ └── delta │ │ └── DiffTest.java └── pom.xml ├── README.md └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | *.ipr 3 | *.iml 4 | *.iws 5 | /*/target -------------------------------------------------------------------------------- /delta-patch/src/test/resources/diff.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexkasko/delta-updater/HEAD/delta-patch/src/test/resources/diff.zip -------------------------------------------------------------------------------- /delta-common/src/main/java/com/alexkasko/delta/NullOutputStream.java: -------------------------------------------------------------------------------- 1 | package com.alexkasko.delta; 2 | 3 | import java.io.OutputStream; 4 | 5 | /** 6 | * alexkasko: Borrowed from Guava, was removed in recent Guava versions. 7 | * Implementation of {@link java.io.OutputStream} that simply discards written bytes. 8 | * 9 | * @author Spencer Kimball 10 | * @since 1.0 11 | */ 12 | public final class NullOutputStream extends OutputStream { 13 | /** Discards the specified byte. */ 14 | @Override public void write(int b) { 15 | } 16 | 17 | /** Discards the specified byte array. */ 18 | @Override public void write(byte[] b, int off, int len) { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /delta-common/src/main/java/com/alexkasko/delta/DeltaIndex.java: -------------------------------------------------------------------------------- 1 | package com.alexkasko.delta; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import com.google.common.collect.Iterables; 5 | 6 | /** 7 | * User: alexkasko 8 | * Date: 11/18/11 9 | */ 10 | class DeltaIndex { 11 | final ImmutableList unchanged; 12 | final ImmutableList created; 13 | final ImmutableList updated; 14 | final ImmutableList deleted; 15 | 16 | DeltaIndex(ImmutableList created, ImmutableList deleted, 17 | ImmutableList updated, ImmutableList unchanged) { 18 | this.unchanged = unchanged; 19 | this.created = created; 20 | this.updated = updated; 21 | this.deleted = deleted; 22 | } 23 | 24 | Iterable getAll() { 25 | return Iterables.concat(unchanged, created, updated, deleted); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012 Alex Kasko (alexkasko.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /assembly.xml: -------------------------------------------------------------------------------- 1 | 5 | distr 6 | false 7 | 8 | 9 | / 10 | ${artifact.artifactId}.${artifact.extension} 11 | false 12 | true 13 | true 14 | 15 | ${project.groupId}:${project.artifactId}:${project.packaging}:* 16 | 17 | 18 | 19 | lib 20 | false 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /delta-patch/src/main/java/com/alexkasko/delta/PatchLauncher.java: -------------------------------------------------------------------------------- 1 | package com.alexkasko.delta; 2 | 3 | import org.apache.commons.cli.*; 4 | 5 | import java.io.File; 6 | 7 | import static java.lang.System.out; 8 | 9 | /** 10 | * Delta patch launcher class 11 | * 12 | * @author alexkasko 13 | * Date: 11/19/11 14 | */ 15 | public class PatchLauncher { 16 | 17 | private static final String HELP_OPTION = "help"; 18 | 19 | /** 20 | * app entry point 21 | */ 22 | public static void main(String[] args) throws Exception { 23 | Options options = new Options(); 24 | try { 25 | options.addOption("h", HELP_OPTION, false, "show this page"); 26 | CommandLine cline = new GnuParser().parse(options, args); 27 | String[] argList = cline.getArgs(); 28 | if (cline.hasOption(HELP_OPTION)) { 29 | throw new ParseException("Printing help page:"); 30 | } else if(2 == argList.length) { 31 | new DirDeltaPatcher().patch(new File(argList[0]), new File(argList[1])); 32 | } else { 33 | throw new ParseException("Incorrect arguments received!"); 34 | } 35 | } catch (ParseException e) { 36 | HelpFormatter formatter = new HelpFormatter(); 37 | out.println(e.getMessage()); 38 | formatter.printHelp("java -jar delta-patch.jar dir patch.zip", options); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /delta-common/src/main/java/com/alexkasko/delta/HashUtils.java: -------------------------------------------------------------------------------- 1 | package com.alexkasko.delta; 2 | 3 | import org.apache.commons.io.IOUtils; 4 | import org.apache.commons.lang.UnhandledException; 5 | 6 | import java.io.File; 7 | import java.io.FileInputStream; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.math.BigInteger; 11 | import java.security.DigestInputStream; 12 | import java.security.MessageDigest; 13 | import java.security.NoSuchAlgorithmException; 14 | 15 | /** 16 | * User: alexkasko 17 | * Date: 11/19/11 18 | */ 19 | class HashUtils { 20 | static String computeSha1(File file) { 21 | InputStream is = null; 22 | try { 23 | is = new FileInputStream(file); 24 | MessageDigest md = MessageDigest.getInstance("SHA-1"); 25 | DigestInputStream dis = new DigestInputStream(is, md); 26 | IOUtils.copyLarge(dis, new NullOutputStream()); 27 | dis.close(); 28 | byte[] bytes = dis.getMessageDigest().digest(); 29 | return hex(bytes); 30 | } catch (NoSuchAlgorithmException e) { 31 | throw new UnhandledException(e); 32 | } catch (IOException e) { 33 | throw new UnhandledException(e); 34 | } finally { 35 | IOUtils.closeQuietly(is); 36 | } 37 | } 38 | 39 | // http://stackoverflow.com/a/3940857/314015 40 | static String hex(byte[] data) { 41 | return String.format("%040x", new BigInteger(1, data)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /delta-common/src/main/java/com/alexkasko/delta/NoCloseOutputStream.java: -------------------------------------------------------------------------------- 1 | package com.alexkasko.delta; 2 | 3 | import java.io.IOException; 4 | import java.io.OutputStream; 5 | 6 | import static com.google.common.base.Preconditions.checkNotNull; 7 | 8 | /** 9 | * Output stream transparent wrapper, {@link java.io.OutputStream#close()} 10 | * and {@link java.io.OutputStream#flush()} overriden as NOOP. 11 | * May be used to prevent rough libs from closing or flushing your streams 12 | * 13 | * @author alexkasko 14 | * Date: 11/19/11 15 | */ 16 | public class NoCloseOutputStream extends OutputStream { 17 | private final OutputStream target; 18 | 19 | /** 20 | * @param target target stream 21 | */ 22 | public NoCloseOutputStream(OutputStream target) { 23 | checkNotNull(target, "Provided output stream is null"); 24 | this.target = target; 25 | } 26 | 27 | /** 28 | * {@inheritDoc} 29 | */ 30 | @Override 31 | public void write(int b) throws IOException { 32 | target.write(b); 33 | } 34 | 35 | /** 36 | * {@inheritDoc} 37 | */ 38 | @Override 39 | public void write(byte[] b, int off, int len) throws IOException { 40 | target.write(b, off, len); 41 | } 42 | 43 | /** 44 | * {@inheritDoc} 45 | */ 46 | @Override 47 | public void flush() throws IOException { 48 | // this line intentionally left blank 49 | } 50 | 51 | /** 52 | * {@inheritDoc} 53 | */ 54 | @Override 55 | public void close() throws IOException { 56 | // this line intentionally left blank 57 | } 58 | } -------------------------------------------------------------------------------- /delta-common/src/main/java/com/alexkasko/delta/IndexEntry.java: -------------------------------------------------------------------------------- 1 | package com.alexkasko.delta; 2 | 3 | import org.apache.commons.lang.builder.ToStringBuilder; 4 | 5 | import static org.apache.commons.lang.builder.ToStringStyle.SHORT_PREFIX_STYLE; 6 | 7 | /** 8 | * User: alexkasko 9 | * Date: 11/18/11 10 | */ 11 | abstract class IndexEntry { 12 | enum State {UNCHANGED, CREATED, UPDATED, DELETED} 13 | final String path; 14 | final State state; 15 | final String oldSha1; 16 | final String newSha1; 17 | 18 | protected IndexEntry(String path, State state, String oldSha1, String newSha1) { 19 | this.path = path; 20 | this.state = state; 21 | this.oldSha1 = oldSha1; 22 | this.newSha1 = newSha1; 23 | } 24 | 25 | // children for type safety, easy filtering etc 26 | 27 | static class Created extends IndexEntry { 28 | public Created(String path, String oldSha1, String newSha1) { 29 | super(path, State.CREATED, oldSha1, newSha1); 30 | } 31 | } 32 | 33 | static class Deleted extends IndexEntry { 34 | public Deleted(String path, String oldSha1, String newSha1) { 35 | super(path, State.DELETED, oldSha1, newSha1); 36 | } 37 | } 38 | 39 | static class Updated extends IndexEntry { 40 | public Updated(String path,String oldSha1, String newSha1) { 41 | super(path, State.UPDATED, oldSha1, newSha1); 42 | } 43 | } 44 | 45 | static class Unchanged extends IndexEntry { 46 | public Unchanged(String path, String oldSha1, String newSha1) { 47 | super(path, State.UNCHANGED, oldSha1, newSha1); 48 | } 49 | } 50 | 51 | @Override 52 | public String toString() { 53 | return ToStringBuilder.reflectionToString(this, SHORT_PREFIX_STYLE); 54 | } 55 | } 56 | 57 | 58 | -------------------------------------------------------------------------------- /delta-common/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.alexkasko.delta 6 | delta-parent 7 | 1.1.4-SNAPSHOT 8 | 9 | delta-common 10 | jar 11 | Delta Updater Common Library 12 | 13 | 14 | Common library for utilities and library for creating and applying binary 15 | patches to file system directories. 16 | 17 | https://github.com/alexkasko/delta-updater 18 | 19 | 20 | MIT License 21 | http://opensource.org/licenses/mit-license.php 22 | 23 | 24 | 25 | https://github.com/alexkasko/delta-updater 26 | scm:git:https://github.com/alexkasko/delta-updater.git 27 | scm:git:https://github.com/alexkasko/delta-updater.git 28 | HEAD 29 | 30 | 31 | 32 | Alex Kasko 33 | mail@alexkasko.com 34 | http://alexkasko.com 35 | 36 | 37 | 38 | 39 | 40 | com.google.guava 41 | guava 42 | 10.0 43 | 44 | 45 | commons-lang 46 | commons-lang 47 | 2.6 48 | 49 | 50 | commons-io 51 | commons-io 52 | 2.1 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /delta-diff/src/main/java/com/alexkasko/delta/DiffLauncher.java: -------------------------------------------------------------------------------- 1 | package com.alexkasko.delta; 2 | 3 | import org.apache.commons.cli.*; 4 | import org.apache.commons.io.IOCase; 5 | 6 | import java.io.File; 7 | 8 | import static java.lang.System.out; 9 | 10 | /** 11 | * Delta diff launcher class 12 | * 13 | * @author alexkasko 14 | * Date: 11/18/11 15 | */ 16 | public class DiffLauncher { 17 | private static final String HELP_OPTION = "help"; 18 | private static final String OUTPUT_OPTION = "out"; 19 | private static final String CASE_SENSITIVE_OPTION = "case-sensitive"; 20 | 21 | /** 22 | * app entry point 23 | */ 24 | public static void main(String[] args) throws Exception { 25 | Options options = new Options(); 26 | try { 27 | options.addOption("h", HELP_OPTION, false, "show this page"); 28 | options.addOption("o", OUTPUT_OPTION, true, "output file path"); 29 | options.addOption("c", CASE_SENSITIVE_OPTION, true, "case sensitive [y/n]"); 30 | CommandLine cline = new GnuParser().parse(options, args); 31 | String[] argList = cline.getArgs(); 32 | if (cline.hasOption(HELP_OPTION)) { 33 | throw new ParseException("Printing help page:"); 34 | } else if(2 == argList.length && cline.hasOption(OUTPUT_OPTION)) { 35 | final IOCase caseSensitive; 36 | if (cline.hasOption(CASE_SENSITIVE_OPTION)) { 37 | String val = cline.getOptionValue(CASE_SENSITIVE_OPTION); 38 | if ("y".equalsIgnoreCase(val)) { 39 | caseSensitive = IOCase.SENSITIVE; 40 | } else if ("n".equalsIgnoreCase(val)) { 41 | caseSensitive = IOCase.INSENSITIVE; 42 | } else { 43 | throw new ParseException("Invalid case arg: [" + val + "], should be [y] or [n]"); 44 | } 45 | } else caseSensitive = IOCase.SYSTEM; 46 | new DirDeltaCreator().create(new File(argList[0]), new File(argList[1]), new File(cline.getOptionValue(OUTPUT_OPTION)), caseSensitive); 47 | } else { 48 | throw new ParseException("Incorrect arguments received!"); 49 | } 50 | } catch (ParseException e) { 51 | HelpFormatter formatter = new HelpFormatter(); 52 | out.println(e.getMessage()); 53 | formatter.printHelp("java -jar delta-diff.jar [-c y/n] dir1 dir2 -o out.zip", options); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /delta-patch/src/test/java/com/alexkasko/delta/PatchTest.java: -------------------------------------------------------------------------------- 1 | package com.alexkasko.delta; 2 | 3 | import org.junit.Test; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.util.zip.ZipInputStream; 9 | 10 | import static java.lang.System.currentTimeMillis; 11 | import static java.lang.System.getProperty; 12 | import static junit.framework.Assert.assertTrue; 13 | import static org.apache.commons.io.FileUtils.deleteDirectory; 14 | import static org.apache.commons.io.FileUtils.writeStringToFile; 15 | import static org.apache.commons.io.IOUtils.closeQuietly; 16 | import static org.junit.Assert.assertEquals; 17 | import static com.alexkasko.delta.HashUtils.computeSha1; 18 | 19 | /** 20 | * User: alexkasko 21 | * Date: 10/26/12 22 | */ 23 | public class PatchTest { 24 | @Test 25 | public void test() throws IOException { 26 | File tmpdir = null; 27 | InputStream diff = null; 28 | try { 29 | tmpdir = createTmpDir(); 30 | // prepare source dir 31 | File source = new File(tmpdir, "source"); 32 | assertTrue("Cannot create tmp directory", source.mkdirs()); 33 | File unchanged = new File(source, "unchanged"); 34 | writeStringToFile(unchanged, "foo", "UTF-8"); 35 | writeStringToFile(new File(source, "deleted"), "bar", "UTF-8"); 36 | File updated = new File(source, "updated"); 37 | writeStringToFile(updated, "baz", "UTF-8"); 38 | File added = new File(source, "added"); 39 | // load patch 40 | diff = PatchTest.class.getResourceAsStream("/diff.zip"); 41 | // apply patch 42 | new DirDeltaPatcher().patch(source, new ZipInputStream(diff)); 43 | // check results 44 | assertEquals("Files count fail", 3, source.listFiles().length); 45 | assertEquals("Unchanged fail", "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33", computeSha1(unchanged)); 46 | assertEquals("Updated fail", "020d4b62f2af4547cdf0c28e2fd937bfc28a3787", computeSha1(updated)); 47 | assertEquals("Added fail", "92cfceb39d57d914ed8b14d0e37643de0797ae56", computeSha1(added)); 48 | } finally { 49 | if(null != tmpdir) deleteDirectory(tmpdir); 50 | closeQuietly(diff); 51 | } 52 | } 53 | 54 | private File createTmpDir() { 55 | File baseDir = new File(getProperty("java.io.tmpdir")); 56 | String baseName = getClass().getName() + "_" + currentTimeMillis() + ".tmp"; 57 | File tmp = new File(baseDir, baseName); 58 | assertTrue("Cannot create tmp directory", tmp.mkdirs()); 59 | return tmp; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /delta-diff/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.alexkasko.delta 5 | delta-parent 6 | 1.1.4-SNAPSHOT 7 | 8 | 4.0.0 9 | delta-diff 10 | jar 11 | Directory Delta Diff Utility 12 | 13 | 14 | Utility and library for creating binary patches comparing file system directories 15 | 16 | https://github.com/alexkasko/delta-updater 17 | 18 | 19 | MIT License 20 | http://opensource.org/licenses/mit-license.php 21 | 22 | 23 | 24 | https://github.com/alexkasko/delta-updater 25 | scm:git:https://github.com/alexkasko/delta-updater.git 26 | scm:git:https://github.com/alexkasko/delta-updater.git 27 | HEAD 28 | 29 | 30 | 31 | Alex Kasko 32 | mail@alexkasko.com 33 | http://alexkasko.com 34 | 35 | 36 | 37 | 38 | com.alexkasko.delta.DiffLauncher 39 | 40 | 41 | 42 | 43 | ${project.groupId} 44 | delta-common 45 | ${project.version} 46 | 47 | 48 | com.nothome 49 | javaxdelta 50 | 2.0.1 51 | 52 | 53 | com.google.guava 54 | guava 55 | 10.0 56 | 57 | 58 | commons-lang 59 | commons-lang 60 | 2.1 61 | 62 | 63 | commons-io 64 | commons-io 65 | 2.1 66 | 67 | 68 | commons-cli 69 | commons-cli 70 | 1.2 71 | 72 | 73 | com.google.code.gson 74 | gson 75 | 2.1 76 | 77 | 78 | 79 | junit 80 | junit 81 | 4.8.1 82 | test 83 | 84 | 85 | 86 | 87 | 88 | 89 | org.apache.maven.plugins 90 | maven-assembly-plugin 91 | 2.2.1 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /delta-patch/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.alexkasko.delta 6 | delta-parent 7 | 1.1.4-SNAPSHOT 8 | 9 | delta-patch 10 | jar 11 | Directory Delta Patch Utility 12 | 13 | 14 | Utility and library for applying binary patches to file system directories 15 | 16 | https://github.com/alexkasko/delta-updater 17 | 18 | 19 | MIT License 20 | http://opensource.org/licenses/mit-license.php 21 | 22 | 23 | 24 | https://github.com/alexkasko/delta-updater 25 | scm:git:https://github.com/alexkasko/delta-updater.git 26 | scm:git:https://github.com/alexkasko/delta-updater.git 27 | HEAD 28 | 29 | 30 | 31 | Alex Kasko 32 | mail@alexkasko.com 33 | http://alexkasko.com 34 | 35 | 36 | 37 | 38 | com.alexkasko.delta.PatchLauncher 39 | 40 | 41 | 42 | 43 | ${project.groupId} 44 | delta-common 45 | ${project.version} 46 | 47 | 48 | com.nothome 49 | javaxdelta 50 | 2.0.1 51 | 52 | 53 | com.google.guava 54 | guava 55 | 10.0 56 | 57 | 58 | commons-lang 59 | commons-lang 60 | 2.1 61 | 62 | 63 | commons-io 64 | commons-io 65 | 2.1 66 | 67 | 68 | commons-cli 69 | commons-cli 70 | 1.2 71 | 72 | 73 | com.google.code.gson 74 | gson 75 | 2.1 76 | 77 | 78 | 79 | junit 80 | junit 81 | 4.8.1 82 | test 83 | 84 | 85 | 86 | 87 | 88 | 89 | org.apache.maven.plugins 90 | maven-assembly-plugin 91 | 2.2.1 92 | 93 | 94 | org.apache.maven.plugins 95 | maven-source-plugin 96 | 2.1.2 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /delta-diff/src/test/java/com/alexkasko/delta/DiffTest.java: -------------------------------------------------------------------------------- 1 | package com.alexkasko.delta; 2 | 3 | import com.alexkasko.delta.DirDeltaCreator; 4 | import org.apache.commons.io.IOUtils; 5 | import org.junit.Test; 6 | 7 | import java.io.*; 8 | import java.math.BigInteger; 9 | import java.security.MessageDigest; 10 | import java.security.NoSuchAlgorithmException; 11 | import java.security.NoSuchProviderException; 12 | import java.util.zip.ZipEntry; 13 | import java.util.zip.ZipInputStream; 14 | 15 | import static com.alexkasko.delta.HashUtils.hex; 16 | import static java.lang.System.currentTimeMillis; 17 | import static junit.framework.Assert.assertTrue; 18 | import static org.apache.commons.io.FileUtils.deleteDirectory; 19 | import static org.apache.commons.io.FileUtils.writeStringToFile; 20 | import static org.apache.commons.io.filefilter.TrueFileFilter.TRUE; 21 | import static org.junit.Assert.assertEquals; 22 | 23 | /** 24 | * User: alexkasko 25 | * Date: 10/26/12 26 | */ 27 | public class DiffTest { 28 | @Test 29 | public void test() throws IOException, NoSuchProviderException, NoSuchAlgorithmException { 30 | File tmpdir = null; 31 | try { 32 | tmpdir = createTmpDir(); 33 | // prepare source dir 34 | File source = new File(tmpdir, "source"); 35 | assertTrue("Cannot create tmp directory", source.mkdirs()); 36 | writeStringToFile(new File(source, "unchanged"), "foo", "UTF-8"); 37 | writeStringToFile(new File(source, "deleted"), "bar", "UTF-8"); 38 | writeStringToFile(new File(source, "updated"), "baz", "UTF-8"); 39 | // prepare target dir 40 | File target = new File(tmpdir, "target"); 41 | writeStringToFile(new File(target, "added"), "42", "UTF-8"); 42 | writeStringToFile(new File(target, "unchanged"), "foo", "UTF-8"); 43 | writeStringToFile(new File(target, "updated"), "ba42", "UTF-8"); 44 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 45 | // create diff 46 | new DirDeltaCreator().create(source, target, TRUE, baos); 47 | // read diff as zip 48 | byte[] delta = baos.toByteArray(); 49 | ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(delta)); 50 | // check contents 51 | ZipEntry indexEntry = zis.getNextEntry(); 52 | assertTrue("Index name fail", indexEntry.getName().startsWith(".index_")); 53 | assertEquals("Index body fail", "4cd7b00b116611d04981d72d07b056f05bbf424c", hash(zis)); 54 | zis.closeEntry(); 55 | ZipEntry addEntry = zis.getNextEntry(); 56 | assertEquals("Added name fail", "added", addEntry.getName()); 57 | assertEquals("Added body fail", "92cfceb39d57d914ed8b14d0e37643de0797ae56", hash(zis)); 58 | zis.closeEntry(); 59 | ZipEntry updatedEntry = zis.getNextEntry(); 60 | assertEquals("Updated name fail", "updated.gdiff", updatedEntry.getName()); 61 | assertEquals("Updated body fail", "1881f0e90abaa44fd54f35b95ad3ef8ebc44ffd0", hash(zis)); 62 | zis.closeEntry(); 63 | zis.close(); 64 | } finally { 65 | if(null != tmpdir) deleteDirectory(tmpdir); 66 | } 67 | } 68 | 69 | private File createTmpDir() { 70 | File baseDir = new File(System.getProperty("java.io.tmpdir")); 71 | String baseName = getClass().getName() + "_" + currentTimeMillis() + ".tmp"; 72 | File tmp = new File(baseDir, baseName); 73 | assertTrue("Cannot create tmp directory", tmp.mkdirs()); 74 | return tmp; 75 | } 76 | 77 | private String hash(InputStream is) throws NoSuchProviderException, NoSuchAlgorithmException, IOException { 78 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 79 | IOUtils.copy(is, baos); 80 | MessageDigest sha1 = MessageDigest.getInstance("SHA-1", "SUN"); 81 | byte[] digest = sha1.digest(baos.toByteArray()); 82 | return hex(digest); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Binary delta patches for directories 2 | ==================================== 3 | 4 | Utilities and library for creating and applying binary patches to file system directories. 5 | It's based on [Javaxdelta](http://javaxdelta.sourceforge.net/) library - java implementation of [xdelta](http://xdelta.org/) binary diff algorithm. 6 | [GDIFF](http://www.w3.org/TR/NOTE-gdiff-19970901) format is used for patches. 7 | 8 | It can be used to implement auto-updating for rich-client applications. 9 | 10 | Delta-diff and delta-patch utilities are available: 11 | 12 | - [delta-diff](https://bitbucket.org/alexkasko/share/downloads/delta-diff-1.1.3-distr.tar.gz) 13 | - [delta-patch](https://bitbucket.org/alexkasko/share/downloads/delta-patch-1.1.3-distr.tar.gz) 14 | 15 | Libraries are available in [Maven cental](http://repo1.maven.org/maven2/com/alexkasko/delta/). 16 | 17 | Javadocs: [delta-diff](http://alexkasko.github.com/delta-updater/diff-javadocs), 18 | [delta-patch](http://alexkasko.github.com/delta-updater/patch-javadocs) 19 | 20 | ###Features 21 | 22 | - supports directories with arbitrary structure 23 | - supports changed, added and deleted files 24 | - directory patch includes human readable '.index' file 25 | - streaming patch creation and applying 26 | - fail-fast patch applying with hash sum checks 27 | - pure java, tested on linux and windows 28 | 29 | ###Directory patch creation 30 | 31 | - takes two directories (to create delta between) and `IOFilter` to include/exclude files 32 | - creates ZIP file (or stream) with GDIFF deltas for all changed files and '.index' text file (with '.index_' prefix) 33 | with list of unchanged, added, updated and deleted files with SHA1 hash sums 34 | 35 | ###Patch application 36 | 37 | Patches are applied in fail-fast mode, application will be aborted on first wrong hash-sum or IO error. 38 | 39 | - takes directory to patch and patch file (or stream) 40 | - reads '.index' file and using it for futher steps: 41 | - checks hash sums for 'unchanged' files 42 | - reads from stream 'added' files, puts them into directory checking hash sums 43 | - check hash sums for 'updated' files 44 | - reads '.gdiff' patches from stream, applies them, checks hash sums for applied files 45 | - checks hash sums for 'deleted' files 46 | - deletes 'deleted' files 47 | 48 | Using library 49 | ------------- 50 | 51 | Maven dependency (available in central repository): 52 | 53 | 54 | com.alexkasko.delta 55 | delta-diff 56 | 57 | 1.1.3 58 | 59 | 60 | For patch file/stream creation you should use one of these methods: 61 | 62 | new DirDeltaCreator().create(oldDirectory, newDirectory, patchFile); 63 | new DirDeltaCreator().create(oldDirectory, newDirectory, filesFilter, patchOutputStream); 64 | 65 | For patch application (it will throw `IOException` on hash-sum error): 66 | 67 | new DirDeltaPatcher().patch(directory, patchFile); 68 | new DirDeltaPatcher().patch(directory, patchZipInputStream); 69 | 70 | Both `DirDeltaCreator` and `DirDeltaPatcher` are thread-safe (stateless). 71 | 72 | Using utilities 73 | --------------- 74 | 75 | `delta-diff` and `delta-patch` programs (they will be put into `delta-updater/delta-xxx/target/delta-xxx-yyy-distr`) can be used as 76 | command line utilities. 77 | 78 | Patch creation: 79 | 80 | java -jar delta-diff.jar dir1 dir2 -o patch.zip 81 | 82 | Patch application: 83 | 84 | java -jar delta-patch.jar dir patch.zip 85 | 86 | How to build 87 | ------------ 88 | 89 | All dependencies are in Maven Central. To build project run: 90 | 91 | mvn clean install 92 | 93 | License Information 94 | ------------------- 95 | 96 | _Note: javaxdelta depends on GNU Trove 1.0.2 library which is released under the [LGPL license](http://www.gnu.org/licenses/lgpl-2.1.html)_. 97 | 98 | This project is released under the [MIT License](http://www.opensource.org/licenses/mit-license.php) 99 | (the same license is used by javaxdelta project). 100 | 101 | Changelog 102 | --------- 103 | 104 | **1.1.3** (2014-10-21) 105 | 106 | * fix typos in error messages (#2) 107 | 108 | **1.1.2** (2014-08-09) 109 | 110 | * add support for case-sensivity flag (#1) 111 | 112 | **1.1.1** (2014-01-31) 113 | 114 | * remove `NullOutputStream` usage to support recent versions of Guava 115 | 116 | **1.1** (2012-10-26) 117 | 118 | * code and dependencies cleanup, maven central upload 119 | 120 | **1.0** (2011-11-20) 121 | 122 | * initial version -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.sonatype.oss 6 | oss-parent 7 | 7 8 | 9 | com.alexkasko.delta 10 | delta-parent 11 | pom 12 | 1.1.4-SNAPSHOT 13 | Delta Updater Application 14 | 15 | 16 | Utilities and library for creating and applying binary patches to file system directories 17 | 18 | https://github.com/alexkasko/delta-updater 19 | 20 | 21 | MIT License 22 | http://opensource.org/licenses/mit-license.php 23 | 24 | 25 | 26 | https://github.com/alexkasko/delta-updater 27 | scm:git:https://github.com/alexkasko/delta-updater.git 28 | scm:git:https://github.com/alexkasko/delta-updater.git 29 | HEAD 30 | 31 | 32 | 33 | Alex Kasko 34 | mail@alexkasko.com 35 | http://alexkasko.com 36 | 37 | 38 | 39 | 40 | delta-diff 41 | delta-patch 42 | delta-common 43 | 44 | 45 | 46 | UTF-8 47 | Alex Kasko 48 | ${project.specification_vendor} 49 | 50 | 51 | 52 | 53 | 54 | 55 | org.apache.maven.plugins 56 | maven-compiler-plugin 57 | 2.3.1 58 | 59 | 1.6 60 | 1.6 61 | 62 | 63 | 64 | org.apache.maven.plugins 65 | maven-deploy-plugin 66 | 2.7 67 | 68 | 69 | org.apache.maven.plugins 70 | maven-jar-plugin 71 | 2.3.2 72 | 73 | 74 | 75 | ${main.class} 76 | true 77 | lib 78 | 79 | 80 | ${project.name} 81 | ${project.version} 82 | ${project.specification_vendor} 83 | ${project.groupId}.${project.artifactId} 84 | ${git.revision} 85 | ${project.implementation_vendor} 86 | ${git.branch} 87 | ${git.tag} 88 | ${git.commitsCount} 89 | 90 | 91 | 92 | 93 | 94 | org.apache.maven.plugins 95 | maven-assembly-plugin 96 | 2.2.1 97 | 98 | 99 | ${project.parent.basedir}/assembly.xml 100 | 101 | 102 | 103 | 104 | package 105 | 106 | directory-single 107 | 108 | package 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | ru.concerteza.buildnumber 117 | maven-jgit-buildnumber-plugin 118 | 1.2.6 119 | 120 | 121 | git-buildnumber 122 | 123 | extract-buildnumber 124 | 125 | prepare-package 126 | 127 | 128 | 129 | 130 | org.apache.maven.plugins 131 | maven-source-plugin 132 | 2.1.2 133 | 134 | 135 | attach-sources 136 | 137 | jar 138 | 139 | 140 | 141 | 142 | 143 | org.apache.maven.plugins 144 | maven-javadoc-plugin 145 | 2.8.1 146 | 147 | 148 | attach-javadoc 149 | 150 | jar 151 | 152 | 153 | true 154 | true 155 | 156 | 157 | 158 | 159 | 160 | org.apache.maven.plugins 161 | maven-gpg-plugin 162 | 1.4 163 | 164 | 165 | sign-artifacts 166 | verify 167 | 168 | sign 169 | 170 | 171 | nopwd 172 | 173 | 174 | 175 | 176 | 177 | org.apache.maven.plugins 178 | maven-release-plugin 179 | 2.3.2 180 | 181 | false 182 | @{project.version} 183 | true 184 | 185 | 186 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /delta-patch/src/main/java/com/alexkasko/delta/DirDeltaPatcher.java: -------------------------------------------------------------------------------- 1 | package com.alexkasko.delta; 2 | 3 | import com.google.common.base.Function; 4 | import com.google.common.collect.ImmutableList; 5 | import com.google.common.collect.Iterables; 6 | import com.google.common.collect.Iterators; 7 | import com.google.gson.Gson; 8 | import com.google.gson.reflect.TypeToken; 9 | import com.nothome.delta.GDiffPatcher; 10 | import com.nothome.delta.RandomAccessFileSeekableSource; 11 | import org.apache.commons.io.IOUtils; 12 | import org.apache.commons.io.LineIterator; 13 | import org.apache.commons.lang.UnhandledException; 14 | 15 | import java.io.*; 16 | import java.lang.reflect.Type; 17 | import java.security.DigestOutputStream; 18 | import java.security.MessageDigest; 19 | import java.security.NoSuchAlgorithmException; 20 | import java.util.*; 21 | import java.util.zip.ZipEntry; 22 | import java.util.zip.ZipInputStream; 23 | 24 | import static com.alexkasko.delta.HashUtils.hex; 25 | import static com.google.common.base.Preconditions.checkArgument; 26 | import static com.google.common.base.Preconditions.checkState; 27 | import static org.apache.commons.io.FileUtils.openInputStream; 28 | import static org.apache.commons.io.FileUtils.openOutputStream; 29 | import static com.alexkasko.delta.IndexEntry.State.*; 30 | import static com.alexkasko.delta.HashUtils.computeSha1; 31 | 32 | /** 33 | * Patches directory with provided ZIP file or stream. 34 | * Patches are applied in fail-fast mode, application will be aborted on first wrong hash-sum or IO error. 35 | *
    36 | *
  • takes directory to patch and patch file (or stream)
  • 37 | *
  • reads '.index' file and using it for futher steps:
  • 38 | *
  • checks hash sums for 'unchanged' files
  • 39 | *
  • reads from stream 'added' files, puts them into directory checking hash sums
  • 40 | *
  • check hash sums for 'updated' files
  • 41 | *
  • reads '.gdiff' patches from stream, applies them, checks hash sums for applied files
  • 42 | *
  • checks hash sums for 'deleted' files
  • 43 | *
  • deletes 'deleted' files
  • 44 |
45 | * 46 | * @author alexkasko 47 | * Date: 11/19/11 48 | */ 49 | public class DirDeltaPatcher { 50 | 51 | /** 52 | * Applies patch file to directory 53 | * 54 | * @param dir target directory 55 | * @param patch ZIP patch file 56 | * @throws IOException on any io or consistency problem 57 | */ 58 | public void patch(File dir, File patch) throws IOException { 59 | ZipInputStream zis = null; 60 | try { 61 | zis = new ZipInputStream(openInputStream(patch)); 62 | patch(dir, zis); 63 | } finally { 64 | IOUtils.closeQuietly(zis); 65 | } 66 | } 67 | 68 | /** 69 | * Applies patch stream to directory 70 | * 71 | * @param dir target directory 72 | * @param patch ZIP patch stream 73 | * @throws IOException on any io or consistency problem 74 | */ 75 | public void patch(File dir, ZipInputStream patch) throws IOException { 76 | DeltaIndex index = readIndex(patch); 77 | check(index.unchanged, dir); 78 | create(index.created, dir, patch); 79 | update(index.updated, dir, patch); 80 | delete(index.deleted, dir); 81 | } 82 | 83 | private DeltaIndex readIndex(ZipInputStream patch) throws IOException { 84 | ZipEntry indexEntry = patch.getNextEntry(); 85 | checkArgument(indexEntry.getName().startsWith(".index"), "Unexpected index file name: '{}', must start with '.index'"); 86 | LineIterator iter = IOUtils.lineIterator(patch, "UTF-8"); 87 | List entries = ImmutableList.copyOf(Iterators.transform(iter, new IndexEntryMapper())); 88 | patch.closeEntry(); 89 | ImmutableList unchanged = ImmutableList.copyOf(Iterables.filter(entries, IndexEntry.Unchanged.class)); 90 | ImmutableList created = ImmutableList.copyOf(Iterables.filter(entries, IndexEntry.Created.class)); 91 | ImmutableList updated = ImmutableList.copyOf(Iterables.filter(entries, IndexEntry.Updated.class)); 92 | ImmutableList deleted = ImmutableList.copyOf(Iterables.filter(entries, IndexEntry.Deleted.class)); 93 | return new DeltaIndex(created, deleted, updated, unchanged); 94 | } 95 | 96 | private void check(List index, File dir) throws IOException { 97 | for (IndexEntry.Unchanged en : index) { 98 | File file = new File(dir, en.path); 99 | if(!(file.exists() && file.isFile())) throw new FileNotFoundException(file.toString()); 100 | String sha1 = computeSha1(file); 101 | if(!sha1.equals(en.oldSha1)) throw new IOException("UNCHANGED file check failed for file: " + file); 102 | } 103 | } 104 | 105 | private void create(List index, File dir, ZipInputStream patch) throws IOException { 106 | for (IndexEntry.Created en : index) { 107 | ZipEntry entry = patch.getNextEntry(); 108 | checkState(en.path.equals(entry.getName()), "Index and zipstream unsynchronized, index: " + en.path + ", zipstream: " + entry.getName()); 109 | File file = new File(dir, en.path); 110 | if (file.exists()) throw new IOException("CREATED file already exists: " + file); 111 | String sha1 = copyStreamToFileWithDigest(patch, file); 112 | if (!sha1.equals(en.newSha1)) throw new IOException("CREATED file check failed for file: " + file); 113 | patch.closeEntry(); 114 | } 115 | } 116 | 117 | private void update(List index, File dir, ZipInputStream patch) throws IOException { 118 | for (IndexEntry.Updated en : index) { 119 | ZipEntry entry = patch.getNextEntry(); 120 | checkState((en.path + ".gdiff").equals(entry.getName()), "Index and zipstream unsynchronized, index: " + en.path + ", zipstream: " + entry.getName()); 121 | File file = new File(dir, en.path); 122 | if (!file.exists()) throw new IOException("UPDATED file doesn't exist: " + file); 123 | String sha1old = computeSha1(file); 124 | if(!sha1old.equals(en.oldSha1)) throw new IOException("UPDATED file check failed for old file: " + file); 125 | File patched = new File(dir, en.path + UUID.randomUUID().toString()); 126 | patch(file, patch, patched); 127 | String sha1new = computeSha1(patched); 128 | if(!sha1new.equals(en.newSha1)) throw new IOException("UPDATED file check failed for new file: " + file); 129 | boolean deleted = file.delete(); 130 | checkState(deleted, "Delete file unsuccessful: " + file); 131 | boolean renamed = patched.renameTo(file); 132 | checkState(renamed, "Rename to file unsuccessful: " + file); 133 | patch.closeEntry(); 134 | } 135 | } 136 | 137 | private void delete(List index, File dir) throws IOException { 138 | for (IndexEntry.Deleted en : index) { 139 | File file = new File(dir, en.path); 140 | if (!file.exists()) throw new IOException("DELETED file doesn't exist: " + file); 141 | String sha1old = computeSha1(file); 142 | if(!sha1old.equals(en.oldSha1)) throw new IOException("DELETED file check failed old file: " + file); 143 | boolean deleted = file.delete(); 144 | checkState(deleted, "Cannot delete file: " + file); 145 | } 146 | } 147 | 148 | private String copyStreamToFileWithDigest(InputStream stream, File file) throws IOException { 149 | DigestOutputStream output = null; 150 | try { 151 | MessageDigest md = MessageDigest.getInstance("SHA-1"); 152 | OutputStream outputStream = new BufferedOutputStream(openOutputStream(file)); 153 | output = new DigestOutputStream(outputStream, md); 154 | IOUtils.copyLarge(stream, output); 155 | output.flush(); 156 | byte[] bytes = output.getMessageDigest().digest(); 157 | return hex(bytes); 158 | } catch (NoSuchAlgorithmException e) { 159 | throw new UnhandledException(e); 160 | } finally { 161 | IOUtils.closeQuietly(output); 162 | } 163 | } 164 | 165 | private void patch(File file, InputStream patch, File patched) throws IOException { 166 | OutputStream out = null; 167 | RandomAccessFile raf = null; 168 | try { 169 | raf = new RandomAccessFile(file, "r"); 170 | RandomAccessFileSeekableSource source = new RandomAccessFileSeekableSource(raf); 171 | out = new BufferedOutputStream(new FileOutputStream(patched)); 172 | new GDiffPatcher().patch(source, patch, out); 173 | } finally { 174 | IOUtils.closeQuietly(out); 175 | IOUtils.closeQuietly(raf); 176 | } 177 | } 178 | 179 | private class IndexEntryMapper implements Function { 180 | private final Gson gson = new Gson(); 181 | 182 | @Override 183 | public IndexEntry apply(String input) { 184 | Type mapType = new TypeToken>() {}.getType(); 185 | Map map = gson.fromJson(input, mapType); 186 | String state = map.get("state"); 187 | String path = map.get("path"); 188 | String oldSha1 = map.get("oldSha1"); 189 | String newSha1 = map.get("newSha1"); 190 | if(UNCHANGED.name().equals(state)) return new IndexEntry.Unchanged(path, oldSha1, newSha1); 191 | if(CREATED.name().equals(state)) return new IndexEntry.Created(path, oldSha1, newSha1); 192 | if(UPDATED.name().equals(state)) return new IndexEntry.Updated(path, oldSha1, newSha1); 193 | if(DELETED.name().equals(state)) return new IndexEntry.Deleted(path, oldSha1, newSha1); 194 | throw new IllegalStateException("Cannot parse index entry from line: " + input); 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /delta-diff/src/main/java/com/alexkasko/delta/DirDeltaCreator.java: -------------------------------------------------------------------------------- 1 | package com.alexkasko.delta; 2 | 3 | import com.google.common.base.Function; 4 | import com.google.common.collect.*; 5 | import com.google.gson.Gson; 6 | import com.google.gson.GsonBuilder; 7 | import com.nothome.delta.Delta; 8 | import com.nothome.delta.GDiffWriter; 9 | import org.apache.commons.io.FileUtils; 10 | import org.apache.commons.io.IOCase; 11 | import org.apache.commons.io.IOUtils; 12 | import org.apache.commons.io.filefilter.IOFileFilter; 13 | import org.apache.commons.io.filefilter.TrueFileFilter; 14 | 15 | import java.io.*; 16 | import java.nio.charset.Charset; 17 | import java.util.Collection; 18 | import java.util.List; 19 | import java.util.Set; 20 | import java.util.UUID; 21 | import java.util.zip.ZipEntry; 22 | import java.util.zip.ZipOutputStream; 23 | 24 | import static com.google.common.base.Preconditions.checkArgument; 25 | import static org.apache.commons.io.FileUtils.listFiles; 26 | import static org.apache.commons.io.FilenameUtils.separatorsToUnix; 27 | import static com.alexkasko.delta.HashUtils.computeSha1; 28 | 29 | /** 30 | * Creates ZIP file (or stream) with GDIFF deltas for all changed files and '.index' text file 31 | * (with '.index_' prefix) with list of unchanged, added, updated and deleted files with SHA1 hash sums 32 | * 33 | * @author alexkasko 34 | * Date: 11/18/11 35 | */ 36 | public class DirDeltaCreator { 37 | private static final String EMPTY_STRING = ""; 38 | 39 | /** 40 | * Creates patch ZIP file 41 | * 42 | * @param oldDir old version of directory 43 | * @param newDir new version of directory 44 | * @param outFile file to write patch into 45 | * @throws IOException on any io or consistency problem 46 | */ 47 | public void create(File oldDir, File newDir, File outFile) throws IOException { 48 | create(oldDir, newDir, outFile, IOCase.SYSTEM); 49 | } 50 | 51 | /** 52 | * Creates patch ZIP file 53 | * 54 | * @param oldDir old version of directory 55 | * @param newDir new version of directory 56 | * @param outFile file to write patch into 57 | * @param caseSensitive case sensivity flag 58 | * @throws IOException on any io or consistency problem 59 | */ 60 | public void create(File oldDir, File newDir, File outFile, IOCase caseSensitive) throws IOException { 61 | OutputStream out = null; 62 | try { 63 | out = FileUtils.openOutputStream(outFile); 64 | IOFileFilter filter = TrueFileFilter.TRUE; 65 | create(oldDir, newDir, filter, out, caseSensitive); 66 | } finally { 67 | IOUtils.closeQuietly(out); 68 | } 69 | } 70 | 71 | /** 72 | * Writes zipped patch into provided output stream 73 | * 74 | * @param oldDir old version of directory 75 | * @param newDir new version of directory 76 | * @param filter IO filter to select files 77 | * @param patch output stream to write patch into 78 | * @throws IOException on any io or consistency problem 79 | */ 80 | public void create(File oldDir, File newDir, IOFileFilter filter, OutputStream patch) throws IOException { 81 | create(oldDir, newDir, filter, patch, IOCase.SYSTEM); 82 | } 83 | 84 | /** 85 | * Writes zipped patch into provided output stream 86 | * 87 | * @param oldDir old version of directory 88 | * @param newDir new version of directory 89 | * @param filter IO filter to select files 90 | * @param patch output stream to write patch into 91 | * @param caseSensitive case sensivity flag 92 | * @throws IOException on any io or consistency problem 93 | */ 94 | public void create(File oldDir, File newDir, IOFileFilter filter, OutputStream patch, IOCase caseSensitive) throws IOException { 95 | DeltaIndex paths = readDeltaPaths(oldDir, newDir, filter, caseSensitive); 96 | ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(patch)); 97 | writeIndex(paths, out); 98 | writeCreated(paths.created, newDir, out); 99 | writeUpdated(paths.updated, oldDir, newDir, out); 100 | out.close(); 101 | } 102 | 103 | private DeltaIndex readDeltaPaths(File oldDir, File newDir, IOFileFilter filter, IOCase caseSensitive) throws IOException { 104 | if(!(null != oldDir && oldDir.exists() && oldDir.isDirectory())) throw new IOException("Bad oldDir argument"); 105 | if(!(null != newDir && newDir.exists() && newDir.isDirectory())) throw new IOException("Bad newDir argument"); 106 | // read files 107 | Collection oldFiles = listFiles(oldDir, filter, filter); 108 | Collection newFiles = listFiles(newDir, filter, filter); 109 | // want to do comparing on strings, without touching FS 110 | Set oldSet = ImmutableSet.copyOf(Collections2.transform(oldFiles, new Relativiser(oldDir, caseSensitive))); 111 | Set newSet = ImmutableSet.copyOf(Collections2.transform(newFiles, new Relativiser(newDir, caseSensitive))); 112 | // partitioning 113 | List createdPaths = Ordering.natural().immutableSortedCopy(Sets.difference(newSet, oldSet)); 114 | List existedPaths = Ordering.natural().immutableSortedCopy(Sets.intersection(oldSet, newSet)); 115 | List deletedPaths = Ordering.natural().immutableSortedCopy(Sets.difference(oldSet, newSet)); 116 | // converting 117 | ImmutableList created = ImmutableList.copyOf(Lists.transform(createdPaths, new CreatedIndexer(newDir))); 118 | ImmutableList deleted = ImmutableList.copyOf(Lists.transform(deletedPaths, new DeletedIndexer(oldDir))); 119 | List existed = Lists.transform(existedPaths, new ExistedIndexer(oldDir, newDir)); 120 | // partitioning 121 | ImmutableList updated = ImmutableList.copyOf(Iterables.filter(existed, IndexEntry.Updated.class)); 122 | ImmutableList unchanged = ImmutableList.copyOf(Iterables.filter(existed, IndexEntry.Unchanged.class)); 123 | return new DeltaIndex(created, deleted, updated, unchanged); 124 | } 125 | 126 | private void writeIndex(DeltaIndex paths, ZipOutputStream out) throws IOException { 127 | // lazy transforms 128 | out.putNextEntry(new ZipEntry(".index_" + UUID.randomUUID().toString())); 129 | Gson gson = new GsonBuilder().create(); 130 | Writer writer = new OutputStreamWriter(out, Charset.forName("UTF-8")); 131 | for (IndexEntry ie : paths.getAll()) { 132 | gson.toJson(ie, IndexEntry.class, writer); 133 | writer.write("\n"); 134 | } 135 | writer.flush(); 136 | out.closeEntry(); 137 | } 138 | 139 | private void writeCreated(List paths, File newDir, ZipOutputStream out) throws IOException { 140 | for(IndexEntry.Created en : paths) { 141 | out.putNextEntry(new ZipEntry(en.path)); 142 | File file = new File(newDir, en.path); 143 | FileUtils.copyFile(file, out); 144 | out.closeEntry(); 145 | } 146 | } 147 | 148 | private void writeUpdated(List paths, File oldDir, File newDir, ZipOutputStream out) throws IOException { 149 | for(IndexEntry.Updated en : paths) { 150 | out.putNextEntry(new ZipEntry(en.path + ".gdiff")); 151 | File source = new File(oldDir, en.path); 152 | File target = new File(newDir, en.path); 153 | computeDelta(source, target, out); 154 | out.closeEntry(); 155 | } 156 | } 157 | 158 | private void computeDelta(File source, File target, OutputStream out) throws IOException { 159 | OutputStream guarded = new NoCloseOutputStream(out); 160 | GDiffWriter writer = new GDiffWriter(guarded); 161 | new Delta().compute(source, target, writer); 162 | } 163 | 164 | private static class Relativiser implements Function { 165 | private final String parent; 166 | private final IOCase caseSensitive; 167 | 168 | private Relativiser(File parent, IOCase caseSensitive) { 169 | this.parent = separatorsToUnix(parent.getPath()); 170 | this.caseSensitive = caseSensitive; 171 | } 172 | 173 | @Override 174 | public String apply(File input) { 175 | String path = separatorsToUnix(input.getPath()); 176 | // check whether actual child 177 | checkArgument(parent.equals(path.substring(0, parent.length()))); 178 | String relative = path.substring(parent.length() + 1); 179 | return caseSensitive.isCaseSensitive() ? relative : relative.toLowerCase(); 180 | } 181 | } 182 | 183 | private static class CreatedIndexer implements Function { 184 | private final File parent; 185 | 186 | private CreatedIndexer(File parent) { 187 | this.parent = parent; 188 | } 189 | 190 | @Override 191 | public IndexEntry.Created apply(String path) { 192 | String sha1 = computeSha1(new File(parent, path)); 193 | return new IndexEntry.Created(path, EMPTY_STRING, sha1); 194 | } 195 | } 196 | 197 | private static class DeletedIndexer implements Function { 198 | private final File parent; 199 | 200 | private DeletedIndexer(File parent) { 201 | this.parent = parent; 202 | } 203 | 204 | @Override 205 | public IndexEntry.Deleted apply(String path) { 206 | String sha1 = computeSha1(new File(parent, path)); 207 | return new IndexEntry.Deleted(path, sha1, EMPTY_STRING); 208 | } 209 | } 210 | 211 | private static class ExistedIndexer implements Function { 212 | private final File oldParent; 213 | private final File newParent; 214 | 215 | private ExistedIndexer(File oldParent, File newParent) { 216 | this.oldParent = oldParent; 217 | this.newParent = newParent; 218 | } 219 | 220 | @Override 221 | public IndexEntry apply(String path) { 222 | String oldSha1 = computeSha1(new File(oldParent, path)); 223 | String newSha1 = computeSha1(new File(newParent, path)); 224 | if(oldSha1.equals(newSha1)) { 225 | return new IndexEntry.Unchanged(path, oldSha1, newSha1); 226 | } else { 227 | return new IndexEntry.Updated(path, oldSha1, newSha1); 228 | } 229 | } 230 | } 231 | } 232 | --------------------------------------------------------------------------------