/* * (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.standalone.commands; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.util.Map; import org.apache.commons.io.FileUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.common.Environment; import org.nuxeo.common.utils.FileMatcher; import org.nuxeo.common.utils.FileRef; import org.nuxeo.common.utils.FileVersion; import org.nuxeo.connect.update.PackageException; import org.nuxeo.connect.update.ValidationStatus; import org.nuxeo.connect.update.task.Command; import org.nuxeo.connect.update.task.Task; import org.nuxeo.connect.update.task.standalone.UninstallTask; import org.nuxeo.connect.update.util.IOUtils; import org.nuxeo.connect.update.xml.XmlWriter; import org.w3c.dom.Element; /** * Copy a file to the given target directory or file. If the target is a directory the file name is preserved. If the * target file exists it will be replaced if overwrite is true otherwise the command validation fails. If the source * file is a directory, then the files it contents will be recursively copied. * <p> * If md5 is set then the copy command will be validated only if the target file has the same md5 as the one specified * in the command. * <p> * The Copy command has as inverse either Delete either another Copy command. If the file was copied without overwriting * then Delete is the inverse (with a md5 set to the one of the copied file). If the file was overwritten then the * inverse of Copy command is another copy command with the md5 to the one of the copied file and the overwrite flag to * true. The file to copy will be the backup of the overwritten file. * * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> */ public class Copy extends AbstractCommand { protected static final Log log = LogFactory.getLog(Copy.class); public static final String ID = "copy"; protected static final String LAUNCHER_JAR = "nuxeo-launcher.jar"; protected static final String LAUNCHER_CHANGED_PROPERTY = "launcher.changed"; /** * The source file. It can be a file or a directory. */ protected File file; /** * The target file. It can be a directory since 5.5 */ protected File tofile; protected boolean overwrite; protected String md5; protected boolean removeOnExit; /** * @since 5.5 */ protected boolean append; /** * @since 5.5 */ private boolean overwriteIfNewerVersion; /** * @since 5.5 */ private boolean upgradeOnly; protected Copy(String id) { super(id); } public Copy() { this(ID); } public Copy(File file, File tofile, String md5, boolean overwrite) { this(ID, file, tofile, md5, overwrite, false); } public Copy(File file, File tofile, String md5, boolean overwrite, boolean removeOnExit) { this(ID, file, tofile, md5, overwrite, removeOnExit); } protected Copy(String id, File file, File tofile, String md5, boolean overwrite, boolean removeOnExit) { this(id); this.file = file; this.tofile = tofile; this.md5 = md5; this.overwrite = overwrite; this.removeOnExit = removeOnExit; } @Override protected Command doRun(Task task, Map<String, String> prefs) throws PackageException { if (!file.exists()) { log.warn("Can't copy " + file + " . File missing."); return null; } return doCopy(task, prefs, file, tofile, overwrite); } /** * @param doOverwrite * @since 5.5 */ protected Command doCopy(Task task, Map<String, String> prefs, File fileToCopy, File dst, boolean doOverwrite) throws PackageException { String dstmd5; File bak = null; CompositeCommand rollbackCommand = new CompositeCommand(); if (fileToCopy.isDirectory()) { if (fileToCopy != file) { dst = new File(dst, fileToCopy.getName()); } dst.mkdirs(); for (File childFile : fileToCopy.listFiles()) { rollbackCommand.addCommand(doCopy(task, prefs, childFile, dst, doOverwrite)); } return rollbackCommand; } if (dst.isDirectory()) { dst = new File(dst, fileToCopy.getName()); } try { FileMatcher filenameMatcher = FileMatcher.getMatcher("{n:.*-}[0-9]+.*\\.jar"); boolean isVersionnedJarFile = filenameMatcher.match(fileToCopy.getName()); if (isVersionnedJarFile) { log.warn(String.format( "Use of the <copy /> command on JAR files is not recommended, prefer using <update /> command to ensure a safe rollback. (%s)", fileToCopy.getName())); } if (isVersionnedJarFile && (overwriteIfNewerVersion || upgradeOnly)) { // Compare source and destination versions set in filename FileVersion fileToCopyVersion, dstVersion = null; String filenameWithoutVersion = filenameMatcher.getValue(); FileMatcher versionMatcher = FileMatcher.getMatcher(filenameWithoutVersion + "{v:[0-9]+.*}\\.jar"); // Get new file version if (versionMatcher.match(fileToCopy.getName())) { fileToCopyVersion = new FileVersion(versionMatcher.getValue()); // Get original file name and version File dir = dst.getParentFile(); File[] list = dir.listFiles(); if (list != null) { for (File f : list) { if (versionMatcher.match(f.getName())) { dst = f; dstVersion = new FileVersion(versionMatcher.getValue()); break; } } } if (dstVersion == null) { if (upgradeOnly) { return null; } } else if (fileToCopyVersion.greaterThan(dstVersion)) { // backup dst and generate rollback command File oldDst = dst; dst = new File(dst.getParentFile(), fileToCopy.getName()); File backup = IOUtils.backup(task.getPackage(), oldDst); rollbackCommand.addCommand(new Copy(backup, oldDst, null, false)); // Delete old dst as its name differs from new version oldDst.delete(); } else if (fileToCopyVersion.isSnapshot() && fileToCopyVersion.equals(dstVersion)) { doOverwrite = true; } else if (!doOverwrite) { log.info("Ignore " + fileToCopy + " because not newer than " + dstVersion + " and 'overwrite' is set to false."); return null; } } } if (dst.exists()) { // backup the destination file if exist. if (!doOverwrite && !append) { // force a rollback throw new PackageException( "Copy command has overwrite flag on false but destination file exists: " + dst); } if (task instanceof UninstallTask) { // no backup for uninstall task } else if (append) { bak = IOUtils.backup(task.getPackage(), fileToCopy); } else { bak = IOUtils.backup(task.getPackage(), dst); } } else { // target file doesn't exists - it will be created dst.getParentFile().mkdirs(); } // copy the file - use getContentToCopy to allow parameterization // for subclasses String content = getContentToCopy(fileToCopy, prefs); if (content != null) { if (append && dst.exists()) { RandomAccessFile rfile = new RandomAccessFile(dst, "r"); try { rfile.seek(dst.length()); if (!"".equals(rfile.readLine())) { content = System.getProperty("line.separator") + content; } } catch (IOException e) { log.error(e); } finally { rfile.close(); } } FileUtils.writeStringToFile(dst, content, append); } else { File tmp = new File(dst.getPath() + ".tmp"); org.nuxeo.common.utils.FileUtils.copy(fileToCopy, tmp); if (!tmp.renameTo(dst)) { tmp.delete(); org.nuxeo.common.utils.FileUtils.copy(fileToCopy, dst); } } // check whether the copied or restored file was the launcher if (dst.getName().equals(LAUNCHER_JAR) || fileToCopy.getName().equals(LAUNCHER_JAR)) { Environment env = Environment.getDefault(); env.setProperty(LAUNCHER_CHANGED_PROPERTY, "true"); } // get the md5 of the copied file. dstmd5 = IOUtils.createMd5(dst); } catch (IOException e) { throw new PackageException("Failed to copy " + fileToCopy, e); } if (bak == null) { // no file was replaced rollbackCommand.addCommand(new Delete(dst, dstmd5, removeOnExit)); } else if (append) { rollbackCommand.addCommand(new UnAppend(bak, dst)); } else { rollbackCommand.addCommand(new Copy(bak, dst, dstmd5, true)); } return rollbackCommand; } /** * Override in subclass to parameterize content. * * @since 5.5 * @param prefs * @return Content to put in destination file. See {@link #append} parameter to determine if returned content is * replacing or appending to destination file. * @throws PackageException */ protected String getContentToCopy(File fileToCopy, Map<String, String> prefs) throws PackageException { // For compliance String deprecatedContent = getContentToCopy(prefs); if (deprecatedContent != null) { return deprecatedContent; } if (append) { try { return FileUtils.readFileToString(fileToCopy); } catch (IOException e) { throw new PackageException("Couldn't read " + fileToCopy.getName(), e); } } else { return null; } } /** * @deprecated Since 5.5, use {@link #getContentToCopy(File, Map)}. This method is missing the fileToCopy reference. * Using {@link #file} is leading to errors. * @throws PackageException */ @Deprecated protected String getContentToCopy(Map<String, String> prefs) throws PackageException { return null; } @Override protected void doValidate(Task task, ValidationStatus status) throws PackageException { if (file == null || tofile == null) { status.addError("Cannot execute command in installer." + " Invalid copy syntax: file, dir, tofile or todir was not specified."); } if (tofile.isFile() && !overwrite && !append) { if (removeOnExit) { // a plugin is still there due to a previous action that needs a // restart status.addError("A restart is needed to perform this operation: cleaning " + tofile.getName()); } else { status.addError("Cannot overwrite existing file: " + tofile.getName()); } } if (md5 != null) { try { if (tofile.isFile() && !md5.equals(IOUtils.createMd5(tofile))) { status.addError("MD5 check failed. File: " + tofile + " has changed since its backup"); } } catch (IOException e) { throw new PackageException(e); } } } @Override public void readFrom(Element element) throws PackageException { boolean sourceIsDir = false; File dir = null; String v = element.getAttribute("dir"); if (v.length() > 0) { dir = new File(v); } v = element.getAttribute("file"); if (v.length() > 0) { if (dir != null) { file = new File(dir, v); } else { file = new File(v); } guardVars.put("file", file); } else { sourceIsDir = true; file = dir; guardVars.put("dir", dir); } v = element.getAttribute("todir"); if (v.length() > 0) { if (sourceIsDir) { tofile = new File(v); guardVars.put("todir", tofile); } else { tofile = new File(v, file.getName()); guardVars.put("tofile", tofile); } } else { v = element.getAttribute("tofile"); if (v.length() > 0) { FileRef ref = FileRef.newFileRef(v); tofile = ref.getFile(); guardVars.put("tofile", tofile); ref.fillPatternVariables(guardVars); } } v = element.getAttribute("md5"); if (v.length() > 0) { md5 = v; } v = element.getAttribute("overwrite"); if (v.length() > 0) { overwrite = Boolean.parseBoolean(v); } v = element.getAttribute("removeOnExit"); if (v.length() > 0) { removeOnExit = Boolean.parseBoolean(v); } v = element.getAttribute("overwriteIfNewerVersion"); if (v.length() > 0) { overwriteIfNewerVersion = Boolean.parseBoolean(v); } v = element.getAttribute("upgradeOnly"); if (v.length() > 0) { upgradeOnly = Boolean.parseBoolean(v); } v = element.getAttribute("append"); if (v.length() > 0) { append = Boolean.parseBoolean(v); } } @Override public void writeTo(XmlWriter writer) { writer.start(ID); if (file != null) { writer.attr("file", file.getAbsolutePath()); } if (tofile != null) { writer.attr("tofile", tofile.getAbsolutePath()); } writer.attr("overwrite", String.valueOf(overwrite)); if (md5 != null) { writer.attr("md5", md5); } if (removeOnExit) { writer.attr("removeOnExit", "true"); } if (overwriteIfNewerVersion) { writer.attr("overwriteIfNewerVersion", "true"); } if (upgradeOnly) { writer.attr("upgradeOnly", "true"); } if (append) { writer.attr("append", "true"); } writer.end(); } }