1 // 2 // Getdown - application installer, patcher and launcher 3 // Copyright (C) 2004-2018 Getdown authors 4 // https://github.com/threerings/getdown/blob/master/LICENSE 5 6 package com.threerings.getdown.tools; 7 8 import java.io.File; 9 import java.io.FileOutputStream; 10 import java.io.IOException; 11 import java.io.InputStream; 12 13 import java.util.Enumeration; 14 import java.util.jar.JarEntry; 15 import java.util.jar.JarFile; 16 import java.util.zip.ZipEntry; 17 18 import com.threerings.getdown.util.FileUtil; 19 import com.threerings.getdown.util.ProgressObserver; 20 import com.threerings.getdown.util.StreamUtil; 21 22 import static com.threerings.getdown.Log.log; 23 24 /** 25 * Applies a unified patch file to an application directory, providing 26 * percentage completion feedback along the way. <em>Note:</em> the 27 * patcher is not thread safe. Create a separate patcher instance for each 28 * patching action that is desired. 29 */ 30 public class Patcher 31 { 32 /** A suffix appended to file names to indicate that a file should be newly created. */ 33 public static final String CREATE = ".create"; 34 35 /** A suffix appended to file names to indicate that a file should be patched. */ 36 public static final String PATCH = ".patch"; 37 38 /** A suffix appended to file names to indicate that a file should be deleted. */ 39 public static final String DELETE = ".delete"; 40 41 /** 42 * Applies the specified patch file to the application living in the 43 * specified application directory. The supplied observer, if 44 * non-null, will be notified of progress along the way. 45 * 46 * <p><em>Note:</em> this method runs on the calling thread, thus the 47 * caller may want to make use of a separate thread in conjunction 48 * with the patcher so that the user interface is not blocked for the 49 * duration of the patch. 50 */ patch(File appdir, File patch, ProgressObserver obs)51 public void patch (File appdir, File patch, ProgressObserver obs) 52 throws IOException 53 { 54 // save this information for later 55 _obs = obs; 56 _plength = patch.length(); 57 58 try (JarFile file = new JarFile(patch)) { 59 Enumeration<JarEntry> entries = file.entries(); // old skool! 60 while (entries.hasMoreElements()) { 61 JarEntry entry = entries.nextElement(); 62 String path = entry.getName(); 63 long elength = entry.getCompressedSize(); 64 65 // depending on the suffix, we do The Right Thing (tm) 66 if (path.endsWith(CREATE)) { 67 path = strip(path, CREATE); 68 log.info("Creating " + path + "..."); 69 createFile(file, entry, new File(appdir, path)); 70 71 } else if (path.endsWith(PATCH)) { 72 path = strip(path, PATCH); 73 log.info("Patching " + path + "..."); 74 patchFile(file, entry, appdir, path); 75 76 } else if (path.endsWith(DELETE)) { 77 path = strip(path, DELETE); 78 log.info("Removing " + path + "..."); 79 File target = new File(appdir, path); 80 if (!FileUtil.deleteHarder(target)) { 81 log.warning("Failure deleting '" + target + "'."); 82 } 83 84 } else { 85 log.warning("Skipping bogus patch file entry: " + path); 86 } 87 88 // note that we've completed this entry 89 _complete += elength; 90 } 91 } 92 } 93 strip(String path, String suffix)94 protected String strip (String path, String suffix) 95 { 96 return path.substring(0, path.length() - suffix.length()); 97 } 98 createFile(JarFile file, ZipEntry entry, File target)99 protected void createFile (JarFile file, ZipEntry entry, File target) 100 { 101 // create our copy buffer if necessary 102 if (_buffer == null) { 103 _buffer = new byte[COPY_BUFFER_SIZE]; 104 } 105 106 // make sure the file's parent directory exists 107 File pdir = target.getParentFile(); 108 if (!pdir.exists() && !pdir.mkdirs()) { 109 log.warning("Failed to create parent for '" + target + "'."); 110 } 111 112 try (InputStream in = file.getInputStream(entry); 113 FileOutputStream fout = new FileOutputStream(target)) { 114 115 int total = 0, read; 116 while ((read = in.read(_buffer)) != -1) { 117 total += read; 118 fout.write(_buffer, 0, read); 119 updateProgress(total); 120 } 121 122 } catch (IOException ioe) { 123 log.warning("Error creating '" + target + "': " + ioe); 124 } 125 } 126 patchFile(JarFile file, ZipEntry entry, File appdir, String path)127 protected void patchFile (JarFile file, ZipEntry entry, 128 File appdir, String path) 129 { 130 File target = new File(appdir, path); 131 File patch = new File(appdir, entry.getName()); 132 File otarget = new File(appdir, path + ".old"); 133 JarDiffPatcher patcher = null; 134 135 // make sure no stale old target is lying around to mess us up 136 FileUtil.deleteHarder(otarget); 137 138 // pipe the contents of the patch into a file 139 try (InputStream in = file.getInputStream(entry); 140 FileOutputStream fout = new FileOutputStream(patch)) { 141 142 StreamUtil.copy(in, fout); 143 StreamUtil.close(fout); 144 145 // move the current version of the jar to .old 146 if (!FileUtil.renameTo(target, otarget)) { 147 log.warning("Failed to .oldify '" + target + "'."); 148 return; 149 } 150 151 // we'll need this to pass progress along to our observer 152 final long elength = entry.getCompressedSize(); 153 ProgressObserver obs = new ProgressObserver() { 154 public void progress (int percent) { 155 updateProgress((int)(percent * elength / 100)); 156 } 157 }; 158 159 // now apply the patch to create the new target file 160 patcher = new JarDiffPatcher(); 161 patcher.patchJar(otarget.getPath(), patch.getPath(), target, obs); 162 163 } catch (IOException ioe) { 164 if (patcher == null) { 165 log.warning("Failed to write patch file '" + patch + "': " + ioe); 166 } else { 167 log.warning("Error patching '" + target + "': " + ioe); 168 } 169 170 } finally { 171 // clean up our temporary files 172 FileUtil.deleteHarder(patch); 173 FileUtil.deleteHarder(otarget); 174 } 175 } 176 updateProgress(int progress)177 protected void updateProgress (int progress) 178 { 179 if (_obs != null) { 180 _obs.progress((int)(100 * (_complete + progress) / _plength)); 181 } 182 } 183 main(String[] args)184 public static void main (String[] args) 185 { 186 if (args.length != 2) { 187 System.err.println("Usage: Patcher appdir patch_file"); 188 System.exit(-1); 189 } 190 191 Patcher patcher = new Patcher(); 192 try { 193 patcher.patch(new File(args[0]), new File(args[1]), null); 194 } catch (IOException ioe) { 195 System.err.println("Error: " + ioe.getMessage()); 196 System.exit(-1); 197 } 198 } 199 200 protected ProgressObserver _obs; 201 protected long _complete, _plength; 202 protected byte[] _buffer; 203 204 protected static final int COPY_BUFFER_SIZE = 4096; 205 } 206