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