001: /*
002: * SSHTools - Java SSH2 API
003: *
004: * Copyright (C) 2002-2003 Lee David Painter and Contributors.
005: *
006: * Contributions made by:
007: *
008: * Brett Smith
009: * Richard Pernavas
010: * Erwin Bolwidt
011: *
012: * This program is free software; you can redistribute it and/or
013: * modify it under the terms of the GNU General Public License
014: * as published by the Free Software Foundation; either version 2
015: * of the License, or (at your option) any later version.
016: *
017: * This program is distributed in the hope that it will be useful,
018: * but WITHOUT ANY WARRANTY; without even the implied warranty of
019: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
020: * GNU General Public License for more details.
021: *
022: * You should have received a copy of the GNU General Public License
023: * along with this program; if not, write to the Free Software
024: * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
025: */
026: package com.sshtools.ant;
027:
028: import com.sshtools.j2ssh.*;
029: import com.sshtools.j2ssh.sftp.*;
030:
031: import org.apache.tools.ant.*;
032: import org.apache.tools.ant.types.*;
033: import org.apache.tools.ant.util.*;
034:
035: import java.io.*;
036:
037: import java.util.*;
038:
039: /**
040: * Basic SFTP client. Performs the following actions:
041: * <ul>
042: * <li> <strong>put</strong> - send files to a remote server. This is the
043: * default action.</li>
044: * <li> <strong>get</strong> - retreive files from a remote server.</li>
045: * <li> <strong>del</strong> - delete files from a remote server.</li>
046: * <li> <strong>chmod</strong> - changes the mode of files on a remote server.</li>
047: * </ul>
048: *
049: */
050: public class Sftp extends SshSubTask {
051: protected static final int SEND_FILES = 0;
052: protected static final int GET_FILES = 1;
053: protected static final int DEL_FILES = 2;
054: protected static final int MK_DIR = 4;
055: protected static final int CHMOD = 5;
056:
057: //private Ssh parent = null;
058: protected static final String[] ACTION_STRS = { "Sending",
059: "Getting", "Deleting", "Listing", "Making directory",
060: "chmod" };
061: protected static final String[] COMPLETED_ACTION_STRS = { "Sent",
062: "Retrieved", "Deleted", "Listed", "Created directory",
063: "Mode changed" };
064: private String remotedir = ".";
065:
066: // private File listing;
067: private boolean verbose = false;
068: private boolean newerOnly = false;
069: private int action = SEND_FILES;
070: private Vector filesets = new Vector();
071: private Vector dirCache = new Vector();
072: private int transferred = 0;
073: private String remoteFileSep = "/";
074: private boolean skipFailedTransfers = false;
075: private int skipped = 0;
076: private boolean ignoreNoncriticalErrors = false;
077: private String chmod = "777";
078: private FileUtils fileUtils = FileUtils.newFileUtils();
079:
080: /**
081: * Set to true to receive notification about each file as it is
082: * transferred.
083: */
084: public void setVerbose(boolean verbose) {
085: this .verbose = verbose;
086: }
087:
088: /**
089: * Sets the remote working directory
090: * */
091: public void setRemotedir(String remotedir) {
092: this .remotedir = remotedir;
093: }
094:
095: /**
096: * A synonym for <tt>depends</tt>. Set to true to transmit only new or changed
097: * files.
098: */
099: public void setNewer(boolean newer) {
100: this .newerOnly = newer;
101: }
102:
103: /**
104: * Set to true to transmit only files that are new or changed from their
105: * remote counterparts. The default is to transmit all files.
106: */
107: public void setDepends(boolean depends) {
108: this .newerOnly = depends;
109: }
110:
111: /**
112: * Sets the file permission mode (Unix only) for files sent to the server.
113: */
114: public void setChmod(String theMode) {
115: this .chmod = theMode;
116: }
117:
118: /**
119: * A set of files to upload or download
120: */
121: public void addFileset(FileSet set) {
122: filesets.addElement(set);
123: }
124:
125: /**
126: * Sets the FTP action to be taken. Currently accepts "put", "get", "del",
127: * "mkdir" and "list".
128: *
129: * @deprecated setAction(String) is deprecated and is replaced with
130: * setAction(FTP.Action) to make Ant's Introspection mechanism do the
131: * work and also to encapsulate operations on the type in its own
132: * class.
133: * @ant.attribute ignore="true"
134: */
135:
136: /* public void setAction(String action) throws BuildException {
137: log("DEPRECATED - The setAction(String) method has been deprecated."
138: + " Use setAction(FTP.Action) instead.");
139: Action a = new Action();
140: a.setValue(action);
141: this.action = a.getAction();
142: }*/
143:
144: /**
145: * Sets the FTP action to be taken. Currently accepts "put", "get", "del",
146: * "mkdir", "chmod" and "list".
147: */
148: public void setAction(Action action) throws BuildException {
149: this .action = action.getAction();
150: }
151:
152: /**
153: * If true, enables unsuccessful file put, delete and get
154: * operations to be skipped with a warning and the remainder
155: * of the files still transferred.
156: */
157: public void setSkipFailedTransfers(boolean skipFailedTransfers) {
158: this .skipFailedTransfers = skipFailedTransfers;
159: }
160:
161: /**
162: * set the flag to skip errors on directory creation.
163: * (and maybe later other server specific errors)
164: */
165: public void setIgnoreNoncriticalErrors(
166: boolean ignoreNoncriticalErrors) {
167: this .ignoreNoncriticalErrors = ignoreNoncriticalErrors;
168: }
169:
170: /** Checks to see that all required parameters are set. */
171: protected void checkConfiguration() throws BuildException {
172: /* if ( (action == LIST_FILES) && (listing == null)) {
173: throw new BuildException("listing attribute must be set for list "
174: + "action!");
175: }*/
176: if ((action == MK_DIR) && (remotedir == null)) {
177: throw new BuildException(
178: "remotedir attribute must be set for "
179: + "mkdir action!");
180: }
181:
182: if ((action == CHMOD) && (chmod == null)) {
183: throw new BuildException(
184: "chmod attribute must be set for chmod "
185: + "action!");
186: }
187: }
188:
189: /**
190: * For each file in the fileset, do the appropriate action: send, get,
191: * delete, or list.
192: */
193: protected int transferFiles(SftpClient sftp, FileSet fs)
194: throws IOException, BuildException {
195: FileScanner ds;
196:
197: if (action == SEND_FILES) {
198: ds = fs.getDirectoryScanner(parent.getProject());
199: } else {
200: ds = new SftpDirectoryScanner(sftp);
201: fs.setupDirectoryScanner(ds, parent.getProject());
202: ds.scan();
203: }
204:
205: String[] dsfiles = ds.getIncludedFiles();
206: String dir = null;
207:
208: if ((ds.getBasedir() == null)
209: && ((action == SEND_FILES) || (action == GET_FILES))) {
210: throw new BuildException(
211: "the dir attribute must be set for send "
212: + "and get actions");
213: } else {
214: if ((action == SEND_FILES) || (action == GET_FILES)) {
215: dir = ds.getBasedir().getAbsolutePath();
216: }
217: }
218:
219: // If we are doing a listing, we need the output stream created now.
220: BufferedWriter bw = null;
221:
222: try {
223: /*if (action == LIST_FILES) {
224: File pd = fileUtils.getParentFile(listing);
225: if (!pd.exists()) {
226: pd.mkdirs();
227: }
228: bw = new BufferedWriter(new FileWriter(listing));
229: }*/
230: for (int i = 0; i < dsfiles.length; i++) {
231: switch (action) {
232: case SEND_FILES: {
233: sendFile(sftp, dir, dsfiles[i]);
234:
235: break;
236: }
237:
238: case GET_FILES: {
239: getFile(sftp, dir, dsfiles[i]);
240:
241: break;
242: }
243:
244: case DEL_FILES: {
245: delFile(sftp, dsfiles[i]);
246:
247: break;
248: }
249:
250: case CHMOD: {
251: chmod(sftp, dsfiles[i]);
252: transferred++;
253:
254: break;
255: }
256:
257: default:
258: throw new BuildException("unknown ftp action "
259: + action);
260: }
261: }
262: } finally {
263: if (bw != null) {
264: bw.close();
265: }
266: }
267:
268: return dsfiles.length;
269: }
270:
271: /**
272: * Sends all files specified by the configured filesets to the remote
273: * server.
274: */
275: protected void transferFiles(SftpClient sftp) throws IOException,
276: BuildException {
277: transferred = 0;
278: skipped = 0;
279:
280: if (filesets.size() == 0) {
281: throw new BuildException(
282: "at least one fileset must be specified.");
283: } else {
284: // get files from filesets
285: for (int i = 0; i < filesets.size(); i++) {
286: FileSet fs = (FileSet) filesets.elementAt(i);
287:
288: if (fs != null) {
289: transferFiles(sftp, fs);
290: }
291: }
292: }
293:
294: log(transferred + " files " + COMPLETED_ACTION_STRS[action]);
295:
296: if (skipped != 0) {
297: log(skipped + " files were not successfully "
298: + COMPLETED_ACTION_STRS[action]);
299: }
300: }
301:
302: /**
303: * Correct a file path to correspond to the remote host requirements. This
304: * implementation currently assumes that the remote end can handle
305: * Unix-style paths with forward-slash separators. This can be overridden
306: * with the <code>separator</code> task parameter. No attempt is made to
307: * determine what syntax is appropriate for the remote host.
308: */
309: protected String resolveFile(String file) {
310: return file.replace(System.getProperty("file.separator")
311: .charAt(0), remoteFileSep.charAt(0));
312: }
313:
314: /**
315: * Creates all parent directories specified in a complete relative
316: * pathname. Attempts to create existing directories will not cause
317: * errors.
318: */
319: protected void createParents(SftpClient sftp, String filename)
320: throws IOException, BuildException {
321: Vector parents = new Vector();
322: File dir = new File(filename);
323: String dirname;
324:
325: while ((dirname = dir.getParent()) != null) {
326: dir = new File(dirname);
327: parents.addElement(dir);
328: }
329:
330: for (int i = parents.size() - 1; i >= 0; i--) {
331: dir = (File) parents.elementAt(i);
332:
333: if (!dirCache.contains(dir)) {
334: log("creating remote directory "
335: + resolveFile(dir.getPath()),
336: Project.MSG_VERBOSE);
337:
338: try {
339: sftp.mkdir(resolveFile(dir.getPath()));
340: } catch (IOException ex) {
341: }
342:
343: dirCache.addElement(dir);
344: }
345: }
346: }
347:
348: /**
349: * Checks to see if the remote file is current as compared with the local
350: * file. Returns true if the remote file is up to date.
351: */
352: protected boolean isUpToDate(SftpClient sftp, File localFile,
353: String remoteFile) throws IOException, BuildException {
354: try {
355: log("Checking date for " + remoteFile, Project.MSG_VERBOSE);
356:
357: FileAttributes attrs = sftp.stat(remoteFile);
358:
359: // SFTP uses seconds since Jan 1 1970 UTC
360: long remoteTimestamp = attrs.getModifiedTime().longValue() * 1000; //files[0].getTimestamp().getTime().getTime();
361:
362: // Java uses milliseconds since Jan 1 1970 UTC
363: long localTimestamp = localFile.lastModified();
364:
365: if (this .action == SEND_FILES) {
366: return remoteTimestamp > localTimestamp;
367: } else {
368: return localTimestamp > remoteTimestamp;
369: }
370: } catch (IOException ex) {
371: return false;
372: }
373: }
374:
375: /**
376: * Sends a single file to the remote host. <code>filename</code> may
377: * contain a relative path specification. When this is the case, <code>sendFile</code>
378: * will attempt to create any necessary parent directories before sending
379: * the file. The file will then be sent using the entire relative path
380: * spec - no attempt is made to change directories. It is anticipated that
381: * this may eventually cause problems with some FTP servers, but it
382: * simplifies the coding.
383: */
384: protected void sendFile(SftpClient sftp, String dir, String filename)
385: throws IOException, BuildException {
386: InputStream instream = null;
387: SftpFileOutputStream out = null;
388:
389: try {
390: File file = parent.getProject().resolveFile(
391: new File(dir, filename).getPath());
392: String remotefile = resolveFile(filename);
393:
394: if (newerOnly && isUpToDate(sftp, file, remotefile)) {
395: return;
396: }
397:
398: if (verbose) {
399: log("transferring " + file.getAbsolutePath() + " to "
400: + remotedir + remotefile);
401: }
402:
403: instream = new BufferedInputStream(
404: new FileInputStream(file));
405: createParents(sftp, filename);
406: sftp.put(file.getAbsolutePath(), remotefile);
407:
408: // Set the umask
409: sftp.chmod(Integer.parseInt(chmod, 8), remotefile);
410: log("File " + file.getAbsolutePath() + " copied to "
411: + parent.host, Project.MSG_VERBOSE);
412: transferred++;
413: } catch (IOException ex1) {
414: String s = "Could not put file: " + ex1.getMessage();
415:
416: if (skipFailedTransfers == true) {
417: log(s, Project.MSG_WARN);
418: skipped++;
419: } else {
420: throw new BuildException(s);
421: }
422: } finally {
423: try {
424: if (instream != null) {
425: instream.close();
426: }
427: } catch (IOException ex) {
428: // ignore it
429: }
430:
431: try {
432: if (out != null) {
433: out.close();
434: }
435: } catch (IOException ex) {
436: }
437: }
438: }
439:
440: /** Delete a file from the remote host. */
441: protected void delFile(SftpClient sftp, String filename)
442: throws IOException, BuildException {
443: if (verbose) {
444: log("deleting " + filename);
445: }
446:
447: try {
448: String remotefile = resolveFile(filename);
449: sftp.rm(remotefile);
450: log("File " + filename + " deleted from " + parent.host,
451: Project.MSG_VERBOSE);
452: transferred++;
453: } catch (IOException ex) {
454: String s = "could not delete file: " + ex.getMessage();
455:
456: if (skipFailedTransfers == true) {
457: log(s, Project.MSG_WARN);
458: skipped++;
459: } else {
460: throw new BuildException(s);
461: }
462: }
463: }
464:
465: protected void chmod(SftpClient sftp, String filename)
466: throws IOException, BuildException {
467: sftp.chmod(Integer.parseInt(chmod, 8), resolveFile(filename));
468: }
469:
470: /**
471: * Retrieve a single file to the remote host. <code>filename</code> may
472: * contain a relative path specification. <p>
473: *
474: * The file will then be retreived using the entire relative path spec -
475: * no attempt is made to change directories. It is anticipated that this
476: * may eventually cause problems with some FTP servers, but it simplifies
477: * the coding.</p>
478: */
479: protected void getFile(SftpClient sftp, String dir, String filename)
480: throws IOException, BuildException {
481: try {
482: String localfile = filename;
483:
484: if (localfile.indexOf("/") >= 0) {
485: localfile = localfile.substring(filename
486: .lastIndexOf("/"));
487: }
488:
489: File file = parent.getProject().resolveFile(
490: new File(dir, localfile).getAbsolutePath());
491: log(dir);
492: log(filename);
493: log(file.getAbsolutePath());
494:
495: if (newerOnly
496: && isUpToDate(sftp, file, resolveFile(filename))) {
497: return;
498: }
499:
500: if (verbose) {
501: log("transferring " + filename + " to "
502: + file.getAbsolutePath());
503: }
504:
505: File pdir = fileUtils.getParentFile(file);
506:
507: if (!pdir.exists()) {
508: pdir.mkdirs();
509: }
510:
511: //sftp.lcd(dir);
512: // Get the file
513: sftp.get(filename, file.getAbsolutePath());
514:
515: if (verbose) {
516: log("File " + file.getAbsolutePath() + " copied from "
517: + parent.host);
518: }
519:
520: FileAttributes attrs = sftp.stat(filename);
521: file
522: .setLastModified(attrs.getModifiedTime()
523: .longValue() * 1000);
524: transferred++;
525: } catch (IOException ioe) {
526: String s = "could not get file: " + ioe.getMessage();
527:
528: if (skipFailedTransfers == true) {
529: log(s, Project.MSG_WARN);
530: skipped++;
531: } else {
532: throw new BuildException(s);
533: }
534: }
535: }
536:
537: /**
538: * Create the specified directory on the remote host.
539: *
540: * @param sftp The SFTP client connection
541: * @param dir The directory to create
542: */
543: protected void makeRemoteDir(SftpClient sftp, String dir)
544: throws BuildException {
545: if (verbose) {
546: log("creating directory: " + dir);
547: }
548:
549: try {
550: sftp.mkdir(dir);
551: } catch (IOException ex) {
552: log(ex.getMessage());
553: }
554: }
555:
556: /** Runs the task. */
557: public void execute(SshClient ssh) throws BuildException {
558: try {
559: Integer.parseInt(chmod, 8);
560: } catch (NumberFormatException ex) {
561: throw new BuildException(
562: "chmod attribute format is incorrect, use octal number format i.e 0777");
563: }
564:
565: executeSFTPTask(ssh);
566: }
567:
568: protected void executeSFTPTask(SshClient ssh) throws BuildException {
569: SftpClient sftp = null;
570:
571: try {
572: sftp = ssh.openSftpClient();
573:
574: if (action == MK_DIR) {
575: makeRemoteDir(sftp, remotedir);
576: } else {
577: if (remotedir.trim().length() > 0) {
578: log("Setting the remote directory "); //, Project.MSG_VERBOSE);
579: sftp.cd(remotedir);
580: }
581:
582: // Get the absolute path of the remote directory
583: remotedir = sftp.pwd();
584: log("Remote directory is " + remotedir);
585:
586: if (!remotedir.endsWith("/")) {
587: remotedir += "/";
588: }
589:
590: log(ACTION_STRS[action] + " files");
591: transferFiles(sftp);
592: }
593: } catch (IOException ex) {
594: throw new BuildException("error during SFTP transfer: "
595: + ex);
596: } finally {
597: if ((sftp != null) && !sftp.isClosed()) {
598: try {
599: log("Quiting SFTP", Project.MSG_VERBOSE);
600: sftp.quit();
601: } catch (IOException ex) {
602: // ignore it
603: }
604: }
605: }
606: }
607:
608: protected class SftpDirectoryScanner extends DirectoryScanner {
609: protected SftpClient sftp = null;
610:
611: public SftpDirectoryScanner(SftpClient sftp) {
612: super ();
613: this .sftp = sftp;
614: }
615:
616: public void scan() {
617: if (includes == null) {
618: // No includes supplied, so set it to 'matches all'
619: includes = new String[1];
620: includes[0] = "**";
621: }
622:
623: if (excludes == null) {
624: excludes = new String[0];
625: }
626:
627: filesIncluded = new Vector();
628: filesNotIncluded = new Vector();
629: filesExcluded = new Vector();
630: dirsIncluded = new Vector();
631: dirsNotIncluded = new Vector();
632: dirsExcluded = new Vector();
633: scandir(remotedir, true);
634: }
635:
636: protected void scandir(String dir, boolean fast) {
637: try {
638: List children = sftp.ls(dir);
639:
640: if (!dir.endsWith("/")) {
641: dir += "/";
642: }
643:
644: Iterator it = children.iterator();
645:
646: while (it.hasNext()) {
647: SftpFile file = (SftpFile) it.next();
648:
649: if (!file.getFilename().equals(".")
650: && !file.getFilename().equals("..")) {
651: if (file.isDirectory()) {
652: String name = dir + file.getFilename();
653:
654: if (isIncluded(name)) {
655: if (!isExcluded(name)) {
656: dirsIncluded.addElement(name);
657:
658: if (fast) {
659: scandir(dir
660: + file.getFilename(),
661: fast);
662: }
663: } else {
664: dirsExcluded.addElement(name);
665:
666: if (fast && couldHoldIncluded(name)) {
667: scandir(dir
668: + file.getFilename(),
669: fast);
670: }
671: }
672: } else {
673: dirsNotIncluded.addElement(name);
674:
675: if (fast && couldHoldIncluded(name)) {
676: scandir(dir + file.getFilename(),
677: fast);
678: }
679: }
680:
681: if (!fast) {
682: scandir(dir + file.getFilename(), fast);
683: }
684: } else {
685: if (file.isFile()) {
686: String name = dir + file.getFilename();
687:
688: if (isIncluded(name)) {
689: if (!isExcluded(name)) {
690: filesIncluded.addElement(name);
691: } else {
692: filesExcluded.addElement(name);
693: }
694: } else {
695: filesNotIncluded.addElement(name);
696: }
697: }
698: }
699: }
700: }
701:
702: //ftp.changeToParentDirectory();
703: } catch (IOException e) {
704: throw new BuildException(
705: "Error while communicating with SFTP ", e);
706: }
707: }
708: }
709:
710: /**
711: * an action to perform, one of
712: * "send", "put", "recv", "get", "del", "delete", "list", "mkdir", "chmod"
713: */
714: public static class Action extends EnumeratedAttribute {
715: private static final String[] validActions = { "send", "put",
716: "recv", "get", "del", "delete", "list", "mkdir",
717: "chmod" };
718:
719: public String[] getValues() {
720: return validActions;
721: }
722:
723: public int getAction() {
724: String actionL = getValue().toLowerCase(Locale.US);
725:
726: if (actionL.equals("send") || actionL.equals("put")) {
727: return SEND_FILES;
728: } else if (actionL.equals("recv") || actionL.equals("get")) {
729: return GET_FILES;
730: } else if (actionL.equals("del")
731: || actionL.equals("delete")) {
732: return DEL_FILES;
733: }
734: /*else if (actionL.equals("list")) {
735: return LIST_FILES;
736: }*/
737: else if (actionL.equals("chmod")) {
738: return CHMOD;
739: } else if (actionL.equals("mkdir")) {
740: return MK_DIR;
741: }
742:
743: return SEND_FILES;
744: }
745: }
746: }
|