001/* 002 * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * bstefanescu 018 * jcarsique 019 * Yannis JULIENNE 020 */ 021package org.nuxeo.connect.update.task.update; 022 023import java.io.File; 024import java.io.FileInputStream; 025import java.io.IOException; 026import java.util.HashMap; 027import java.util.Map; 028 029import org.apache.commons.io.IOUtils; 030import org.apache.commons.logging.Log; 031import org.apache.commons.logging.LogFactory; 032 033import org.nuxeo.common.utils.FileUtils; 034import org.nuxeo.common.utils.FileVersion; 035import org.nuxeo.connect.update.PackageException; 036import org.nuxeo.connect.update.task.Task; 037import org.nuxeo.connect.update.task.update.JarUtils.Match; 038 039/** 040 * Manage jar versions update. 041 * <p> 042 * To manipulate the jar version registry you need to create a new instance of this class. 043 * <p> 044 * If you want to modify the registry then you may want to synchronize the entire update process. This is how is done in 045 * the Task run method. 046 * <p> 047 * Only reading the registry is thread safe. 048 * <p> 049 * TODO backup md5 are not really used since we rely on versions - we can remove md5 050 * 051 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 052 */ 053public class UpdateManager { 054 055 private static final Log log = LogFactory.getLog(UpdateManager.class); 056 057 public static final String STUDIO_SNAPSHOT_VERSION = "0.0.0-SNAPSHOT"; 058 059 protected Task task; 060 061 protected Map<String, Entry> registry; 062 063 protected File file; 064 065 protected File backupRoot; 066 067 protected File serverRoot; 068 069 public UpdateManager(File serverRoot, File regFile) { 070 file = regFile; 071 backupRoot = new File(file.getParentFile(), "backup"); 072 backupRoot.mkdirs(); 073 this.serverRoot = serverRoot; 074 } 075 076 public File getServerRoot() { 077 return serverRoot; 078 } 079 080 public File getBackupRoot() { 081 return backupRoot; 082 } 083 084 public Task getTask() { 085 return task; 086 } 087 088 public Map<String, Entry> getRegistry() { 089 return registry; 090 } 091 092 public synchronized void load() throws PackageException { 093 if (!file.isFile()) { 094 registry = new HashMap<String, Entry>(); 095 return; 096 } 097 try { 098 registry = RegistrySerializer.load(file); 099 } catch (PackageException e) { 100 throw e; 101 } catch (IOException e) { 102 throw new PackageException("IOException while trying to load the registry", e); 103 } 104 } 105 106 public synchronized void store() throws PackageException { 107 try { 108 RegistrySerializer.store(registry, file); 109 } catch (IOException e) { 110 throw new PackageException("IOException while trying to write the registry", e); 111 } 112 } 113 114 public String getVersionPath(UpdateOptions opt) { 115 return getServerRelativePath(opt.getTargetFile()); 116 } 117 118 public String getKey(UpdateOptions opt) { 119 String key = getServerRelativePath(opt.getTargetDir()); 120 if (key.endsWith(File.separator)) { 121 key = key.concat(opt.nameWithoutVersion); 122 } else { 123 key = key.concat(File.separator).concat(opt.nameWithoutVersion); 124 } 125 return key; 126 } 127 128 public RollbackOptions update(UpdateOptions opt) throws PackageException { 129 String key = getKey(opt); 130 Entry entry = registry.get(key); 131 if (entry == null) { // New Entry 132 entry = createEntry(key); 133 } else if (!entry.hasBaseVersion() && entry.getLastVersion(false) == null) { 134 // Existing Entry but all versions provided only by packages with upgradeOnly => check missing base 135 // version... 136 if (createBaseVersion(entry)) { 137 log.warn("Registry repaired: JAR introduced without corresponding entry in the registry (copy task?) : " 138 + key); 139 } 140 } 141 Version v = entry.getVersion(opt.version); 142 boolean newVersion = v == null; 143 if (v == null) { 144 v = entry.addVersion(new Version(opt.getVersion())); 145 v.setPath(getVersionPath(opt)); 146 } 147 v.addPackage(opt); 148 if (newVersion || opt.isSnapshotVersion()) { 149 // Snapshots "backup" are overwritten by new versions 150 backupFile(opt.getFile(), v.getPath()); 151 } 152 153 Match<File> currentJar = findInstalledJar(key); 154 UpdateOptions optToUpdate = shouldUpdate(key, opt, currentJar); 155 if (optToUpdate != null) { 156 File currentFile = currentJar != null ? currentJar.object : null; 157 doUpdate(currentFile, optToUpdate); 158 } 159 160 return new RollbackOptions(key, opt); 161 } 162 163 /** 164 * Look if an update is required, taking into account the given UpdateOptions, the currently installed JAR and the 165 * other available JARs. 166 * 167 * @since 5.7 168 * @param key 169 * @param opt 170 * @param currentJar 171 * @return null if no update required, else the right UpdateOptions 172 * @throws PackageException 173 */ 174 protected UpdateOptions shouldUpdate(String key, UpdateOptions opt, Match<File> currentJar) 175 throws PackageException { 176 log.debug("Look for updating " + opt.file.getName()); 177 if (opt.upgradeOnly && currentJar == null) { 178 log.debug("=> don't update (upgradeOnly)"); 179 return null; 180 } 181 if (opt.allowDowngrade) { 182 log.debug("=> update (allowDowngrade)"); 183 return opt; 184 } 185 186 // !opt.allowDowngrade && (!opt.upgradeOnly || currentJar != null) ... 187 UpdateOptions optToUpdate = null; 188 Version packageVersion = registry.get(key).getVersion(opt.version); 189 Version greatestVersion = registry.get(key).getGreatestVersion(); 190 if (packageVersion.equals(greatestVersion)) { 191 optToUpdate = opt; 192 } else { // we'll use the greatest available JAR instead 193 optToUpdate = UpdateOptions.newInstance(opt.pkgId, new File(backupRoot, greatestVersion.path), 194 opt.targetDir); 195 } 196 FileVersion greatestFileVersion = greatestVersion.getFileVersion(); 197 if (currentJar == null) { 198 log.debug("=> update (new) " + greatestFileVersion); 199 return optToUpdate; 200 } 201 202 // !opt.allowDowngrade && currentJar != null ... 203 FileVersion currentVersion = new FileVersion(currentJar.version); 204 log.debug("=> comparing " + greatestFileVersion + " with " + currentVersion); 205 if (greatestFileVersion.greaterThan(currentVersion)) { 206 log.debug("=> update (greater)"); 207 return optToUpdate; 208 } else if (greatestFileVersion.equals(currentVersion)) { 209 if (greatestFileVersion.isSnapshot()) { 210 FileInputStream is1 = null; 211 FileInputStream is2 = null; 212 try { 213 is1 = new FileInputStream(new File(backupRoot, greatestVersion.path)); 214 is2 = new FileInputStream(currentJar.object); 215 if (IOUtils.contentEquals(is1, is2)) { 216 log.debug("=> don't update (already installed)"); 217 return null; 218 } else { 219 log.debug("=> update (newer SNAPSHOT)"); 220 return optToUpdate; 221 } 222 } catch (IOException e) { 223 throw new PackageException(e); 224 } finally { 225 IOUtils.closeQuietly(is1); 226 IOUtils.closeQuietly(is2); 227 } 228 } else { 229 log.debug("=> don't update (already installed)"); 230 return null; 231 } 232 } else { 233 log.debug("Don't update (lower)"); 234 return null; 235 } 236 } 237 238 /** 239 * Ugly method to know what file is going to be deleted before it is, so that it can be undeployed for hotreload. 240 * <p> 241 * FIXME: will only handle simple cases for now (ignores version, etc...), e.g only tested with the main Studio 242 * jars. Should use version from RollbackOptions 243 * 244 * @since 5.6 245 */ 246 public File getRollbackTarget(RollbackOptions opt) { 247 String entryKey = opt.getKey(); 248 Match<File> m = findInstalledJar(entryKey); 249 if (m != null) { 250 return m.object; 251 } else { 252 log.trace("Could not find jar with key: " + entryKey); 253 return null; 254 } 255 } 256 257 /** 258 * Perform a rollback. 259 * <p> 260 * TODO the deleteOnExit is inherited from the current rollback command ... may be it should be read from the 261 * version that is rollbacked. (deleteOnExit should be an attribute of the entry not of the version) 262 * 263 * @param opt 264 * @throws PackageException 265 */ 266 public void rollback(RollbackOptions opt) throws PackageException { 267 Entry entry = registry.get(opt.getKey()); 268 if (entry == null) { 269 log.debug("Key not found in registry for: " + opt); 270 return; 271 } 272 Version v = entry.getVersion(opt.getVersion()); 273 if (v == null) { 274 // allow empty version for Studio snapshot... 275 v = entry.getVersion(STUDIO_SNAPSHOT_VERSION); 276 } 277 if (v == null) { 278 log.debug("Version not found in registry for: " + opt); 279 return; 280 } 281 // store current last version 282 Version lastVersion = entry.getLastVersion(); 283 boolean removeBackup = false; 284 285 v.removePackage(opt.getPackageId()); 286 if (!v.hasPackages()) { 287 // remove this version 288 entry.removeVersion(v); 289 removeBackup = true; 290 } 291 292 // Include upgradeOnly versions only if there is a base version or a non-upgradeOnly version 293 boolean includeUpgradeOnly = entry.hasBaseVersion() || entry.getLastVersion(false) != null; 294 Version versionToRollback = entry.getLastVersion(includeUpgradeOnly); 295 if (versionToRollback == null) { 296 // no more versions - remove entry and rollback base version if any 297 if (entry.isEmpty()) { 298 registry.remove(entry.getKey()); 299 } 300 rollbackBaseVersion(entry, opt); 301 } else if (versionToRollback != lastVersion) { 302 // we removed the currently installed version so we need to rollback 303 rollbackVersion(entry, versionToRollback, opt); 304 } else { 305 // handle jars that were blocked using allowDowngrade or 306 // upgradeOnly 307 Match<File> m = findInstalledJar(opt.getKey()); 308 if (m != null) { 309 if (entry.getVersion(m.version) == null) { 310 // the currently installed version is no more in registry 311 // should be the one we just removed 312 Version greatest = entry.getGreatestVersion(); 313 if (greatest != null) { 314 // rollback to the greatest version 315 rollbackVersion(entry, greatest, opt); 316 } 317 } 318 } 319 } 320 321 if (removeBackup) { 322 removeBackup(v.getPath()); 323 } 324 325 } 326 327 protected void rollbackBaseVersion(Entry entry, RollbackOptions opt) throws PackageException { 328 Version base = entry.getBaseVersion(); 329 if (base != null) { 330 rollbackVersion(entry, base, opt); 331 removeBackup(base.getPath()); 332 } else { 333 // simply remove the installed file if exists 334 Match<File> m = JarUtils.findJar(serverRoot, entry.getKey()); 335 if (m != null) { 336 if (opt.isDeleteOnExit()) { 337 m.object.deleteOnExit(); 338 } else { 339 m.object.delete(); 340 } 341 } 342 } 343 } 344 345 protected void rollbackVersion(Entry entry, Version version, RollbackOptions opt) throws PackageException { 346 File versionFile = getBackup(version.getPath()); 347 if (!versionFile.isFile()) { 348 log.warn("Could not rollback version " + version.getPath() + " since the backup file was not found"); 349 return; 350 } 351 Match<File> m = findInstalledJar(entry.getKey()); 352 File oldFile = m != null ? m.object : null; 353 File targetFile = getTargetFile(version.getPath()); 354 if (deleteOldFile(targetFile, oldFile, opt.deleteOnExit)) { 355 copy(versionFile, targetFile); 356 } 357 } 358 359 public String getServerRelativePath(File someFile) { 360 String path; 361 String serverPath; 362 try { 363 path = someFile.getCanonicalPath(); 364 serverPath = serverRoot.getCanonicalPath(); 365 } catch (IOException e) { 366 log.error("Failed to get a canonical path. " + "Fall back to absolute paths...", e); 367 path = someFile.getAbsolutePath(); 368 serverPath = serverRoot.getAbsolutePath(); 369 } 370 if (!serverPath.endsWith(File.separator)) { 371 serverPath = serverPath.concat(File.separator); 372 } 373 if (path.startsWith(serverPath)) { 374 return path.substring(serverPath.length()); 375 } 376 return path; 377 } 378 379 /** 380 * Create a new entry in the registry given the entry key. A base version will be automatically created if needed. 381 * 382 * @param key 383 * @throws PackageException 384 */ 385 public Entry createEntry(String key) throws PackageException { 386 Entry entry = new Entry(key); 387 createBaseVersion(entry); 388 registry.put(key, entry); 389 return entry; 390 } 391 392 /** 393 * Create a base version for the given entry if needed. 394 * 395 * @param entry 396 * @return true if a base version was actually created, false otherwise 397 * @throws PackageException 398 * @since 1.4.26 399 */ 400 public boolean createBaseVersion(Entry entry) throws PackageException { 401 Match<File> m = JarUtils.findJar(serverRoot, entry.getKey()); 402 if (m != null) { 403 String path = getServerRelativePath(m.object); 404 Version base = new Version(m.version); 405 base.setPath(path); 406 entry.setBaseVersion(base); 407 backupFile(m.object, path); 408 return true; 409 } 410 return false; 411 } 412 413 /** 414 * Backup the given file in the registry storage. Backup is not a backup performed on removed files: it is rather 415 * like a uniformed storage of all libraries potentially installed by packages (whereas each package can have its 416 * own directory structure). So SNAPSHOT will always be overwritten. Backup of original SNAPSHOT can be found in the 417 * backup directory of the stored package. 418 * 419 * @param fileToBackup 420 * @param path 421 */ 422 protected void backupFile(File fileToBackup, String path) throws PackageException { 423 try { 424 File dst = new File(backupRoot, path); 425 copy(fileToBackup, dst); 426 // String md5 = IOUtils.createMd5(dst); 427 // FileUtils.writeFile(new 428 // File(dst.getAbsolutePath().concat(".md5")), 429 // md5); 430 } catch (PackageException e) { 431 throw new PackageException("Failed to backup file: " + path, e); 432 } 433 } 434 435 /** 436 * Remove the backup given its path. This is also removing the md5. 437 * 438 * @param path 439 */ 440 protected void removeBackup(String path) { 441 File dst = new File(backupRoot, path); 442 if (!dst.delete()) { 443 dst.deleteOnExit(); 444 } 445 } 446 447 protected File getBackup(String path) { 448 return new File(backupRoot, path); 449 } 450 451 protected File getTargetFile(String path) { 452 return new File(serverRoot, path); 453 } 454 455 protected void copy(File src, File dst) throws PackageException { 456 try { 457 dst.getParentFile().mkdirs(); 458 File tmp = new File(dst.getPath() + ".tmp"); 459 // File tmp = new File(dst.getParentFile(), dst.getName() + 460 // ".tmp"); 461 FileUtils.copy(src, tmp); 462 if (!tmp.renameTo(dst)) { 463 tmp.delete(); 464 FileUtils.copy(src, dst); 465 } 466 } catch (IOException e) { 467 throw new PackageException("Failed to copy file: " + src + " to " + dst, e); 468 } 469 } 470 471 protected boolean deleteOldFile(File targetFile, File oldFile, boolean deleteOnExit) { 472 if (oldFile == null || !oldFile.exists()) { 473 return false; 474 } 475 if (deleteOnExit) { 476 if (targetFile.getName().equals(oldFile.getName())) { 477 oldFile.delete(); 478 } else { 479 oldFile.deleteOnExit(); 480 } 481 } else { 482 oldFile.delete(); 483 } 484 return true; 485 } 486 487 public Match<File> findInstalledJar(String key) { 488 return JarUtils.findJar(serverRoot, key); 489 } 490 491 public Match<File> findBackupJar(String key) { 492 return JarUtils.findJar(backupRoot, key); 493 } 494 495 /** 496 * Update oldFile with file pointed by opt 497 * 498 * @throws PackageException 499 */ 500 public void doUpdate(File oldFile, UpdateOptions opt) throws PackageException { 501 deleteOldFile(opt.targetFile, oldFile, opt.deleteOnExit); 502 copy(opt.file, opt.targetFile); 503 log.trace("Updated " + opt.targetFile); 504 } 505 506}