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