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}