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