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}