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 java.io.File; 024import java.io.IOException; 025import java.io.RandomAccessFile; 026import java.util.Map; 027 028import org.apache.commons.io.FileUtils; 029import org.apache.commons.logging.Log; 030import org.apache.commons.logging.LogFactory; 031import org.nuxeo.common.Environment; 032import org.nuxeo.common.utils.FileMatcher; 033import org.nuxeo.common.utils.FileRef; 034import org.nuxeo.common.utils.FileVersion; 035import org.nuxeo.connect.update.PackageException; 036import org.nuxeo.connect.update.ValidationStatus; 037import org.nuxeo.connect.update.task.Command; 038import org.nuxeo.connect.update.task.Task; 039import org.nuxeo.connect.update.task.standalone.UninstallTask; 040import org.nuxeo.connect.update.util.IOUtils; 041import org.nuxeo.connect.update.xml.XmlWriter; 042import org.w3c.dom.Element; 043 044/** 045 * Copy a file to the given target directory or file. If the target is a directory the file name is preserved. If the 046 * target file exists it will be replaced if overwrite is true otherwise the command validation fails. If the source 047 * file is a directory, then the files it contents will be recursively copied. 048 * <p> 049 * If md5 is set then the copy command will be validated only if the target file has the same md5 as the one specified 050 * in the command. 051 * <p> 052 * The Copy command has as inverse either Delete either another Copy command. If the file was copied without overwriting 053 * then Delete is the inverse (with a md5 set to the one of the copied file). If the file was overwritten then the 054 * inverse of Copy command is another copy command with the md5 to the one of the copied file and the overwrite flag to 055 * true. The file to copy will be the backup of the overwritten file. 056 * 057 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 058 */ 059public class Copy extends AbstractCommand { 060 061 protected static final Log log = LogFactory.getLog(Copy.class); 062 063 public static final String ID = "copy"; 064 065 protected static final String LAUNCHER_JAR = "nuxeo-launcher.jar"; 066 067 protected static final String LAUNCHER_CHANGED_PROPERTY = "launcher.changed"; 068 069 /** 070 * The source file. It can be a file or a directory. 071 */ 072 protected File file; 073 074 /** 075 * The target file. It can be a directory since 5.5 076 */ 077 protected File tofile; 078 079 protected boolean overwrite; 080 081 protected String md5; 082 083 protected boolean removeOnExit; 084 085 /** 086 * @since 5.5 087 */ 088 protected boolean append; 089 090 /** 091 * @since 5.5 092 */ 093 private boolean overwriteIfNewerVersion; 094 095 /** 096 * @since 5.5 097 */ 098 private boolean upgradeOnly; 099 100 protected Copy(String id) { 101 super(id); 102 } 103 104 public Copy() { 105 this(ID); 106 } 107 108 public Copy(File file, File tofile, String md5, boolean overwrite) { 109 this(ID, file, tofile, md5, overwrite, false); 110 } 111 112 public Copy(File file, File tofile, String md5, boolean overwrite, boolean removeOnExit) { 113 this(ID, file, tofile, md5, overwrite, removeOnExit); 114 } 115 116 protected Copy(String id, File file, File tofile, String md5, boolean overwrite, boolean removeOnExit) { 117 this(id); 118 this.file = file; 119 this.tofile = tofile; 120 this.md5 = md5; 121 this.overwrite = overwrite; 122 this.removeOnExit = removeOnExit; 123 } 124 125 @Override 126 protected Command doRun(Task task, Map<String, String> prefs) throws PackageException { 127 if (!file.exists()) { 128 log.warn("Can't copy " + file + " . File missing."); 129 return null; 130 } 131 return doCopy(task, prefs, file, tofile, overwrite); 132 } 133 134 /** 135 * @param doOverwrite 136 * @since 5.5 137 */ 138 protected Command doCopy(Task task, Map<String, String> prefs, File fileToCopy, File dst, boolean doOverwrite) 139 throws PackageException { 140 String dstmd5; 141 File bak = null; 142 CompositeCommand rollbackCommand = new CompositeCommand(); 143 if (fileToCopy.isDirectory()) { 144 if (fileToCopy != file) { 145 dst = new File(dst, fileToCopy.getName()); 146 } 147 dst.mkdirs(); 148 for (File childFile : fileToCopy.listFiles()) { 149 rollbackCommand.addCommand(doCopy(task, prefs, childFile, dst, doOverwrite)); 150 } 151 return rollbackCommand; 152 } 153 if (dst.isDirectory()) { 154 dst = new File(dst, fileToCopy.getName()); 155 } 156 try { 157 FileMatcher filenameMatcher = FileMatcher.getMatcher("{n:.*-}[0-9]+.*\\.jar"); 158 boolean isVersionnedJarFile = filenameMatcher.match(fileToCopy.getName()); 159 if (isVersionnedJarFile) { 160 log.warn(String.format( 161 "Use of the <copy /> command on JAR files is not recommended, prefer using <update /> command to ensure a safe rollback. (%s)", 162 fileToCopy.getName())); 163 } 164 if (isVersionnedJarFile && (overwriteIfNewerVersion || upgradeOnly)) { 165 // Compare source and destination versions set in filename 166 FileVersion fileToCopyVersion, dstVersion = null; 167 String filenameWithoutVersion = filenameMatcher.getValue(); 168 FileMatcher versionMatcher = FileMatcher.getMatcher(filenameWithoutVersion + "{v:[0-9]+.*}\\.jar"); 169 // Get new file version 170 if (versionMatcher.match(fileToCopy.getName())) { 171 fileToCopyVersion = new FileVersion(versionMatcher.getValue()); 172 // Get original file name and version 173 File dir = dst.getParentFile(); 174 File[] list = dir.listFiles(); 175 if (list != null) { 176 for (File f : list) { 177 if (versionMatcher.match(f.getName())) { 178 dst = f; 179 dstVersion = new FileVersion(versionMatcher.getValue()); 180 break; 181 } 182 } 183 } 184 if (dstVersion == null) { 185 if (upgradeOnly) { 186 return null; 187 } 188 } else if (fileToCopyVersion.greaterThan(dstVersion)) { 189 // backup dst and generate rollback command 190 File oldDst = dst; 191 dst = new File(dst.getParentFile(), fileToCopy.getName()); 192 File backup = IOUtils.backup(task.getPackage(), oldDst); 193 rollbackCommand.addCommand(new Copy(backup, oldDst, null, false)); 194 // Delete old dst as its name differs from new version 195 oldDst.delete(); 196 } else if (fileToCopyVersion.isSnapshot() && fileToCopyVersion.equals(dstVersion)) { 197 doOverwrite = true; 198 } else if (!doOverwrite) { 199 log.info("Ignore " + fileToCopy + " because not newer than " + dstVersion 200 + " and 'overwrite' is set to false."); 201 return null; 202 } 203 } 204 } 205 if (dst.exists()) { // backup the destination file if exist. 206 if (!doOverwrite && !append) { // force a rollback 207 throw new PackageException( 208 "Copy command has overwrite flag on false but destination file exists: " + dst); 209 } 210 if (task instanceof UninstallTask) { 211 // no backup for uninstall task 212 } else if (append) { 213 bak = IOUtils.backup(task.getPackage(), fileToCopy); 214 } else { 215 bak = IOUtils.backup(task.getPackage(), dst); 216 } 217 } else { // target file doesn't exists - it will be created 218 dst.getParentFile().mkdirs(); 219 } 220 221 // copy the file - use getContentToCopy to allow parameterization 222 // for subclasses 223 String content = getContentToCopy(fileToCopy, prefs); 224 if (content != null) { 225 if (append && dst.exists()) { 226 RandomAccessFile rfile = new RandomAccessFile(dst, "r"); 227 try { 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 } finally { 235 rfile.close(); 236 } 237 } 238 FileUtils.writeStringToFile(dst, content, append); 239 } else { 240 File tmp = new File(dst.getPath() + ".tmp"); 241 org.nuxeo.common.utils.FileUtils.copy(fileToCopy, tmp); 242 if (!tmp.renameTo(dst)) { 243 tmp.delete(); 244 org.nuxeo.common.utils.FileUtils.copy(fileToCopy, dst); 245 } 246 } 247 // check whether the copied or restored file was the launcher 248 if (dst.getName().equals(LAUNCHER_JAR) || fileToCopy.getName().equals(LAUNCHER_JAR)) { 249 Environment env = Environment.getDefault(); 250 env.setProperty(LAUNCHER_CHANGED_PROPERTY, "true"); 251 } 252 // get the md5 of the copied file. 253 dstmd5 = IOUtils.createMd5(dst); 254 } catch (IOException e) { 255 throw new PackageException("Failed to copy " + fileToCopy, e); 256 } 257 if (bak == null) { // no file was replaced 258 rollbackCommand.addCommand(new Delete(dst, dstmd5, removeOnExit)); 259 } else if (append) { 260 rollbackCommand.addCommand(new UnAppend(bak, dst)); 261 } else { 262 rollbackCommand.addCommand(new Copy(bak, dst, dstmd5, true)); 263 } 264 return rollbackCommand; 265 } 266 267 /** 268 * Override in subclass to parameterize content. 269 * 270 * @since 5.5 271 * @param prefs 272 * @return Content to put in destination file. See {@link #append} parameter to determine if returned content is 273 * replacing or appending to destination file. 274 * @throws PackageException 275 */ 276 protected String getContentToCopy(File fileToCopy, Map<String, String> prefs) throws PackageException { 277 // For compliance 278 String deprecatedContent = getContentToCopy(prefs); 279 if (deprecatedContent != null) { 280 return deprecatedContent; 281 } 282 if (append) { 283 try { 284 return FileUtils.readFileToString(fileToCopy); 285 } catch (IOException e) { 286 throw new PackageException("Couldn't read " + fileToCopy.getName(), e); 287 } 288 } else { 289 return null; 290 } 291 } 292 293 /** 294 * @deprecated Since 5.5, use {@link #getContentToCopy(File, Map)}. This method is missing the fileToCopy reference. 295 * Using {@link #file} is leading to errors. 296 * @throws PackageException 297 */ 298 @Deprecated 299 protected String getContentToCopy(Map<String, String> prefs) throws PackageException { 300 return null; 301 } 302 303 @Override 304 protected void doValidate(Task task, ValidationStatus status) throws PackageException { 305 if (file == null || tofile == null) { 306 status.addError("Cannot execute command in installer." 307 + " Invalid copy syntax: file, dir, tofile or todir was not specified."); 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}