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}