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.logging.Log;
029import org.apache.commons.logging.LogFactory;
030import org.nuxeo.common.Environment;
031import org.nuxeo.common.utils.FileMatcher;
032import org.nuxeo.common.utils.FileRef;
033import org.nuxeo.common.utils.FileUtils;
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.writeFile(dst, content, append);
239            } else {
240                File tmp = new File(dst.getPath() + ".tmp");
241                FileUtils.copy(fileToCopy, tmp);
242                if (!tmp.renameTo(dst)) {
243                    tmp.delete();
244                    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.readFile(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}