/* * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * bstefanescu * jcarsique * Yannis JULIENNE */ package org.nuxeo.connect.update.task.update; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.HashMap; import java.util.Map; import org.apache.commons.io.IOUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.common.utils.FileUtils; import org.nuxeo.common.utils.FileVersion; import org.nuxeo.connect.update.PackageException; import org.nuxeo.connect.update.task.Task; import org.nuxeo.connect.update.task.update.JarUtils.Match; /** * Manage jar versions update. * <p> * To manipulate the jar version registry you need to create a new instance of this class. * <p> * If you want to modify the registry then you may want to synchronize the entire update process. This is how is done in * the Task run method. * <p> * Only reading the registry is thread safe. * <p> * TODO backup md5 are not really used since we rely on versions - we can remove md5 * * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> */ public class UpdateManager { private static final Log log = LogFactory.getLog(UpdateManager.class); public static final String STUDIO_SNAPSHOT_VERSION = "0.0.0-SNAPSHOT"; protected Task task; protected Map<String, Entry> registry; protected File file; protected File backupRoot; protected File serverRoot; public UpdateManager(File serverRoot, File regFile) { file = regFile; backupRoot = new File(file.getParentFile(), "backup"); backupRoot.mkdirs(); this.serverRoot = serverRoot; } public File getServerRoot() { return serverRoot; } public File getBackupRoot() { return backupRoot; } public Task getTask() { return task; } public Map<String, Entry> getRegistry() { return registry; } public synchronized void load() throws PackageException { if (!file.isFile()) { registry = new HashMap<String, Entry>(); return; } try { registry = RegistrySerializer.load(file); } catch (PackageException e) { throw e; } catch (IOException e) { throw new PackageException("IOException while trying to load the registry", e); } } public synchronized void store() throws PackageException { try { RegistrySerializer.store(registry, file); } catch (IOException e) { throw new PackageException("IOException while trying to write the registry", e); } } public String getVersionPath(UpdateOptions opt) { return getServerRelativePath(opt.getTargetFile()); } public String getKey(UpdateOptions opt) { String key = getServerRelativePath(opt.getTargetDir()); if (key.endsWith(File.separator)) { key = key.concat(opt.nameWithoutVersion); } else { key = key.concat(File.separator).concat(opt.nameWithoutVersion); } return key; } public RollbackOptions update(UpdateOptions opt) throws PackageException { String key = getKey(opt); Entry entry = registry.get(key); if (entry == null) { // New Entry entry = createEntry(key); } else if (!entry.hasBaseVersion() && entry.getLastVersion(false) == null) { // Existing Entry but all versions provided only by packages with upgradeOnly => check missing base // version... if (createBaseVersion(entry)) { log.warn("Registry repaired: JAR introduced without corresponding entry in the registry (copy task?) : " + key); } } Version v = entry.getVersion(opt.version); boolean newVersion = v == null; if (v == null) { v = entry.addVersion(new Version(opt.getVersion())); v.setPath(getVersionPath(opt)); } v.addPackage(opt); if (newVersion || opt.isSnapshotVersion()) { // Snapshots "backup" are overwritten by new versions backupFile(opt.getFile(), v.getPath()); } Match<File> currentJar = findInstalledJar(key); UpdateOptions optToUpdate = shouldUpdate(key, opt, currentJar); if (optToUpdate != null) { File currentFile = currentJar != null ? currentJar.object : null; doUpdate(currentFile, optToUpdate); } return new RollbackOptions(key, opt); } /** * Look if an update is required, taking into account the given UpdateOptions, the currently installed JAR and the * other available JARs. * * @since 5.7 * @param key * @param opt * @param currentJar * @return null if no update required, else the right UpdateOptions * @throws PackageException */ protected UpdateOptions shouldUpdate(String key, UpdateOptions opt, Match<File> currentJar) throws PackageException { log.debug("Look for updating " + opt.file.getName()); if (opt.upgradeOnly && currentJar == null) { log.debug("=> don't update (upgradeOnly)"); return null; } if (opt.allowDowngrade) { log.debug("=> update (allowDowngrade)"); return opt; } // !opt.allowDowngrade && (!opt.upgradeOnly || currentJar != null) ... UpdateOptions optToUpdate = null; Version packageVersion = registry.get(key).getVersion(opt.version); Version greatestVersion = registry.get(key).getGreatestVersion(); if (packageVersion.equals(greatestVersion)) { optToUpdate = opt; } else { // we'll use the greatest available JAR instead optToUpdate = UpdateOptions.newInstance(opt.pkgId, new File(backupRoot, greatestVersion.path), opt.targetDir); } FileVersion greatestFileVersion = greatestVersion.getFileVersion(); if (currentJar == null) { log.debug("=> update (new) " + greatestFileVersion); return optToUpdate; } // !opt.allowDowngrade && currentJar != null ... FileVersion currentVersion = new FileVersion(currentJar.version); log.debug("=> comparing " + greatestFileVersion + " with " + currentVersion); if (greatestFileVersion.greaterThan(currentVersion)) { log.debug("=> update (greater)"); return optToUpdate; } else if (greatestFileVersion.equals(currentVersion)) { if (greatestFileVersion.isSnapshot()) { FileInputStream is1 = null; FileInputStream is2 = null; try { is1 = new FileInputStream(new File(backupRoot, greatestVersion.path)); is2 = new FileInputStream(currentJar.object); if (IOUtils.contentEquals(is1, is2)) { log.debug("=> don't update (already installed)"); return null; } else { log.debug("=> update (newer SNAPSHOT)"); return optToUpdate; } } catch (IOException e) { throw new PackageException(e); } finally { IOUtils.closeQuietly(is1); IOUtils.closeQuietly(is2); } } else { log.debug("=> don't update (already installed)"); return null; } } else { log.debug("Don't update (lower)"); return null; } } /** * Ugly method to know what file is going to be deleted before it is, so that it can be undeployed for hotreload. * <p> * FIXME: will only handle simple cases for now (ignores version, etc...), e.g only tested with the main Studio * jars. Should use version from RollbackOptions * * @since 5.6 */ public File getRollbackTarget(RollbackOptions opt) { String entryKey = opt.getKey(); Match<File> m = findInstalledJar(entryKey); if (m != null) { return m.object; } else { log.trace("Could not find jar with key: " + entryKey); return null; } } /** * Perform a rollback. * <p> * TODO the deleteOnExit is inherited from the current rollback command ... may be it should be read from the * version that is rollbacked. (deleteOnExit should be an attribute of the entry not of the version) * * @param opt * @throws PackageException */ public void rollback(RollbackOptions opt) throws PackageException { Entry entry = registry.get(opt.getKey()); if (entry == null) { log.debug("Key not found in registry for: " + opt); return; } Version v = entry.getVersion(opt.getVersion()); if (v == null) { // allow empty version for Studio snapshot... v = entry.getVersion(STUDIO_SNAPSHOT_VERSION); } if (v == null) { log.debug("Version not found in registry for: " + opt); return; } // store current last version Version lastVersion = entry.getLastVersion(); boolean removeBackup = false; v.removePackage(opt.getPackageId()); if (!v.hasPackages()) { // remove this version entry.removeVersion(v); removeBackup = true; } // Include upgradeOnly versions only if there is a base version or a non-upgradeOnly version boolean includeUpgradeOnly = entry.hasBaseVersion() || entry.getLastVersion(false) != null; Version versionToRollback = entry.getLastVersion(includeUpgradeOnly); if (versionToRollback == null) { // no more versions - remove entry and rollback base version if any if (entry.isEmpty()) { registry.remove(entry.getKey()); } rollbackBaseVersion(entry, opt); } else if (versionToRollback != lastVersion) { // we removed the currently installed version so we need to rollback rollbackVersion(entry, versionToRollback, opt); } else { // handle jars that were blocked using allowDowngrade or // upgradeOnly Match<File> m = findInstalledJar(opt.getKey()); if (m != null) { if (entry.getVersion(m.version) == null) { // the currently installed version is no more in registry // should be the one we just removed Version greatest = entry.getGreatestVersion(); if (greatest != null) { // rollback to the greatest version rollbackVersion(entry, greatest, opt); } } } } if (removeBackup) { removeBackup(v.getPath()); } } protected void rollbackBaseVersion(Entry entry, RollbackOptions opt) throws PackageException { Version base = entry.getBaseVersion(); if (base != null) { rollbackVersion(entry, base, opt); removeBackup(base.getPath()); } else { // simply remove the installed file if exists Match<File> m = JarUtils.findJar(serverRoot, entry.getKey()); if (m != null) { if (opt.isDeleteOnExit()) { m.object.deleteOnExit(); } else { m.object.delete(); } } } } protected void rollbackVersion(Entry entry, Version version, RollbackOptions opt) throws PackageException { File versionFile = getBackup(version.getPath()); if (!versionFile.isFile()) { log.warn("Could not rollback version " + version.getPath() + " since the backup file was not found"); return; } Match<File> m = findInstalledJar(entry.getKey()); File oldFile = m != null ? m.object : null; File targetFile = getTargetFile(version.getPath()); if (deleteOldFile(targetFile, oldFile, opt.deleteOnExit)) { copy(versionFile, targetFile); } } public String getServerRelativePath(File someFile) { String path; String serverPath; try { path = someFile.getCanonicalPath(); serverPath = serverRoot.getCanonicalPath(); } catch (IOException e) { log.error("Failed to get a canonical path. " + "Fall back to absolute paths...", e); path = someFile.getAbsolutePath(); serverPath = serverRoot.getAbsolutePath(); } if (!serverPath.endsWith(File.separator)) { serverPath = serverPath.concat(File.separator); } if (path.startsWith(serverPath)) { return path.substring(serverPath.length()); } return path; } /** * Create a new entry in the registry given the entry key. A base version will be automatically created if needed. * * @param key * @throws PackageException */ public Entry createEntry(String key) throws PackageException { Entry entry = new Entry(key); createBaseVersion(entry); registry.put(key, entry); return entry; } /** * Create a base version for the given entry if needed. * * @param entry * @return true if a base version was actually created, false otherwise * @throws PackageException * @since 1.4.26 */ public boolean createBaseVersion(Entry entry) throws PackageException { Match<File> m = JarUtils.findJar(serverRoot, entry.getKey()); if (m != null) { String path = getServerRelativePath(m.object); Version base = new Version(m.version); base.setPath(path); entry.setBaseVersion(base); backupFile(m.object, path); return true; } return false; } /** * Backup the given file in the registry storage. Backup is not a backup performed on removed files: it is rather * like a uniformed storage of all libraries potentially installed by packages (whereas each package can have its * own directory structure). So SNAPSHOT will always be overwritten. Backup of original SNAPSHOT can be found in the * backup directory of the stored package. * * @param fileToBackup * @param path */ protected void backupFile(File fileToBackup, String path) throws PackageException { try { File dst = new File(backupRoot, path); copy(fileToBackup, dst); // String md5 = IOUtils.createMd5(dst); // FileUtils.writeFile(new // File(dst.getAbsolutePath().concat(".md5")), // md5); } catch (PackageException e) { throw new PackageException("Failed to backup file: " + path, e); } } /** * Remove the backup given its path. This is also removing the md5. * * @param path */ protected void removeBackup(String path) { File dst = new File(backupRoot, path); if (!dst.delete()) { dst.deleteOnExit(); } } protected File getBackup(String path) { return new File(backupRoot, path); } protected File getTargetFile(String path) { return new File(serverRoot, path); } protected void copy(File src, File dst) throws PackageException { try { dst.getParentFile().mkdirs(); File tmp = new File(dst.getPath() + ".tmp"); // File tmp = new File(dst.getParentFile(), dst.getName() + // ".tmp"); FileUtils.copy(src, tmp); if (!tmp.renameTo(dst)) { tmp.delete(); FileUtils.copy(src, dst); } } catch (IOException e) { throw new PackageException("Failed to copy file: " + src + " to " + dst, e); } } protected boolean deleteOldFile(File targetFile, File oldFile, boolean deleteOnExit) { if (oldFile == null || !oldFile.exists()) { return false; } if (deleteOnExit) { if (targetFile.getName().equals(oldFile.getName())) { oldFile.delete(); } else { oldFile.deleteOnExit(); } } else { oldFile.delete(); } return true; } public Match<File> findInstalledJar(String key) { return JarUtils.findJar(serverRoot, key); } public Match<File> findBackupJar(String key) { return JarUtils.findJar(backupRoot, key); } /** * Update oldFile with file pointed by opt * * @throws PackageException */ public void doUpdate(File oldFile, UpdateOptions opt) throws PackageException { deleteOldFile(opt.targetFile, oldFile, opt.deleteOnExit); copy(opt.file, opt.targetFile); log.trace("Updated " + opt.targetFile); } }