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}