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}