JAL-3130 adapted getdown src. attempt 2. first attempt failed due to cp'ed .git files
[jalview.git] / getdown / src / getdown / core / src / main / java / com / threerings / getdown / tools / Patcher.java
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      */
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
94     protected String strip (String path, String suffix)
95     {
96         return path.substring(0, path.length() - suffix.length());
97     }
98
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
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
177     protected void updateProgress (int progress)
178     {
179         if (_obs != null) {
180             _obs.progress((int)(100 * (_complete + progress) / _plength));
181         }
182     }
183
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 }