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<>(); 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 * @return null if no update required, else the right UpdateOptions 169 */ 170 protected UpdateOptions shouldUpdate(String key, UpdateOptions opt, Match<File> currentJar) 171 throws PackageException { 172 log.debug("Look for updating " + opt.file.getName()); 173 if (opt.upgradeOnly && currentJar == null) { 174 log.debug("=> don't update (upgradeOnly)"); 175 return null; 176 } 177 if (opt.allowDowngrade) { 178 log.debug("=> update (allowDowngrade)"); 179 return opt; 180 } 181 182 // !opt.allowDowngrade && (!opt.upgradeOnly || currentJar != null) ... 183 UpdateOptions optToUpdate = null; 184 Version packageVersion = registry.get(key).getVersion(opt.version); 185 Version greatestVersion = registry.get(key).getGreatestVersion(); 186 if (packageVersion.equals(greatestVersion)) { 187 optToUpdate = opt; 188 } else { // we'll use the greatest available JAR instead 189 optToUpdate = UpdateOptions.newInstance(opt.pkgId, new File(backupRoot, greatestVersion.path), 190 opt.targetDir); 191 } 192 FileVersion greatestFileVersion = greatestVersion.getFileVersion(); 193 if (currentJar == null) { 194 log.debug("=> update (new) " + greatestFileVersion); 195 return optToUpdate; 196 } 197 198 // !opt.allowDowngrade && currentJar != null ... 199 FileVersion currentVersion = new FileVersion(currentJar.version); 200 log.debug("=> comparing " + greatestFileVersion + " with " + currentVersion); 201 if (greatestFileVersion.greaterThan(currentVersion)) { 202 log.debug("=> update (greater)"); 203 return optToUpdate; 204 } else if (greatestFileVersion.equals(currentVersion)) { 205 if (greatestFileVersion.isSnapshot()) { 206 FileInputStream is1 = null; 207 FileInputStream is2 = null; 208 try { 209 is1 = new FileInputStream(new File(backupRoot, greatestVersion.path)); 210 is2 = new FileInputStream(currentJar.object); 211 if (IOUtils.contentEquals(is1, is2)) { 212 log.debug("=> don't update (already installed)"); 213 return null; 214 } else { 215 log.debug("=> update (newer SNAPSHOT)"); 216 return optToUpdate; 217 } 218 } catch (IOException e) { 219 throw new PackageException(e); 220 } finally { 221 IOUtils.closeQuietly(is1); 222 IOUtils.closeQuietly(is2); 223 } 224 } else { 225 log.debug("=> don't update (already installed)"); 226 return null; 227 } 228 } else { 229 log.debug("Don't update (lower)"); 230 return null; 231 } 232 } 233 234 /** 235 * Ugly method to know what file is going to be deleted before it is, so that it can be undeployed for hotreload. 236 * <p> 237 * FIXME: will only handle simple cases for now (ignores version, etc...), e.g only tested with the main Studio 238 * jars. Should use version from RollbackOptions 239 * 240 * @since 5.6 241 */ 242 public File getRollbackTarget(RollbackOptions opt) { 243 String entryKey = opt.getKey(); 244 Match<File> m = findInstalledJar(entryKey); 245 if (m != null) { 246 return m.object; 247 } else { 248 log.trace("Could not find jar with key: " + entryKey); 249 return null; 250 } 251 } 252 253 /** 254 * Perform a rollback. 255 * <p> 256 * TODO the deleteOnExit is inherited from the current rollback command ... may be it should be read from the 257 * version that is rollbacked. (deleteOnExit should be an attribute of the entry not of the version) 258 */ 259 public void rollback(RollbackOptions opt) throws PackageException { 260 Entry entry = registry.get(opt.getKey()); 261 if (entry == null) { 262 log.debug("Key not found in registry for: " + opt); 263 return; 264 } 265 Version v = entry.getVersion(opt.getVersion()); 266 if (v == null) { 267 // allow empty version for Studio snapshot... 268 v = entry.getVersion(STUDIO_SNAPSHOT_VERSION); 269 } 270 if (v == null) { 271 log.debug("Version not found in registry for: " + opt); 272 return; 273 } 274 // store current last version 275 Version lastVersion = entry.getLastVersion(); 276 boolean removeBackup = false; 277 278 v.removePackage(opt.getPackageId()); 279 if (!v.hasPackages()) { 280 // remove this version 281 entry.removeVersion(v); 282 removeBackup = true; 283 } 284 285 // Include upgradeOnly versions only if there is a base version or a non-upgradeOnly version 286 boolean includeUpgradeOnly = entry.hasBaseVersion() || entry.getLastVersion(false) != null; 287 Version versionToRollback = entry.getLastVersion(includeUpgradeOnly); 288 if (versionToRollback == null) { 289 // no more versions - remove entry and rollback base version if any 290 if (entry.isEmpty()) { 291 registry.remove(entry.getKey()); 292 } 293 rollbackBaseVersion(entry, opt); 294 } else if (versionToRollback != lastVersion) { 295 // we removed the currently installed version so we need to rollback 296 rollbackVersion(entry, versionToRollback, opt); 297 } else { 298 // handle jars that were blocked using allowDowngrade or 299 // upgradeOnly 300 Match<File> m = findInstalledJar(opt.getKey()); 301 if (m != null) { 302 if (entry.getVersion(m.version) == null) { 303 // the currently installed version is no more in registry 304 // should be the one we just removed 305 Version greatest = entry.getGreatestVersion(); 306 if (greatest != null) { 307 // rollback to the greatest version 308 rollbackVersion(entry, greatest, opt); 309 } 310 } 311 } 312 } 313 314 if (removeBackup) { 315 removeBackup(v.getPath()); 316 } 317 318 } 319 320 protected void rollbackBaseVersion(Entry entry, RollbackOptions opt) throws PackageException { 321 Version base = entry.getBaseVersion(); 322 if (base != null) { 323 rollbackVersion(entry, base, opt); 324 removeBackup(base.getPath()); 325 } else { 326 // simply remove the installed file if exists 327 Match<File> m = JarUtils.findJar(serverRoot, entry.getKey()); 328 if (m != null) { 329 if (opt.isDeleteOnExit()) { 330 m.object.deleteOnExit(); 331 } else { 332 m.object.delete(); 333 } 334 } 335 } 336 } 337 338 protected void rollbackVersion(Entry entry, Version version, RollbackOptions opt) throws PackageException { 339 File versionFile = getBackup(version.getPath()); 340 if (!versionFile.isFile()) { 341 log.warn("Could not rollback version " + version.getPath() + " since the backup file was not found"); 342 return; 343 } 344 Match<File> m = findInstalledJar(entry.getKey()); 345 File oldFile = m != null ? m.object : null; 346 File targetFile = getTargetFile(version.getPath()); 347 if (deleteOldFile(targetFile, oldFile, opt.deleteOnExit)) { 348 copy(versionFile, targetFile); 349 } 350 } 351 352 public String getServerRelativePath(File someFile) { 353 String path; 354 String serverPath; 355 try { 356 path = someFile.getCanonicalPath(); 357 serverPath = serverRoot.getCanonicalPath(); 358 } catch (IOException e) { 359 log.error("Failed to get a canonical path. " + "Fall back to absolute paths...", e); 360 path = someFile.getAbsolutePath(); 361 serverPath = serverRoot.getAbsolutePath(); 362 } 363 if (!serverPath.endsWith(File.separator)) { 364 serverPath = serverPath.concat(File.separator); 365 } 366 if (path.startsWith(serverPath)) { 367 return path.substring(serverPath.length()); 368 } 369 return path; 370 } 371 372 /** 373 * Create a new entry in the registry given the entry key. A base version will be automatically created if needed. 374 */ 375 public Entry createEntry(String key) throws PackageException { 376 Entry entry = new Entry(key); 377 createBaseVersion(entry); 378 registry.put(key, entry); 379 return entry; 380 } 381 382 /** 383 * Create a base version for the given entry if needed. 384 * 385 * @return true if a base version was actually created, false otherwise 386 * @since 1.4.26 387 */ 388 public boolean createBaseVersion(Entry entry) throws PackageException { 389 Match<File> m = JarUtils.findJar(serverRoot, entry.getKey()); 390 if (m != null) { 391 String path = getServerRelativePath(m.object); 392 Version base = new Version(m.version); 393 base.setPath(path); 394 entry.setBaseVersion(base); 395 backupFile(m.object, path); 396 return true; 397 } 398 return false; 399 } 400 401 /** 402 * Backup the given file in the registry storage. Backup is not a backup performed on removed files: it is rather 403 * like a uniformed storage of all libraries potentially installed by packages (whereas each package can have its 404 * own directory structure). So SNAPSHOT will always be overwritten. Backup of original SNAPSHOT can be found in the 405 * backup directory of the stored package. 406 */ 407 protected void backupFile(File fileToBackup, String path) throws PackageException { 408 try { 409 File dst = new File(backupRoot, path); 410 copy(fileToBackup, dst); 411 // String md5 = IOUtils.createMd5(dst); 412 // FileUtils.writeFile(new 413 // File(dst.getAbsolutePath().concat(".md5")), 414 // md5); 415 } catch (PackageException e) { 416 throw new PackageException("Failed to backup file: " + path, e); 417 } 418 } 419 420 /** 421 * Remove the backup given its path. This is also removing the md5. 422 */ 423 protected void removeBackup(String path) { 424 File dst = new File(backupRoot, path); 425 if (!dst.delete()) { 426 dst.deleteOnExit(); 427 } 428 } 429 430 protected File getBackup(String path) { 431 return new File(backupRoot, path); 432 } 433 434 protected File getTargetFile(String path) { 435 return new File(serverRoot, path); 436 } 437 438 protected void copy(File src, File dst) throws PackageException { 439 try { 440 dst.getParentFile().mkdirs(); 441 File tmp = new File(dst.getPath() + ".tmp"); 442 // File tmp = new File(dst.getParentFile(), dst.getName() + 443 // ".tmp"); 444 FileUtils.copy(src, tmp); 445 if (!tmp.renameTo(dst)) { 446 tmp.delete(); 447 FileUtils.copy(src, dst); 448 } 449 } catch (IOException e) { 450 throw new PackageException("Failed to copy file: " + src + " to " + dst, e); 451 } 452 } 453 454 protected boolean deleteOldFile(File targetFile, File oldFile, boolean deleteOnExit) { 455 if (oldFile == null || !oldFile.exists()) { 456 return false; 457 } 458 if (deleteOnExit) { 459 if (targetFile.getName().equals(oldFile.getName())) { 460 oldFile.delete(); 461 } else { 462 oldFile.deleteOnExit(); 463 } 464 } else { 465 oldFile.delete(); 466 } 467 return true; 468 } 469 470 public Match<File> findInstalledJar(String key) { 471 return JarUtils.findJar(serverRoot, key); 472 } 473 474 public Match<File> findBackupJar(String key) { 475 return JarUtils.findJar(backupRoot, key); 476 } 477 478 /** 479 * Update oldFile with file pointed by opt 480 */ 481 public void doUpdate(File oldFile, UpdateOptions opt) throws PackageException { 482 deleteOldFile(opt.targetFile, oldFile, opt.deleteOnExit); 483 copy(opt.file, opt.targetFile); 484 log.trace("Updated " + opt.targetFile); 485 } 486 487}