001/*
002 * Copyright (c) 2004-2010 The Regents of the University of California.
003 * All rights reserved.
004 *
005 * '$Author: welker $'
006 * '$Date: 2010-05-06 05:21:26 +0000 (Thu, 06 May 2010) $' 
007 * '$Revision: 24234 $'
008 * 
009 * Permission is hereby granted, without written agreement and without
010 * license or royalty fees, to use, copy, modify, and distribute this
011 * software and its documentation for any purpose, provided that the above
012 * copyright notice and the following two paragraphs appear in all copies
013 * of this software.
014 *
015 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
016 * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
017 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
018 * THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
019 * SUCH DAMAGE.
020 *
021 * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
022 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
023 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
024 * PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
025 * CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
026 * ENHANCEMENTS, OR MODIFICATIONS.
027 *
028 */
029
030package org.kepler.ssh;
031
032import java.io.File;
033import java.io.FilenameFilter;
034import java.io.IOException;
035import java.util.Vector;
036import java.util.regex.Matcher;
037import java.util.regex.Pattern;
038
039import org.apache.commons.logging.Log;
040import org.apache.commons.logging.LogFactory;
041
042/**
043 * Local file delete according to a regular expression. This class can delete
044 * files and directories recursively. E.g /home/dummy/dir?/sub*d??/f* Relative
045 * pathes are handled as relative to the current dir.
046 */
047public class LocalDelete {
048        public LocalDelete() {
049        }
050
051        public boolean deleteFiles(String mask, boolean recursive)
052                        throws ExecException {
053
054                if (mask == null)
055                        return true;
056                if (mask.trim() == "")
057                        return true;
058
059                // pre-test to conform to 'rm -rf': if the mask ends for . or ..
060                // it must throw an error
061                String name = new File(mask).getName();
062                if (name.equals(".") || name.equals("..")) {
063                        throw new ExecException(
064                                        "Directories like . or ..  are not allowed to be removed: "
065                                                        + mask);
066                }
067
068                // split the mask into a vector of single masks
069                // e.g. a/b*d/c?? into (a, b*d, c??)
070                Vector splittedMask = splitMask(mask);
071                // sure it has at least one element: .
072                String path = (String) splittedMask.firstElement();
073                splittedMask.remove(0);
074
075                return delete(new File(path), splittedMask, recursive);
076        }
077
078        /********************/
079        /* Private methods */
080        /********************/
081
082        private static final Log log = LogFactory.getLog(LocalDelete.class
083                        .getName());
084        private static final boolean isDebugging = log.isDebugEnabled();
085
086        /**
087         * Return the first position of * or ? in the string. returns -1 if none
088         * found or the string is null.
089         */
090        private static int wildcardPos(String mask) {
091                if (mask == null)
092                        return -1;
093                int firstStarIdx = mask.indexOf("*");
094                int firstQmIdx = mask.indexOf("?");
095                if (firstStarIdx == -1)
096                        return firstQmIdx;
097                if (firstQmIdx == -1)
098                        return firstStarIdx;
099                return Math.min(firstStarIdx, firstQmIdx);
100        }
101
102        private static boolean wildcarded(String mask) {
103                return (wildcardPos(mask) > -1);
104        }
105
106        /**
107         * Split the mask on the file separators. Instead of the String.split()
108         * method, we use the File.getParentFile() method to split the mask string.
109         * This works on Windows, where the separator can be both / and \\. The
110         * first element of the vector will be the wildcardless head of the mask. If
111         * there is no leading wildcardless element, the first part (of the path)
112         * will be either . or / according to the mask's type (relative/absolute
113         * path). The vector size will be at least 1, containing . or the full mask
114         * if the mask is entirely wildcardless.
115         */
116        private static Vector splitMask(String mask) {
117                Vector result = new Vector();
118
119                File f = new File(mask);
120                File p = f.getParentFile();
121                while (wildcarded(f.getPath()) && p != null) {
122                        if (isDebugging)
123                                log.debug("Parent of file: " + f.getPath() + " is "
124                                                + p.getPath());
125                        // insert in front the substring of the mask related to this dir
126                        result.add(0, f.getName());
127                        f = p;
128                        p = p.getParentFile();
129                }
130                // insert the full path of the wildcardless mask in front
131                result.add(0, f.getPath());
132
133                if (wildcarded(f.getPath())) { // mask does not contain a wildcardless
134                                                                                // head
135                        // the very first part is wildcarded
136                        // we have to add a . or / as first element
137                        if (f.isAbsolute())
138                                result.add(0, File.separator);
139                        else {
140                                // Because of later tests for symbolic links, simply adding .
141                                // here
142                                // leads to problems.
143                                // Instead, we add the canonical path of the . here
144                                String dot;
145                                try {
146                                        dot = new File(".").getCanonicalPath();
147                                } catch (IOException e) {
148                                        log.debug("Cannot get canonical filename of .");
149                                        dot = new String(".");
150                                }
151
152                                result.add(0, dot);
153
154                        }
155                }
156
157                if (isDebugging)
158                        log.debug("The splitted vector is " + result.size() + " long.:");
159                for (int i = 0; i < result.size(); i++) {
160                        if (isDebugging)
161                                log.debug("    " + result.get(i));
162                }
163                return result;
164        }
165
166        /**
167         * Recursively traverse the directories looking for matches on each level to
168         * the relevant part of the mask. Matched files will be deleted. Matched
169         * directories will be deleted only if 'recursive' is true.
170         */
171        private boolean delete(File node, Vector masks, boolean recursive) {
172
173                if (isDebugging)
174                        log.debug(">>> " + node.getPath() + " with masks length = "
175                                        + masks.size() + ": " + masks.toString());
176
177                // the query is for a single file/dir --> it will be deleted now
178                if (masks.isEmpty()) {
179                        return deleteNode(node, recursive, "Delete ");
180                }
181
182                // handle the case where path is not a directory but something else
183                if (!node.isDirectory()) {
184                        if (node.isFile()) {
185                                // single file
186                                // this file cannot match the rest of the query mask
187                                return true; // this is not an error, just skip
188                        } else {
189                                // wildcardless mask referred to a non-existing file/dir
190                                log.error("Path " + node.getPath() + " is not a directory!");
191                                return false;
192                        }
193                }
194
195                // path refers to an existing dir.
196                // Let's list its content with the appropriate mask
197                String localMask = null;
198                Vector restMask = (Vector) masks.clone();
199                if (!masks.isEmpty()) {
200                        localMask = (String) masks.firstElement(); // first element as local
201                                                                                                                // mask
202                        restMask.remove(0); // the rest
203                }
204
205                boolean result = true; // will become false if at least one file removal
206                                                                // fails
207
208                // handle special masks . and .. separately
209                if (localMask.equals(".") || localMask.equals("..")) {
210
211                        // we just need to call this method again with the next mask
212                        File newNode = new File(node, localMask);
213                        if (isDebugging)
214                                log.debug("Special case of " + localMask
215                                                + " --> Call delete() with " + newNode.getPath());
216                        result = delete(newNode, restMask, recursive);
217
218                } else {
219                        // meaningful mask... so list the directory and recursively traverse
220                        // directories
221                        MyLocalFilter localFilter = new MyLocalFilter(localMask);
222
223                        // Get files matching the localMask in the dir 'node'
224                        File[] files = node.listFiles(localFilter);
225
226                        if (isDebugging)
227                                log.debug("Found " + files.length + " matching files in "
228                                                + node);
229
230                        for (int i = 0; i < files.length; i++) {
231                                // recursive call with the rest of the masks
232                                boolean succ = delete(files[i], restMask, recursive);
233                                if (!succ && isDebugging)
234                                        log.debug("Failed removal of " + files[i].getPath());
235                                result = result && succ;
236                        }
237                }
238
239                if (isDebugging)
240                        log.debug("<<< " + node.getPath());
241                return result;
242        }
243
244        /**
245         * Recursively delete a file or directory. If the directory is a symbolic
246         * link, it is not followed, but only the link will be deleted.
247         */
248        private boolean deleteNode(File f, boolean recursive, String indent) {
249                boolean result = false;
250                if (!f.isDirectory()) {
251                        // single file
252                        log.info(indent + f);
253                        result = f.delete();
254                } else if (isSymbolicLink(f)) {
255                        // This directory is a symbolic link, and there's no reason for us
256                        // to
257                        // follow it, because then we might be deleting something outside of
258                        // the directory we were told to delete.
259                        // Delete the link only.
260                        log.info(indent + f + "@");
261                        result = f.delete();
262                } else if (recursive) {
263                        // directory and recursive is on
264                        File[] files = f.listFiles();
265                        for (int i = 0; i < files.length; i++) {
266                                deleteNode(files[i], recursive, indent + "    ");
267                        }
268                        // finally, delete the directory itself
269                        log.info(indent + f + File.separator);
270                        result = f.delete();
271                }
272                return result;
273        }
274
275        private class MyLocalFilter implements FilenameFilter {
276                Pattern p;
277
278                MyLocalFilter(String filemask) {
279                        String pattern;
280                        // convert file mask pattern to regular expression
281                        if (filemask != null) {
282                                String p1 = filemask.replaceAll("\\.", "\\\\.");
283                                String p2 = p1.replaceAll("\\*", ".*");
284                                String p3 = p2.replaceAll("\\?", ".");
285                                // System.out.println("pattern conversion: [" + p3 + "] = [" +
286                                // p1 + "] -> [" + p2 + "]");
287                                p = Pattern.compile(p3);
288                        } else
289                                p = null;
290                }
291
292                public boolean accept(File dir, String name) {
293                        if (p != null) {
294                                Matcher m = p.matcher(name);
295                                return m.matches();
296                        } else
297                                return true;
298                }
299        }
300
301        /**
302         * Test if a File is a symbolic link. It returns true if the file is a
303         * symbolic link, false otherwise. Exception: if the symlink points to
304         * itself, it returns false. Sorry. How does it work: it compares the
305         * canonical path and the absolute path of the file. The former gives the
306         * referred file of a link and thus differs from the absolute path of the
307         * link itself.
308         */
309        private static boolean isSymbolicLink(File f) {
310
311                if (f == null)
312                        return false;
313                if (!f.exists())
314                        return false;
315
316                // special case: path ends with .., which can never be a symlink, right?
317                // Note: symlink/.. refers to the directory containing the symlink and
318                // not the link itself
319                // special case: path ends with ., it is the same
320                // Note: symlink/. refers to the pointed directory and not the link
321                // itself
322                // They are handled here because they would result in true in later
323                // tests.
324                if (f.getName().equals("..") || f.getName().equals("."))
325                        return false;
326
327                // to see if this file is actually a symbolic link to a directory,
328                // we want to get its canonical path - that is, we follow the link to
329                // the file it's actually linked to
330                File canf;
331                try {
332                        canf = f.getCanonicalFile();
333                } catch (IOException e) {
334                        log.error("Cannot get canonical filename of file " + f.getPath());
335                        return true;
336                }
337
338                // we need to get the absolute path
339                // Unfortunately File.getAbsolutePath() does not eliminate . and ..
340                // thus the equality test fails for paths containing them.
341                // Let's do some magic with the parent dir name and get the absolute
342                // path this way.
343
344                File absf;
345                File parent = f.getParentFile();
346                if (parent == null) {
347                        // no problem, we have a single (relative) file name
348                        absf = f.getAbsoluteFile();
349                } else {
350                        // eliminate . and .. from the parent path, using getCanonicalFile()
351                        try {
352                                parent = parent.getCanonicalFile();
353                        } catch (IOException e) {
354                                log.error("Cannot get canonical filename of file "
355                                                + parent.getPath());
356                        }
357                        // recreate the absolute filename
358                        // Note: if f's name is .., this would not be eliminated here. See
359                        // pre-test above
360                        absf = new File(parent, f.getName());
361                }
362
363                if (isDebugging)
364                        log.debug("File " + f.getPath() + "\nCanonical =  "
365                                        + canf.getPath() + "\nAbsolute = " + absf.getPath());
366
367                // a symbolic link has a different canonical path than its actual path,
368                // unless it's a link to itself
369                return (!canf.equals(absf));
370        }
371
372}