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:[email protected]">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 * @param doOverwrite 138 * @since 5.5 139 */ 140 protected Command doCopy(Task task, Map<String, String> prefs, File fileToCopy, File dst, boolean doOverwrite) 141 throws PackageException { 142 String dstmd5; 143 File bak = null; 144 CompositeCommand rollbackCommand = new CompositeCommand(); 145 if (fileToCopy.isDirectory()) { 146 if (fileToCopy != file) { 147 dst = new File(dst, fileToCopy.getName()); 148 } 149 dst.mkdirs(); 150 for (File childFile : fileToCopy.listFiles()) { 151 rollbackCommand.addCommand(doCopy(task, prefs, childFile, dst, doOverwrite)); 152 } 153 return rollbackCommand; 154 } 155 if (dst.isDirectory()) { 156 dst = new File(dst, fileToCopy.getName()); 157 } 158 try { 159 FileMatcher filenameMatcher = FileMatcher.getMatcher("{n:.*-}[0-9]+.*\\.jar"); 160 boolean isVersionnedJarFile = filenameMatcher.match(fileToCopy.getName()); 161 if (isVersionnedJarFile) { 162 log.warn(String.format( 163 "Use of the <copy /> command on JAR files is not recommended, prefer using <update /> command to ensure a safe rollback. (%s)", 164 fileToCopy.getName())); 165 } 166 if (isVersionnedJarFile && (overwriteIfNewerVersion || upgradeOnly)) { 167 // Compare source and destination versions set in filename 168 FileVersion fileToCopyVersion, dstVersion = null; 169 String filenameWithoutVersion = filenameMatcher.getValue(); 170 FileMatcher versionMatcher = FileMatcher.getMatcher(filenameWithoutVersion + "{v:[0-9]+.*}\\.jar"); 171 // Get new file version 172 if (versionMatcher.match(fileToCopy.getName())) { 173 fileToCopyVersion = new FileVersion(versionMatcher.getValue()); 174 // Get original file name and version 175 File dir = dst.getParentFile(); 176 File[] list = dir.listFiles(); 177 if (list != null) { 178 for (File f : list) { 179 if (versionMatcher.match(f.getName())) { 180 dst = f; 181 dstVersion = new FileVersion(versionMatcher.getValue()); 182 break; 183 } 184 } 185 } 186 if (dstVersion == null) { 187 if (upgradeOnly) { 188 return null; 189 } 190 } else if (fileToCopyVersion.greaterThan(dstVersion)) { 191 // backup dst and generate rollback command 192 File oldDst = dst; 193 dst = new File(dst.getParentFile(), fileToCopy.getName()); 194 File backup = IOUtils.backup(task.getPackage(), oldDst); 195 rollbackCommand.addCommand(new Copy(backup, oldDst, null, false)); 196 // Delete old dst as its name differs from new version 197 oldDst.delete(); 198 } else if (fileToCopyVersion.isSnapshot() && fileToCopyVersion.equals(dstVersion)) { 199 doOverwrite = true; 200 } else if (!doOverwrite) { 201 log.info("Ignore " + fileToCopy + " because not newer than " + dstVersion 202 + " and 'overwrite' is set to false."); 203 return null; 204 } 205 } 206 } 207 if (dst.exists()) { // backup the destination file if exist. 208 if (!doOverwrite && !append) { // force a rollback 209 throw new PackageException( 210 "Copy command has overwrite flag on false but destination file exists: " + dst); 211 } 212 if (task instanceof UninstallTask) { 213 // no backup for uninstall task 214 } else if (append) { 215 bak = IOUtils.backup(task.getPackage(), fileToCopy); 216 } else { 217 bak = IOUtils.backup(task.getPackage(), dst); 218 } 219 } else { // target file doesn't exists - it will be created 220 dst.getParentFile().mkdirs(); 221 } 222 223 // copy the file - use getContentToCopy to allow parameterization 224 // for subclasses 225 String content = getContentToCopy(fileToCopy, prefs); 226 if (content != null) { 227 if (append && dst.exists()) { 228 try (RandomAccessFile rfile = new RandomAccessFile(dst, "r")) { 229 rfile.seek(dst.length()); 230 if (!"".equals(rfile.readLine())) { 231 content = System.getProperty("line.separator") + content; 232 } 233 } catch (IOException e) { 234 log.error(e); 235 } 236 } 237 FileUtils.writeStringToFile(dst, content, UTF_8, append); 238 } else { 239 File tmp = new File(dst.getPath() + ".tmp"); 240 org.nuxeo.common.utils.FileUtils.copy(fileToCopy, tmp); 241 if (!tmp.renameTo(dst)) { 242 tmp.delete(); 243 org.nuxeo.common.utils.FileUtils.copy(fileToCopy, dst); 244 } 245 } 246 // check whether the copied or restored file was the launcher 247 if (dst.getName().equals(LAUNCHER_JAR) || fileToCopy.getName().equals(LAUNCHER_JAR)) { 248 Environment env = Environment.getDefault(); 249 env.setProperty(LAUNCHER_CHANGED_PROPERTY, "true"); 250 } 251 // get the md5 of the copied file. 252 dstmd5 = IOUtils.createMd5(dst); 253 } catch (IOException e) { 254 throw new PackageException("Failed to copy " + fileToCopy, e); 255 } 256 if (bak == null) { // no file was replaced 257 rollbackCommand.addCommand(new Delete(dst, dstmd5, removeOnExit)); 258 } else if (append) { 259 rollbackCommand.addCommand(new UnAppend(bak, dst)); 260 } else { 261 rollbackCommand.addCommand(new Copy(bak, dst, dstmd5, true)); 262 } 263 return rollbackCommand; 264 } 265 266 /** 267 * Override in subclass to parameterize content. 268 * 269 * @since 5.5 270 * @param prefs 271 * @return Content to put in destination file. See {@link #append} parameter to determine if returned content is 272 * replacing or appending to destination file. 273 * @throws PackageException 274 */ 275 protected String getContentToCopy(File fileToCopy, Map<String, String> prefs) throws PackageException { 276 // For compliance 277 String deprecatedContent = getContentToCopy(prefs); 278 if (deprecatedContent != null) { 279 return deprecatedContent; 280 } 281 if (append) { 282 try { 283 return FileUtils.readFileToString(fileToCopy, UTF_8); 284 } catch (IOException e) { 285 throw new PackageException("Couldn't read " + fileToCopy.getName(), e); 286 } 287 } else { 288 return null; 289 } 290 } 291 292 /** 293 * @deprecated Since 5.5, use {@link #getContentToCopy(File, Map)}. This method is missing the fileToCopy reference. 294 * Using {@link #file} is leading to errors. 295 * @throws PackageException 296 */ 297 @Deprecated 298 protected String getContentToCopy(Map<String, String> prefs) throws PackageException { 299 return null; 300 } 301 302 @Override 303 protected void doValidate(Task task, ValidationStatus status) throws PackageException { 304 if (file == null || tofile == null) { 305 status.addError("Cannot execute command in installer." 306 + " Invalid copy syntax: file, dir, tofile or todir was not specified."); 307 return; 308 } 309 if (tofile.isFile() && !overwrite && !append) { 310 if (removeOnExit) { 311 // a plugin is still there due to a previous action that needs a 312 // restart 313 status.addError("A restart is needed to perform this operation: cleaning " + tofile.getName()); 314 } else { 315 status.addError("Cannot overwrite existing file: " + tofile.getName()); 316 } 317 } 318 if (md5 != null) { 319 try { 320 if (tofile.isFile() && !md5.equals(IOUtils.createMd5(tofile))) { 321 status.addError("MD5 check failed. File: " + tofile + " has changed since its backup"); 322 } 323 } catch (IOException e) { 324 throw new PackageException(e); 325 } 326 } 327 } 328 329 @Override 330 public void readFrom(Element element) throws PackageException { 331 boolean sourceIsDir = false; 332 File dir = null; 333 String v = element.getAttribute("dir"); 334 if (v.length() > 0) { 335 dir = new File(v); 336 } 337 v = element.getAttribute("file"); 338 if (v.length() > 0) { 339 if (dir != null) { 340 file = new File(dir, v); 341 } else { 342 file = new File(v); 343 } 344 guardVars.put("file", file); 345 } else { 346 sourceIsDir = true; 347 file = dir; 348 guardVars.put("dir", dir); 349 } 350 351 v = element.getAttribute("todir"); 352 if (v.length() > 0) { 353 if (sourceIsDir) { 354 tofile = new File(v); 355 guardVars.put("todir", tofile); 356 } else { 357 tofile = new File(v, file.getName()); 358 guardVars.put("tofile", tofile); 359 } 360 } else { 361 v = element.getAttribute("tofile"); 362 if (v.length() > 0) { 363 FileRef ref = FileRef.newFileRef(v); 364 tofile = ref.getFile(); 365 guardVars.put("tofile", tofile); 366 ref.fillPatternVariables(guardVars); 367 } 368 } 369 370 v = element.getAttribute("md5"); 371 if (v.length() > 0) { 372 md5 = v; 373 } 374 v = element.getAttribute("overwrite"); 375 if (v.length() > 0) { 376 overwrite = Boolean.parseBoolean(v); 377 } 378 v = element.getAttribute("removeOnExit"); 379 if (v.length() > 0) { 380 removeOnExit = Boolean.parseBoolean(v); 381 } 382 v = element.getAttribute("overwriteIfNewerVersion"); 383 if (v.length() > 0) { 384 overwriteIfNewerVersion = Boolean.parseBoolean(v); 385 } 386 v = element.getAttribute("upgradeOnly"); 387 if (v.length() > 0) { 388 upgradeOnly = Boolean.parseBoolean(v); 389 } 390 v = element.getAttribute("append"); 391 if (v.length() > 0) { 392 append = Boolean.parseBoolean(v); 393 } 394 } 395 396 @Override 397 public void writeTo(XmlWriter writer) { 398 writer.start(ID); 399 if (file != null) { 400 writer.attr("file", file.getAbsolutePath()); 401 } 402 if (tofile != null) { 403 writer.attr("tofile", tofile.getAbsolutePath()); 404 } 405 writer.attr("overwrite", String.valueOf(overwrite)); 406 if (md5 != null) { 407 writer.attr("md5", md5); 408 } 409 if (removeOnExit) { 410 writer.attr("removeOnExit", "true"); 411 } 412 if (overwriteIfNewerVersion) { 413 writer.attr("overwriteIfNewerVersion", "true"); 414 } 415 if (upgradeOnly) { 416 writer.attr("upgradeOnly", "true"); 417 } 418 if (append) { 419 writer.attr("append", "true"); 420 } 421 writer.end(); 422 } 423 424}