001package dk.netarkivet.common.tools; 002 003import java.io.ByteArrayInputStream; 004import java.io.File; 005import java.io.FileInputStream; 006import java.io.FileNotFoundException; 007import java.io.FileOutputStream; 008import java.io.IOException; 009import java.io.InputStream; 010import java.lang.reflect.Field; 011import java.net.SocketException; 012import java.nio.ByteBuffer; 013import java.nio.channels.FileChannel; 014import java.util.ArrayList; 015import java.util.Arrays; 016import java.util.List; 017 018import org.apache.commons.io.IOUtils; 019import org.apache.commons.net.ftp.FTPClient; 020import org.apache.commons.net.ftp.FTPFile; 021 022import dk.netarkivet.common.CommonSettings; 023import dk.netarkivet.common.distribute.ExtendedFTPRemoteFile; 024import dk.netarkivet.common.distribute.FTPRemoteFile; 025import dk.netarkivet.common.distribute.RemoteFile; 026import dk.netarkivet.common.distribute.RemoteFileFactory; 027import dk.netarkivet.common.exceptions.ArgumentNotValid; 028import dk.netarkivet.common.exceptions.IOFailure; 029import dk.netarkivet.common.utils.FileUtils; 030import dk.netarkivet.common.utils.Settings; 031 032/** 033 * 034 * Tool for testing if a FTP server is NetarchiveSuite compliant. 035 * Usage: 036 * 037 * export OPTs=-Ddk.netarkivet.settings.file=$INSTALLDIR/conf/settings_GUIApplication.xml 038 * java $OPTS FTPValidator 039 * java FTPValidator /full/path/to/settings.xml 040 * java FTPValidator ftpHost ftpPort ftpUser ftpPasswd 041 * 042 */ 043public class FTPValidator { 044 045 public static final String SETTINGSFILEPATH = "dk.netarkivet.settings.file"; 046 047 private FTPClient theFTPClient; 048 private ArrayList<RemoteFile> upLoadedFTPRemoteFiles = new ArrayList<RemoteFile>(); 049 private ArrayList<String> upLoadedFiles = new ArrayList<String>(); 050 051 private File tmpDir = new File(this.getClass().getSimpleName()); 052 053 private File testFile1; 054 private File testFile2; 055 private File testFile3; 056 057 private String ftpHost; 058 private String ftpUser; 059 private String ftpPasswd; 060 private int ftpPort; 061 062 063 /** 064 * @param args 065 * @throws Exception 066 */ 067 public static void main(String[] args) throws Exception { 068 boolean useDefaultSettingsFile = false; 069 if (args.length == 0) { 070 useDefaultSettingsFile = true; 071 } else if (args.length == 1) { 072 System.out.println("Using settingsfile given as argument: " + args[0]); 073 System.setProperty(SETTINGSFILEPATH, args[0]); 074 File settingsfile = new File(args[0]); 075 if (!settingsfile.exists()) { 076 System.err.println("Aborting program. Settingsfile '" + settingsfile.getAbsolutePath() + "' does not exist or is not a file"); 077 System.exit(1); 078 } 079 useDefaultSettingsFile = true; 080 } else if (args.length != 4) { 081 printArgs(); 082 System.exit(1); 083 } 084 085 FTPValidator validator = null; 086 if (!useDefaultSettingsFile) { 087 String ftphost = args[0]; 088 int ftpPort = Integer.parseInt(args[1]); 089 String user = args[2]; 090 String passwd = args[3]; 091 System.out.println("Confirming ftp-server at '" + ftphost 092 + "' using username/passwd='" + user + "/" + passwd 093 + "'"); 094 validator = new FTPValidator(ftphost, ftpPort, user, passwd); 095 } else { 096 String remoteFileClassSet = Settings.get(CommonSettings.REMOTE_FILE_CLASS); 097 if (remoteFileClassSet.equals(FTPRemoteFile.class.getName()) || 098 remoteFileClassSet.equals(ExtendedFTPRemoteFile.class.getName())) { 099 validator = new FTPValidator(); 100 } else { 101 System.err.println("Wrong remotefileClass defined: " + remoteFileClassSet); 102 System.err.println("Aborting program"); 103 System.exit(1); 104 } 105 } 106 boolean result = validator.test(); 107 if (result == false) { 108 System.out.println("test failed"); 109 } else { 110 System.out.println("test succeeded"); 111 } 112 } 113 /** 114 * Constructor for the {@link FTPValidator} that takes the given arguments, 115 * and updates the FTP-settings accordingly. 116 * @param ftphost a given ftp-server 117 * @param port a given ftp-port number 118 * @param user a given ftp user 119 * @param passwd a given ftp password 120 * @throws Exception if not able to reset the temporary directory used by the tool. 121 */ 122 public FTPValidator(String ftphost, int port, String user, String passwd) throws IOException { 123 ftpHost = ftphost; 124 ftpUser = user; 125 ftpPasswd = passwd; 126 ftpPort = port; 127 Settings.set(CommonSettings.FTP_SERVER_NAME, ftpHost); 128 Settings.set(CommonSettings.FTP_SERVER_PORT, ftpPort + ""); 129 Settings.set(CommonSettings.FTP_USER_NAME, ftpUser); 130 Settings.set(CommonSettings.FTP_USER_PASSWORD, ftpPasswd); 131 Settings.set(CommonSettings.FTP_RETRIES_SETTINGS, "3"); 132 Settings.set(CommonSettings.REMOTE_FILE_CLASS, FTPRemoteFile.class.getName()); 133 134 if (tmpDir.exists()) { 135 FileUtils.removeRecursively(tmpDir); 136 if (tmpDir.exists()) { 137 String message= "Unable to delete tmpdir '" 138 + tmpDir.getAbsolutePath() + "'"; 139 throw new IOException(message); 140 } 141 } 142 143 if (!tmpDir.mkdir()) { 144 String message = "Unable to create tmpdir '" 145 + tmpDir.getAbsolutePath() + "'"; 146 throw new IOException(message); 147 } 148 } 149 150 public FTPValidator() { 151 ftpHost = Settings.get(CommonSettings.FTP_SERVER_NAME); 152 ftpUser = Settings.get(CommonSettings.FTP_USER_NAME); 153 ftpPasswd = Settings.get(CommonSettings.FTP_USER_PASSWORD); 154 ftpPort = Settings.getInt(CommonSettings.FTP_SERVER_PORT); 155 } 156 157 158 private boolean test() throws Exception { 159 /* make 3 duplicates of TestInfo.TESTXML: test1.xml, test2.xml, test3.xml */ 160 testFile1 = new File("FTPValidator_file1.xml"); 161 testFile2 = new File("FTPValidator_file2.xml"); 162 testFile3 = new File("FTPValidator_file3.xml"); 163 String fileAsString = "<test>" 164 + "<file>" 165 + "<attachHere>Should go away</attachHere>" 166 + "<keepThis>Should be kept</keepThis>" 167 + "</file>" 168 + "<foo>" 169 + "<attachHere>Should stay</attachHere>" 170 + "</foo>" 171 + "</test>"; 172 List<String> fileAsList = new ArrayList<String>(); 173 fileAsList.add(fileAsString); 174 FileUtils.writeCollectionToFile(testFile1, fileAsList); 175 FileUtils.writeCollectionToFile(testFile2, fileAsList); 176 FileUtils.writeCollectionToFile(testFile3, fileAsList); 177 178 /* Connect to test ftp-server. */ 179 theFTPClient = new FTPClient(); 180 181 try { 182 theFTPClient.connect(ftpHost, ftpPort); 183 if (!theFTPClient.login(ftpUser, ftpPasswd)) { 184 System.out.println("Could not login to ' + " + ftpHost 185 + ":" + ftpPort + "' with username,password=" 186 + ftpUser + "," + ftpPasswd); 187 System.exit(1); 188 } 189 if (!theFTPClient.setFileType(FTPClient.BINARY_FILE_TYPE)) { 190 System.out.println("Unable to set the file type to binary after login"); 191 System.exit(1); 192 } 193 if (!testConfigSettings()) { 194 return false; 195 } 196 if (!testUploadAndRetrieve()) { 197 return false; 198 } 199 if (!testDelete()) { 200 return false; 201 } 202 if (!test501MFile()) { 203 return false; 204 } 205 if (!testWrongChecksumThrowsError()) { 206 return false; 207 } 208 209 } catch (SocketException e) { 210 e.printStackTrace(); 211 throw new IOFailure("Connect to " + ftpHost + ":" + ftpPort + 212 " failed", e.getCause()); 213 } catch (IOException e) { 214 e.printStackTrace(); 215 throw new IOFailure("Connect to " + ftpHost + ":" + ftpPort + 216 " failed", e.getCause()); 217 } finally { 218 219 if (theFTPClient != null) { 220 theFTPClient.disconnect(); 221 } 222 } 223 return true; 224 } 225 226 /** 227 * Initially verify that communication with the ftp-server succeeds 228 * without using the RemoteFile. 229 * (1) Verify, that you can upload a file to a ftp-server, and retrieve the 230 * same file from this server-server. 231 * (2) Verify, that file was not corrupted in transit 232 * @throws IOException 233 */ 234 public boolean testConfigSettings() throws IOException { 235 /** this code has been tested with 236 * the ftp-server proftpd (www.proftpd.org), using 237 * the configuration stored in CVS here: /projects/webarkivering/proftpd.org 238 */ 239 InputStream in = null; 240 InputStream in2 = null; 241 InputStream in3 = null; 242 243 try { 244 String nameOfUploadedFile; 245 String nameOfUploadedFile3; 246 247 File inputFile = testFile1; 248 File inputFile2 = testFile2; 249 File inputFile3 = testFile3; 250 251 in = new FileInputStream(inputFile); 252 in2 = new FileInputStream(inputFile2); 253 in3 = new FileInputStream(inputFile3); 254 255 nameOfUploadedFile = inputFile.getName(); 256 nameOfUploadedFile3 = inputFile3.getName(); 257 258 /** try to append data to file on FTP-server. */ 259 /** Assumption: If file exists already on FTP-server, try to delete it */ 260 if (onServer(nameOfUploadedFile)) { 261 System.out.println("File '" + nameOfUploadedFile 262 + "' should not exist already on server. Trying to delete it"); 263 boolean deleted = theFTPClient.deleteFile(nameOfUploadedFile); 264 if (!deleted) { 265 System.err.println("Unable to delete file '" + nameOfUploadedFile + "' from ftp-server"); 266 return false; 267 } 268 } 269 270 if (!theFTPClient.appendFile(nameOfUploadedFile, in)) { 271 System.out.println("Appendfile operation failed"); 272 return false; 273 } 274 upLoadedFiles.add(nameOfUploadedFile); 275 // 276 // /** try to append data to file on the FTP-server. */ 277 // if (!theFTPClient.appendFile(nameOfUploadedFile, in2)) { 278 // System.out.println("Appendfile operation 2 failed"); 279 // return false; 280 // } 281 282 if (!upLoadedFiles.contains(nameOfUploadedFile)) { 283 upLoadedFiles.add(nameOfUploadedFile); 284 } 285 286 /** try to store data to a file on the FTP-server. */ 287 if (!theFTPClient.storeFile(nameOfUploadedFile3, in3)) { 288 System.out.println("Store operation failed"); 289 return false; 290 } 291 upLoadedFiles.add(nameOfUploadedFile3); 292 return true; 293 } finally { 294 IOUtils.closeQuietly(in); 295 IOUtils.closeQuietly(in2); 296 IOUtils.closeQuietly(in3); 297 } 298 } 299 300 private static void printArgs() { 301 System.out.println("ValidateFTPServer [ftphost ftpPort user passwd]"); 302 System.exit(1); 303 } 304 305 /** 306 * (1) Test, if uploaded and retrieved file are equal 307 * (2) test that rf.getSize() reports the correct value; 308 * @throws IOException 309 */ 310 public boolean testUploadAndRetrieve() throws IOException { 311 File testFile = testFile1; 312 RemoteFile rf = FTPRemoteFile.getInstance(testFile, true, false, true); 313 314 File newFile = new File(tmpDir, "newfile.xml"); 315 316 /** register that testFile should now be present on ftp-server */ 317 upLoadedFTPRemoteFiles.add(rf); 318 319 if (rf.getSize() != testFile.length()) { 320 System.out.println("The size of the file written to the ftp-server " + 321 "should not differ from the original size"); 322 return false; 323 } 324 rf.copyTo(newFile); 325 326 /** check, if the original file and the same file retrieved 327 * from the ftp-server contains the same contents 328 */ 329 byte[] datasend = FileUtils.readBinaryFile(testFile); 330 byte[] datareceived = FileUtils.readBinaryFile(newFile); 331 boolean isok = Arrays.equals(datareceived, datasend); 332 if (!isok) { 333 System.out.println("verify the same data received as uploaded "); 334 return false; 335 } 336 337 return true; 338 } 339 340 /** 341 * Check that the delete method can delete a file on the ftp server 342 * @throws FileNotFoundException 343 */ 344 public boolean testDelete() throws FileNotFoundException { 345 File testFile = testFile1; 346 RemoteFile rf = FTPRemoteFile.getInstance(testFile, true, false, true); 347 348 File newFile = new File(tmpDir, "newfile.xml"); 349 350 //Check that file is actually there 351 rf.copyTo(newFile); 352 353 // Delete the file (both locally and one the server) 354 newFile.delete(); 355 rf.cleanup(); 356 357 //And check to see that it's gone 358 try { 359 rf.copyTo(newFile); 360 System.out.println("Should throw an exception getting deleted file"); 361 return false; 362 } catch (IOFailure e) { 363 //expected 364 } 365 return true; 366 } 367 368 public boolean test501MFile() throws Exception { 369 long FiveHundredMbytes = 530000000; 370 File bigFile = new File(tmpDir, "500-mega"); 371 writeBytesToFile(FiveHundredMbytes, bigFile); 372 373 if (!bigFile.exists()) { 374 System.out.println("File '" + bigFile.getAbsolutePath() + 375 "' does not exist!"); 376 return false; 377 } 378 379 RemoteFile rf = FTPRemoteFile.getInstance(bigFile, true, false, true); 380 381 upLoadedFTPRemoteFiles.add(rf); 382 if (bigFile.length() != rf.getSize()) { 383 System.out.println("Size of Uploaded data should be the same as original data"); 384 return false; 385 } 386 387 File destinationFile = new File(tmpDir, 388 bigFile.getName() + ".new"); 389 390 rf.copyTo(destinationFile); 391 392 /** Check filesizes, and see, if they differ. */ 393 if (bigFile.length() != destinationFile.length()) { 394 System.out.println("Length of original unzipped file ' " 395 + bigFile.getAbsolutePath() 396 + "' and unzipped file retrieved from the ftp-server '" 397 + destinationFile.getAbsolutePath() + "'" 398 + "should not differ!"); 399 return false; 400 } 401 return true; 402 } 403 404 405 public boolean testWrongChecksumThrowsError() throws Exception { 406 RemoteFile rf = RemoteFileFactory.getInstance(testFile2, true, false, 407 true); 408 if (!(rf instanceof FTPRemoteFile)) { 409 System.out.println("The remotefile returned from the factory was incorrect type: " 410 + rf.getClass().getName()); 411 return false; 412 } 413 //upload error to ftp server 414 File temp = File.createTempFile("foo", "bar", tmpDir); 415 FTPClient client = new FTPClient(); 416 client.connect(ftpHost,ftpPort); 417 client.login(ftpUser, ftpPasswd); 418 Field field = FTPRemoteFile.class.getDeclaredField("ftpFileName"); 419 field.setAccessible(true); 420 String filename = (String)field.get(rf); 421 client.storeFile(filename, new ByteArrayInputStream("foo".getBytes())); 422 client.logout(); 423 try { 424 rf.copyTo(temp); 425 System.out.println("Should throw exception on wrong checksum"); 426 return false; 427 } catch(IOFailure e) { 428 //expected 429 } 430 if (temp.exists()) { 431 System.out.println( 432 "Destination file '" + temp.getAbsolutePath() 433 + "' should not exist"); 434 return false; 435 } 436 return true; 437 } 438 439 public boolean onServer(String nameOfUploadedFile) 440 throws IOException { 441 ArgumentNotValid.checkNotNull(theFTPClient, "theFTPClient should not be null"); 442 443 FTPFile[] listOfFiles = theFTPClient.listFiles(); 444 445 if (listOfFiles == null) { 446 return false; 447 } 448 449 for (int i = 0; i < listOfFiles.length; i++) { 450 if (listOfFiles[i].getName().equals(nameOfUploadedFile)) { 451 return true; 452 } 453 } 454 return false; 455 } 456 457 private static void writeBytesToFile(long bytes, File destination) throws Exception { 458 // A reasonably optimal value for the chunksize 459 int byteChunkSize = 10000000; 460 461 long nbytes = bytes; 462 File outputFile = destination; 463 byte[] byteArr = new byte[byteChunkSize]; 464 FileOutputStream os = new FileOutputStream(outputFile); 465 FileChannel chan = os.getChannel(); 466 for (int i = 0; i < nbytes / byteChunkSize; i++) { 467 chan.write(ByteBuffer.wrap(byteArr)); 468 } 469 os.close(); 470 chan.close(); 471 } 472}