2 // Getdown - application installer, patcher and launcher
3 // Copyright (C) 2004-2018 Getdown authors
4 // https://github.com/threerings/getdown/blob/master/LICENSE
6 package com.threerings.getdown.tools;
9 import java.io.FileOutputStream;
10 import java.io.IOException;
11 import java.io.InputStream;
13 import java.util.Enumeration;
14 import java.util.jar.JarEntry;
15 import java.util.jar.JarFile;
16 import java.util.zip.ZipEntry;
18 import com.threerings.getdown.util.FileUtil;
19 import com.threerings.getdown.util.ProgressObserver;
20 import com.threerings.getdown.util.StreamUtil;
22 import static com.threerings.getdown.Log.log;
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.
32 /** A suffix appended to file names to indicate that a file should be newly created. */
33 public static final String CREATE = ".create";
35 /** A suffix appended to file names to indicate that a file should be patched. */
36 public static final String PATCH = ".patch";
38 /** A suffix appended to file names to indicate that a file should be deleted. */
39 public static final String DELETE = ".delete";
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.
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.
51 public void patch (File appdir, File patch, ProgressObserver obs)
54 // save this information for later
56 _plength = patch.length();
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();
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));
71 } else if (path.endsWith(PATCH)) {
72 path = strip(path, PATCH);
73 log.info("Patching " + path + "...");
74 patchFile(file, entry, appdir, path);
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 + "'.");
85 log.warning("Skipping bogus patch file entry: " + path);
88 // note that we've completed this entry
94 protected String strip (String path, String suffix)
96 return path.substring(0, path.length() - suffix.length());
99 protected void createFile (JarFile file, ZipEntry entry, File target)
101 // create our copy buffer if necessary
102 if (_buffer == null) {
103 _buffer = new byte[COPY_BUFFER_SIZE];
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 + "'.");
112 try (InputStream in = file.getInputStream(entry);
113 FileOutputStream fout = new FileOutputStream(target)) {
116 while ((read = in.read(_buffer)) != -1) {
118 fout.write(_buffer, 0, read);
119 updateProgress(total);
122 } catch (IOException ioe) {
123 log.warning("Error creating '" + target + "': " + ioe);
127 protected void patchFile (JarFile file, ZipEntry entry,
128 File appdir, String path)
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;
135 // make sure no stale old target is lying around to mess us up
136 FileUtil.deleteHarder(otarget);
138 // pipe the contents of the patch into a file
139 try (InputStream in = file.getInputStream(entry);
140 FileOutputStream fout = new FileOutputStream(patch)) {
142 StreamUtil.copy(in, fout);
143 StreamUtil.close(fout);
145 // move the current version of the jar to .old
146 if (!FileUtil.renameTo(target, otarget)) {
147 log.warning("Failed to .oldify '" + target + "'.");
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));
159 // now apply the patch to create the new target file
160 patcher = new JarDiffPatcher();
161 patcher.patchJar(otarget.getPath(), patch.getPath(), target, obs);
163 } catch (IOException ioe) {
164 if (patcher == null) {
165 log.warning("Failed to write patch file '" + patch + "': " + ioe);
167 log.warning("Error patching '" + target + "': " + ioe);
171 // clean up our temporary files
172 FileUtil.deleteHarder(patch);
173 FileUtil.deleteHarder(otarget);
177 protected void updateProgress (int progress)
180 _obs.progress((int)(100 * (_complete + progress) / _plength));
184 public static void main (String[] args)
186 if (args.length != 2) {
187 System.err.println("Usage: Patcher appdir patch_file");
191 Patcher patcher = new Patcher();
193 patcher.patch(new File(args[0]), new File(args[1]), null);
194 } catch (IOException ioe) {
195 System.err.println("Error: " + ioe.getMessage());
200 protected ProgressObserver _obs;
201 protected long _complete, _plength;
202 protected byte[] _buffer;
204 protected static final int COPY_BUFFER_SIZE = 4096;