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