001package co.codewizards.cloudstore.updater;
002
003import static co.codewizards.cloudstore.core.util.Util.*;
004
005import java.io.File;
006import java.io.FileFilter;
007import java.io.FileOutputStream;
008import java.io.IOException;
009import java.io.InputStream;
010import java.net.URL;
011import java.util.Collection;
012import java.util.HashSet;
013import java.util.Properties;
014import java.util.Set;
015
016import org.kohsuke.args4j.CmdLineException;
017import org.kohsuke.args4j.CmdLineParser;
018import org.kohsuke.args4j.Option;
019import org.slf4j.Logger;
020import org.slf4j.LoggerFactory;
021
022import ch.qos.logback.classic.LoggerContext;
023import ch.qos.logback.classic.joran.JoranConfigurator;
024import ch.qos.logback.core.joran.spi.JoranException;
025import ch.qos.logback.core.util.StatusPrinter;
026import co.codewizards.cloudstore.core.config.ConfigDir;
027import co.codewizards.cloudstore.core.updater.CloudStoreUpdaterCore;
028import co.codewizards.cloudstore.core.util.IOUtil;
029
030public class CloudStoreUpdater extends CloudStoreUpdaterCore {
031        private static final Logger logger = LoggerFactory.getLogger(CloudStoreUpdater.class);
032
033        private final String[] args;
034        private boolean throwException = true;
035
036        @Option(name="-installationDir", required=true, usage="Base-directory of the installation containing the 'bin' directory as well as the 'installation.properties' file - e.g. '/opt/cloudstore'. The installation in this directory will be updated.")
037        private String installationDir;
038        private File installationDirFile;
039
040        private Properties remoteUpdateProperties;
041        private File tempDownloadDir;
042
043        public static void main(String[] args) throws Exception {
044                initLogging();
045                try {
046                        int programExitStatus = new CloudStoreUpdater(args).throwException(false).execute();
047                        System.exit(programExitStatus);
048                } catch (Throwable x) {
049                        logger.error(x.toString(), x);
050                        System.exit(999);
051                }
052        }
053
054        public CloudStoreUpdater(String[] args) {
055                this.args = args;
056        }
057
058        public boolean isThrowException() {
059                return throwException;
060        }
061        public void setThrowException(boolean throwException) {
062                this.throwException = throwException;
063        }
064        public CloudStoreUpdater throwException(boolean throwException) {
065                setThrowException(throwException);
066                return this;
067        }
068
069        public int execute() throws Exception {
070                int programExitStatus = 1;
071                CmdLineParser parser = new CmdLineParser(this);
072                try {
073                        parser.parseArgument(args);
074                        this.run();
075                        programExitStatus = 0;
076                } catch (CmdLineException e) {
077                        // handling of wrong arguments
078                        programExitStatus = 2;
079                        System.err.println("Error: " + e.getMessage());
080                        System.err.println();
081                        if (throwException)
082                                throw e;
083                } catch (Exception x) {
084                        programExitStatus = 3;
085                        logger.error(x.toString(), x);
086                        if (throwException)
087                                throw x;
088                }
089                return programExitStatus;
090        }
091
092        private static void initLogging() throws IOException, JoranException {
093                ConfigDir.getInstance().getLogDir();
094
095                final String logbackXmlName = "logback.updater.xml";
096                File logbackXmlFile = new File(ConfigDir.getInstance().getFile(), logbackXmlName);
097                if (!logbackXmlFile.exists())
098                        IOUtil.copyResource(CloudStoreUpdater.class, logbackXmlName, logbackXmlFile);
099
100                LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
101            try {
102              JoranConfigurator configurator = new JoranConfigurator();
103              configurator.setContext(context);
104              // Call context.reset() to clear any previous configuration, e.g. default
105              // configuration. For multi-step configuration, omit calling context.reset().
106              context.reset();
107              configurator.doConfigure(logbackXmlFile);
108            } catch (JoranException je) {
109                // StatusPrinter will handle this
110                doNothing();
111            }
112            StatusPrinter.printInCaseOfErrorsOrWarnings(context);
113        }
114
115        private void run() throws Exception {
116                System.out.println("CloudStore updater started. Downloading meta-data.");
117
118                boolean restoreRenamedFiles = false;
119                try {
120                        final File downloadFile = downloadURLViaRemoteUpdateProperties("artifact[co.codewizards.cloudstore.aggregator].downloadURL");
121                        final File signatureFile = downloadURLViaRemoteUpdateProperties("artifact[co.codewizards.cloudstore.aggregator].signatureURL");
122
123                        System.out.println("Verifying PGP signature.");
124                        new PGPVerifier().verify(downloadFile, signatureFile);
125
126                        checkAvailableDiskSpace(getInstallationDir(), downloadFile.length() * 5);
127
128                        final File backupDir = getBackupDir();
129                        backupDir.mkdirs();
130                        final File backupTarGzFile = new File(backupDir, resolve(String.format("co.codewizards.cloudstore.aggregator-${localVersion}.backup-%s.tar.gz", Long.toString(System.currentTimeMillis(), 36))));
131                        System.out.println("Creating backup: " + backupTarGzFile);
132
133                        new TarGzFile(backupTarGzFile)
134                        .fileFilter(fileFilterIgnoringBackupAndUpdaterDir)
135                        .compress(getInstallationDir());
136
137                        // Because of f***ing Windows and its insane file-locking, we first try to move all
138                        // files out of the way by renaming them. If this fails, we restore the previous
139                        // state. This way, we increase the probability that we leave a consistent state.
140                        // If a file is locked, this should fail already now, rather than later after we extracted
141                        // half of the tarball.
142                        System.out.println("Renaming files in installation directory: " + getInstallationDir());
143                        restoreRenamedFiles = true;
144                        renameFiles(getInstallationDir(), fileFilterIgnoringBackupAndUpdaterDir);
145
146                        System.out.println("Overwriting installation directory: " + getInstallationDir());
147                        final Set<File> keepFiles = new HashSet<>();
148                        keepFiles.add(getInstallationDir());
149                        populateFilesRecursively(getBackupDir(), keepFiles);
150                        populateFilesRecursively(getUpdaterDir(), keepFiles);
151
152                        new TarGzFile(downloadFile)
153                        .tarGzEntryNameConverter(new ExtractTarGzEntryNameConverter())
154                        .fileFilter(new FileFilterTrackingExtractedFiles(keepFiles))
155                        .extract(getInstallationDir());
156
157                        restoreRenamedFiles = false;
158
159                        System.out.println("Deleting old files from installation directory: " + getInstallationDir());
160                        deleteAllExcept(getInstallationDir(), keepFiles);
161                } finally {
162                        if (restoreRenamedFiles)
163                                restoreRenamedFiles(getInstallationDir());
164
165                        if (tempDownloadDir != null) {
166                                System.out.println("Deleting temporary download-directory.");
167                                IOUtil.deleteDirectoryRecursively(tempDownloadDir);
168                        }
169                }
170                System.out.println("Update successfully done. Exiting.");
171        }
172
173        private void checkAvailableDiskSpace(final File dir, final long expectedRequiredSpace) throws IOException {
174                final long usableSpace = dir.getUsableSpace();
175                logger.debug("checkAvailableDiskSpace: dir='{}' dir.usableSpace='{} MiB' expectedRequiredSpace='{} MiB'",
176                                dir, usableSpace / 1024 / 1024, expectedRequiredSpace / 1024 / 1024);
177
178                if (usableSpace < expectedRequiredSpace) {
179                        final String msg = String.format("Insufficient disk space! The file system of the directory '%s' has %s MiB (%s B) available, but %s MiB (%s B) are required!",
180                                        dir, usableSpace / 1024 / 1024, usableSpace, expectedRequiredSpace / 1024 / 1024, expectedRequiredSpace);
181                        logger.error("checkAvailableDiskSpace: " + msg);
182                        throw new IOException(msg);
183                }
184        }
185
186        private static final String RENAMED_FILE_SUFFIX = ".csupdbak";
187
188        private void renameFiles(final File dir, final FileFilter fileFilter) throws IOException {
189                final File[] children = dir.listFiles(fileFilter);
190                if (children != null) {
191                        for (final File child : children) {
192                                if (child.isDirectory())
193                                        renameFiles(child, fileFilter);
194                                else {
195                                        final File newChild = new File(dir, child.getName() + RENAMED_FILE_SUFFIX);
196                                        logger.debug("renameFiles: file='{}', newName='{}'", child, newChild.getName());
197                                        if (!child.renameTo(newChild)) {
198                                                final String msg = String.format("Failed to rename the file '%s' to '%s' (in the same directory)!", child, newChild.getName());
199                                                logger.error("renameFiles: {}", msg);
200                                                throw new IOException(msg);
201                                        }
202                                }
203                        }
204                }
205        }
206
207        private void restoreRenamedFiles(File dir) {
208                final File[] children = dir.listFiles();
209                if (children != null) {
210                        for (final File child : children) {
211                                if (child.isDirectory())
212                                        restoreRenamedFiles(child);
213                                else if (child.getName().endsWith(RENAMED_FILE_SUFFIX)) {
214                                        final File newChild = new File(dir, child.getName().substring(0, child.getName().length() - RENAMED_FILE_SUFFIX.length()));
215                                        logger.debug("restoreRenamedFiles: file='{}', newName='{}'", child, newChild.getName());
216                                        newChild.delete();
217                                        if (!child.renameTo(newChild))
218                                                logger.warn("restoreRenamedFiles: Failed to rename the file '{}' back to its original name '{}' (in the same directory)!", child, newChild.getName());
219                                }
220                        }
221                }
222        }
223
224        private static class FileFilterTrackingExtractedFiles implements FileFilter {
225                private final Collection<File> files;
226
227                public FileFilterTrackingExtractedFiles(Collection<File> files) {
228                        this.files = assertNotNull("files", files);
229                }
230
231                @Override
232                public boolean accept(File file) {
233                        files.add(file);
234                        files.add(file.getParentFile()); // just in case the parent didn't have its own entry and was created implicitly
235                        return true;
236                }
237        }
238
239        private static class ExtractTarGzEntryNameConverter implements TarGzEntryNameConverter {
240                @Override
241                public String getEntryName(final File rootDir, final File file) { throw new UnsupportedOperationException(); }
242
243                @Override
244                public File getFile(final File rootDir, String entryName) {
245                        final String prefix = "cloudstore/";
246                        if (entryName.startsWith(prefix))
247                                entryName = entryName.substring(prefix.length());
248
249                        return entryName.isEmpty() ? rootDir : new File(rootDir, entryName);
250                }
251        }
252
253        private void populateFilesRecursively(final File fileOrDir, final Set<File> files) {
254                assertNotNull("fileOrDir", fileOrDir);
255                assertNotNull("files", files);
256                files.add(fileOrDir);
257                final File[] children = fileOrDir.listFiles();
258                if (children != null) {
259                        for (File child : children)
260                                populateFilesRecursively(child, files);
261                }
262        }
263
264        private void deleteAllExcept(final File fileOrDir, final Set<File> keepFiles) {
265                assertNotNull("fileOrDir", fileOrDir);
266                assertNotNull("keepFiles", keepFiles);
267                if (keepFiles.contains(fileOrDir)) {
268                        logger.debug("deleteAllExcept: Keeping: {}", fileOrDir);
269                        final File[] children = fileOrDir.listFiles();
270                        if (children != null) {
271                                for (File child : children)
272                                        deleteAllExcept(child, keepFiles);
273                        }
274                }
275                else {
276                        logger.debug("deleteAllExcept: Deleting: {}", fileOrDir);
277                        IOUtil.deleteDirectoryRecursively(fileOrDir);
278                }
279        }
280
281        private File downloadURLViaRemoteUpdateProperties(final String remoteUpdatePropertiesKey) {
282                logger.debug("downloadURLViaRemoteUpdateProperties: remoteUpdatePropertiesKey='{}'", remoteUpdatePropertiesKey);
283                final String resolvedKey = resolve(remoteUpdatePropertiesKey);
284                final String urlStr = getRemoteUpdateProperties().getProperty(resolvedKey);
285                if (urlStr == null || urlStr.trim().isEmpty())
286                        throw new IllegalStateException("No value for key in remoteUpdateProperties: " + resolvedKey);
287
288                final String resolvedURLStr = resolve(urlStr);
289                logger.debug("downloadURLViaRemoteUpdateProperties: resolvedURLStr='{}'", resolvedURLStr);
290
291                final File tempDownloadDir = getTempDownloadDir();
292
293                try {
294                        System.out.println("Downloading: " + resolvedURLStr);
295                        final URL url = new URL(resolvedURLStr);
296                        long contentLength = url.openConnection().getContentLengthLong();
297                        if (contentLength < 0)
298                                logger.warn("downloadURLViaRemoteUpdateProperties: contentLength unknown! url='{}'", url);
299                        else {
300                                logger.debug("downloadURLViaRemoteUpdateProperties: contentLength={} url='{}'", contentLength, url);
301                                checkAvailableDiskSpace(tempDownloadDir, Math.max(1024 * 1024, contentLength * 3 / 2));
302                        }
303
304                        final String path = url.getPath();
305                        final int lastSlashIndex = path.lastIndexOf('/');
306                        if (lastSlashIndex < 0)
307                                throw new IllegalStateException("No '/' found in URL?!");
308
309                        final String fileName = path.substring(lastSlashIndex + 1);
310                        final File downloadFile = new File(tempDownloadDir, fileName);
311
312                        boolean successful = false;
313                        final InputStream in = url.openStream();
314                        try {
315                                final FileOutputStream out = new FileOutputStream(downloadFile);
316                                try {
317                                        IOUtil.transferStreamData(in, out);
318                                } finally {
319                                        out.close();
320                                }
321                                successful = true;
322                        } finally {
323                                in.close();
324
325                                if (!successful)
326                                        downloadFile.delete();
327                        }
328
329                        return downloadFile;
330                } catch (IOException e) {
331                        throw new RuntimeException(e);
332                }
333        }
334
335        private File getTempDownloadDir() {
336                if (tempDownloadDir == null) {
337                        try {
338                                tempDownloadDir = IOUtil.createUniqueRandomFolder(IOUtil.getTempDir(), "cloudstore-update-");
339                        } catch (IOException e) {
340                                throw new RuntimeException(e);
341                        }
342                }
343                return tempDownloadDir;
344        }
345
346        /**
347         * Gets the installation directory that was passed as command line parameter.
348         */
349        @Override
350        protected File getInstallationDir() {
351                if (installationDirFile == null) {
352                        final String path = IOUtil.simplifyPath(new File(assertNotNull("installationDir", installationDir)));
353                        final File f = new File(path);
354                        if (!f.exists())
355                                throw new IllegalArgumentException(String.format("installationDir '%s' (specified as '%s') does not exist!", f, installationDir));
356
357                        if (!f.isDirectory())
358                                throw new IllegalArgumentException(String.format("installationDir '%s' (specified as '%s') is not a directory!", f, installationDir));
359
360                        installationDirFile = f;
361                }
362                return installationDirFile;
363        }
364
365        private Properties getRemoteUpdateProperties() {
366                if (remoteUpdateProperties == null) {
367                        final String resolvedRemoteUpdatePropertiesURL = resolve(remoteUpdatePropertiesURL);
368                        final Properties properties = new Properties();
369                        try {
370                                final URL url = new URL(resolvedRemoteUpdatePropertiesURL);
371                                final InputStream in = url.openStream();
372                                try {
373                                        properties.load(in);
374                                } finally {
375                                        in.close();
376                                }
377                        } catch (IOException e) {
378                                throw new RuntimeException(e);
379                        }
380                        remoteUpdateProperties = properties;
381                }
382                return remoteUpdateProperties;
383        }
384
385        private static final void doNothing() { }
386}