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.standalone.commands; 022 023import static java.nio.charset.StandardCharsets.UTF_8; 024 025import java.io.File; 026import java.io.IOException; 027import java.io.RandomAccessFile; 028import java.util.Map; 029 030import org.apache.commons.io.FileUtils; 031import org.apache.commons.logging.Log; 032import org.apache.commons.logging.LogFactory; 033import org.nuxeo.common.Environment; 034import org.nuxeo.common.utils.FileMatcher; 035import org.nuxeo.common.utils.FileRef; 036import org.nuxeo.common.utils.FileVersion; 037import org.nuxeo.connect.update.PackageException; 038import org.nuxeo.connect.update.ValidationStatus; 039import org.nuxeo.connect.update.task.Command; 040import org.nuxeo.connect.update.task.Task; 041import org.nuxeo.connect.update.task.standalone.UninstallTask; 042import org.nuxeo.connect.update.util.IOUtils; 043import org.nuxeo.connect.update.xml.XmlWriter; 044import org.w3c.dom.Element; 045 046/** 047 * Copy a file to the given target directory or file. If the target is a directory the file name is preserved. If the 048 * target file exists it will be replaced if overwrite is true otherwise the command validation fails. If the source 049 * file is a directory, then the files it contents will be recursively copied. 050 * <p> 051 * If md5 is set then the copy command will be validated only if the target file has the same md5 as the one specified 052 * in the command. 053 * <p> 054 * The Copy command has as inverse either Delete either another Copy command. If the file was copied without overwriting 055 * then Delete is the inverse (with a md5 set to the one of the copied file). If the file was overwritten then the 056 * inverse of Copy command is another copy command with the md5 to the one of the copied file and the overwrite flag to 057 * true. The file to copy will be the backup of the overwritten file. 058 * 059 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 060 */ 061public class Copy extends AbstractCommand { 062 063 protected static final Log log = LogFactory.getLog(Copy.class); 064 065 public static final String ID = "copy"; 066 067 protected static final String LAUNCHER_JAR = "nuxeo-launcher.jar"; 068 069 protected static final String LAUNCHER_CHANGED_PROPERTY = "launcher.changed"; 070 071 /** 072 * The source file. It can be a file or a directory. 073 */ 074 protected File file; 075 076 /** 077 * The target file. It can be a directory since 5.5 078 */ 079 protected File tofile; 080 081 protected boolean overwrite; 082 083 protected String md5; 084 085 protected boolean removeOnExit; 086 087 /** 088 * @since 5.5 089 */ 090 protected boolean append; 091 092 /** 093 * @since 5.5 094 */ 095 private boolean overwriteIfNewerVersion; 096 097 /** 098 * @since 5.5 099 */ 100 private boolean upgradeOnly; 101 102 protected Copy(String id) { 103 super(id); 104 } 105 106 public Copy() { 107 this(ID); 108 } 109 110 public Copy(File file, File tofile, String md5, boolean overwrite) { 111 this(ID, file, tofile, md5, overwrite, false); 112 } 113 114 public Copy(File file, File tofile, String md5, boolean overwrite, boolean removeOnExit) { 115 this(ID, file, tofile, md5, overwrite, removeOnExit); 116 } 117 118 protected Copy(String id, File file, File tofile, String md5, boolean overwrite, boolean removeOnExit) { 119 this(id); 120 this.file = file; 121 this.tofile = tofile; 122 this.md5 = md5; 123 this.overwrite = overwrite; 124 this.removeOnExit = removeOnExit; 125 } 126 127 @Override 128 protected Command doRun(Task task, Map<String, String> prefs) throws PackageException { 129 if (!file.exists()) { 130 log.warn("Can't copy " + file + " . File missing."); 131 return null; 132 } 133 return doCopy(task, prefs, file, tofile, overwrite); 134 } 135 136 /** 137 * @since 5.5 138 */ 139 protected Command doCopy(Task task, Map<String, String> prefs, File fileToCopy, File dst, boolean doOverwrite) 140 throws PackageException { 141 String dstmd5; 142 File bak = null; 143 CompositeCommand rollbackCommand = new CompositeCommand(); 144 if (fileToCopy.isDirectory()) { 145 if (fileToCopy != file) { 146 dst = new File(dst, fileToCopy.getName()); 147 } 148 dst.mkdirs(); 149 for (File childFile : fileToCopy.listFiles()) { 150 rollbackCommand.addCommand(doCopy(task, prefs, childFile, dst, doOverwrite)); 151 } 152 return rollbackCommand; 153 } 154 if (dst.isDirectory()) { 155 dst = new File(dst, fileToCopy.getName()); 156 } 157 try { 158 FileMatcher filenameMatcher = FileMatcher.getMatcher("{n:.*-}[0-9]+.*\\.jar"); 159 boolean isVersionnedJarFile = filenameMatcher.match(fileToCopy.getName()); 160 if (isVersionnedJarFile) { 161 log.warn(String.format( 162 "Use of the <copy /> command on JAR files is not recommended, prefer using <update /> command to ensure a safe rollback. (%s)", 163 fileToCopy.getName())); 164 } 165 if (isVersionnedJarFile && (overwriteIfNewerVersion || upgradeOnly)) { 166 // Compare source and destination versions set in filename 167 FileVersion fileToCopyVersion, dstVersion = null; 168 String filenameWithoutVersion = filenameMatcher.getValue(); 169 FileMatcher versionMatcher = FileMatcher.getMatcher(filenameWithoutVersion + "{v:[0-9]+.*}\\.jar"); 170 // Get new file version 171 if (versionMatcher.match(fileToCopy.getName())) { 172 fileToCopyVersion = new FileVersion(versionMatcher.getValue()); 173 // Get original file name and version 174 File dir = dst.getParentFile(); 175 File[] list = dir.listFiles(); 176 if (list != null) { 177 for (File f : list) { 178 if (versionMatcher.match(f.getName())) { 179 dst = f; 180 dstVersion = new FileVersion(versionMatcher.getValue()); 181 break; 182 } 183 } 184 } 185 if (dstVersion == null) { 186 if (upgradeOnly) { 187 return null; 188 } 189 } else if (fileToCopyVersion.greaterThan(dstVersion)) { 190 // backup dst and generate rollback command 191 File oldDst = dst; 192 dst = new File(dst.getParentFile(), fileToCopy.getName()); 193 File backup = IOUtils.backup(task.getPackage(), oldDst); 194 rollbackCommand.addCommand(new Copy(backup, oldDst, null, false)); 195 // Delete old dst as its name differs from new version 196 oldDst.delete(); 197 } else if (fileToCopyVersion.isSnapshot() && fileToCopyVersion.equals(dstVersion)) { 198 doOverwrite = true; 199 } else if (!doOverwrite) { 200 log.info("Ignore " + fileToCopy + " because not newer than " + dstVersion 201 + " and 'overwrite' is set to false."); 202 return null; 203 } 204 } 205 } 206 if (dst.exists()) { // backup the destination file if exist. 207 if (!doOverwrite && !append) { // force a rollback 208 throw new PackageException( 209 "Copy command has overwrite flag on false but destination file exists: " + dst); 210 } 211 if (task instanceof UninstallTask) { 212 // no backup for uninstall task 213 } else if (append) { 214 bak = IOUtils.backup(task.getPackage(), fileToCopy); 215 } else { 216 bak = IOUtils.backup(task.getPackage(), dst); 217 } 218 } else { // target file doesn't exists - it will be created 219 dst.getParentFile().mkdirs(); 220 } 221 222 // copy the file - use getContentToCopy to allow parameterization 223 // for subclasses 224 String content = getContentToCopy(fileToCopy, prefs); 225 if (content != null) { 226 if (append && dst.exists()) { 227 try (RandomAccessFile rfile = new RandomAccessFile(dst, "r")) { 228 rfile.seek(dst.length()); 229 if (!"".equals(rfile.readLine())) { 230 content = System.getProperty("line.separator") + content; 231 } 232 } catch (IOException e) { 233 log.error(e); 234 } 235 } 236 FileUtils.writeStringToFile(dst, content, UTF_8, append); 237 } else { 238 File tmp = new File(dst.getPath() + ".tmp"); 239 org.nuxeo.common.utils.FileUtils.copy(fileToCopy, tmp); 240 if (!tmp.renameTo(dst)) { 241 tmp.delete(); 242 org.nuxeo.common.utils.FileUtils.copy(fileToCopy, dst); 243 } 244 } 245 // check whether the copied or restored file was the launcher 246 if (dst.getName().equals(LAUNCHER_JAR) || fileToCopy.getName().equals(LAUNCHER_JAR)) { 247 Environment env = Environment.getDefault(); 248 env.setProperty(LAUNCHER_CHANGED_PROPERTY, "true"); 249 } 250 // get the md5 of the copied file. 251 dstmd5 = IOUtils.createMd5(dst); 252 } catch (IOException e) { 253 throw new PackageException("Failed to copy " + fileToCopy, e); 254 } 255 if (bak == null) { // no file was replaced 256 rollbackCommand.addCommand(new Delete(dst, dstmd5, removeOnExit)); 257 } else if (append) { 258 rollbackCommand.addCommand(new UnAppend(bak, dst)); 259 } else { 260 rollbackCommand.addCommand(new Copy(bak, dst, dstmd5, true)); 261 } 262 return rollbackCommand; 263 } 264 265 /** 266 * Override in subclass to parameterize content. 267 * 268 * @since 5.5 269 * @return Content to put in destination file. See {@link #append} parameter to determine if returned content is 270 * replacing or appending to destination file. 271 */ 272 protected String getContentToCopy(File fileToCopy, Map<String, String> prefs) throws PackageException { 273 // For compliance 274 String deprecatedContent = getContentToCopy(prefs); 275 if (deprecatedContent != null) { 276 return deprecatedContent; 277 } 278 if (append) { 279 try { 280 return FileUtils.readFileToString(fileToCopy, UTF_8); 281 } catch (IOException e) { 282 throw new PackageException("Couldn't read " + fileToCopy.getName(), e); 283 } 284 } else { 285 return null; 286 } 287 } 288 289 /** 290 * @deprecated Since 5.5, use {@link #getContentToCopy(File, Map)}. This method is missing the fileToCopy reference. 291 * Using {@link #file} is leading to errors. 292 */ 293 @Deprecated 294 protected String getContentToCopy(Map<String, String> prefs) throws PackageException { 295 return null; 296 } 297 298 @Override 299 protected void doValidate(Task task, ValidationStatus status) throws PackageException { 300 if (file == null || tofile == null) { 301 status.addError("Cannot execute command in installer." 302 + " Invalid copy syntax: file, dir, tofile or todir was not specified."); 303 return; 304 } 305 if (tofile.isFile() && !overwrite && !append) { 306 if (removeOnExit) { 307 // a plugin is still there due to a previous action that needs a 308 // restart 309 status.addError("A restart is needed to perform this operation: cleaning " + tofile.getName()); 310 } else { 311 status.addError("Cannot overwrite existing file: " + tofile.getName()); 312 } 313 } 314 if (md5 != null) { 315 try { 316 if (tofile.isFile() && !md5.equals(IOUtils.createMd5(tofile))) { 317 status.addError("MD5 check failed. File: " + tofile + " has changed since its backup"); 318 } 319 } catch (IOException e) { 320 throw new PackageException(e); 321 } 322 } 323 } 324 325 @Override 326 public void readFrom(Element element) throws PackageException { 327 boolean sourceIsDir = false; 328 File dir = null; 329 String v = element.getAttribute("dir"); 330 if (v.length() > 0) { 331 dir = new File(v); 332 } 333 v = element.getAttribute("file"); 334 if (v.length() > 0) { 335 if (dir != null) { 336 file = new File(dir, v); 337 } else { 338 file = new File(v); 339 } 340 guardVars.put("file", file); 341 } else { 342 sourceIsDir = true; 343 file = dir; 344 guardVars.put("dir", dir); 345 } 346 347 v = element.getAttribute("todir"); 348 if (v.length() > 0) { 349 if (sourceIsDir) { 350 tofile = new File(v); 351 guardVars.put("todir", tofile); 352 } else { 353 tofile = new File(v, file.getName()); 354 guardVars.put("tofile", tofile); 355 } 356 } else { 357 v = element.getAttribute("tofile"); 358 if (v.length() > 0) { 359 FileRef ref = FileRef.newFileRef(v); 360 tofile = ref.getFile(); 361 guardVars.put("tofile", tofile); 362 ref.fillPatternVariables(guardVars); 363 } 364 } 365 366 v = element.getAttribute("md5"); 367 if (v.length() > 0) { 368 md5 = v; 369 } 370 v = element.getAttribute("overwrite"); 371 if (v.length() > 0) { 372 overwrite = Boolean.parseBoolean(v); 373 } 374 v = element.getAttribute("removeOnExit"); 375 if (v.length() > 0) { 376 removeOnExit = Boolean.parseBoolean(v); 377 } 378 v = element.getAttribute("overwriteIfNewerVersion"); 379 if (v.length() > 0) { 380 overwriteIfNewerVersion = Boolean.parseBoolean(v); 381 } 382 v = element.getAttribute("upgradeOnly"); 383 if (v.length() > 0) { 384 upgradeOnly = Boolean.parseBoolean(v); 385 } 386 v = element.getAttribute("append"); 387 if (v.length() > 0) { 388 append = Boolean.parseBoolean(v); 389 } 390 } 391 392 @Override 393 public void writeTo(XmlWriter writer) { 394 writer.start(ID); 395 if (file != null) { 396 writer.attr("file", file.getAbsolutePath()); 397 } 398 if (tofile != null) { 399 writer.attr("tofile", tofile.getAbsolutePath()); 400 } 401 writer.attr("overwrite", String.valueOf(overwrite)); 402 if (md5 != null) { 403 writer.attr("md5", md5); 404 } 405 if (removeOnExit) { 406 writer.attr("removeOnExit", "true"); 407 } 408 if (overwriteIfNewerVersion) { 409 writer.attr("overwriteIfNewerVersion", "true"); 410 } 411 if (upgradeOnly) { 412 writer.attr("upgradeOnly", "true"); 413 } 414 if (append) { 415 writer.attr("append", "true"); 416 } 417 writer.end(); 418 } 419 420}