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