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-05 22:21:26 -0700 (Wed, 05 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.actor.ssh;
031
032import java.io.File;
033
034import org.apache.commons.logging.Log;
035import org.apache.commons.logging.LogFactory;
036import org.kepler.ssh.ExecException;
037import org.kepler.ssh.ExecFactory;
038import org.kepler.ssh.ExecInterface;
039import org.kepler.ssh.LocalExec;
040
041import ptolemy.actor.TypedAtomicActor;
042import ptolemy.actor.TypedIOPort;
043import ptolemy.actor.parameters.PortParameter;
044import ptolemy.data.BooleanToken;
045import ptolemy.data.StringToken;
046import ptolemy.data.expr.Parameter;
047import ptolemy.data.type.BaseType;
048import ptolemy.kernel.CompositeEntity;
049import ptolemy.kernel.util.IllegalActionException;
050import ptolemy.kernel.util.NameDuplicationException;
051
052//////////////////////////////////////////////////////////////////////////
053//// FileCopier 
054/**
055 * Connects to a remote host using Ssh protocol (or does nothing for the local
056 * host) and copies a file to or from there.
057 * 
058 * <p>
059 * This actor uses the org.kepler.ssh package for longlasting connections
060 * 
061 * <p>
062 * The file references should be in the format: [[user@]host:]path. E.g.
063 * <ul>
064 * <li><it>foo.txt</it> foo.txt in the current dir on the local machine</li>
065 * <li><it>playdir/foo.txt</it> relative path to current dir on the local
066 * machine</li>
067 * <li><it>/home/littleboy/playdir/foo.txt<it> absolute path on the local
068 * machine</li>
069 * <li><it>local:playdir/foo.txt</it> relative path to $HOME on the local
070 * machine</li>
071 * <li><it>localhost:playdir/foo.txt</it> relative path to $HOME on the
072 * 'localhost' machine (it counts to be a remote file!)</li>
073 * <li><it>john@farmachine:playdir/foo.txt</it> relative path to $HOME on the
074 * 'farmachine' machine of user 'john'</li>
075 * </ul>
076 * 
077 * <p>
078 * The target becomes overwritten if exists, just like with scp and cp, in case
079 * of single files. For directories, a subdirectory will be created with the
080 * name of the source within the existing directory (again, just like with scp
081 * and cp).
082 * 
083 * <p>
084 * If the source refers to a directory, you should set the parameter 'recursive'
085 * to true, and then the whole directory will be copied to target.
086 * 
087 * <p>
088 * Either the source or the target file should be local. This actor cannot copy
089 * remote files to remote places. For such operations, you need to use ExecCmd
090 * actor with executing remote scp commands.
091 * 
092 * <p>
093 * If both source and target refers to local files/directories, the Java File
094 * class will be used for local copy instead of ssh. It behaves similarly to
095 * other local file copier actors of Kepler but you do not need to change your
096 * workflow for remote/local executions by using this actor.
097 * 
098 * <p>
099 * This actor produces a Boolean token on 'succ' port. TRUE indicates successful
100 * operation, while false indicates an error. The actor also produces a String
101 * token on the 'error' port; an empty string on success, internal error
102 * messages on failure.
103 * 
104 * @author Norbert Podhorszki
105 * @version $Revision: 24234 $
106 * @category.name remote
107 * @category.name connection
108 * @category.name file operation
109 */
110
111public class FileCopier extends TypedAtomicActor {
112
113        /**
114         * Construct an ExecuteCmd actor with the given container and name. Create
115         * the parameters, initialize their values.
116         * 
117         * @param container
118         *            The container.
119         * @param name
120         *            The name of this actor.
121         * @exception IllegalActionException
122         *                If the entity cannot be contained by the proposed
123         *                container.
124         * @exception NameDuplicationException
125         *                If the container already has an actor with this name.
126         */
127        public FileCopier(CompositeEntity container, String name)
128                        throws NameDuplicationException, IllegalActionException {
129
130                super(container, name);
131
132                /*
133                 * Input ports
134                 */
135
136                // source file/dir
137                source = new PortParameter(this, "source", new StringToken(
138                                "[[user]@host:]path"));
139                new Parameter(source.getPort(), "_showName", BooleanToken.TRUE);
140
141                // target file/dir
142                target = new PortParameter(this, "target", new StringToken(
143                                "[[user]@host:]path"));
144                new Parameter(target.getPort(), "_showName", BooleanToken.TRUE);
145
146                // recursive parameter
147                recursive = new Parameter(this, "recursive", new BooleanToken(false));
148                recursive.setTypeEquals(BaseType.BOOLEAN);
149
150                /*
151                 * Output ports
152                 */
153
154                succ = new TypedIOPort(this, "succ", false, true);
155                succ.setTypeEquals(BaseType.BOOLEAN);
156                new Parameter(succ, "_showName", BooleanToken.TRUE);
157
158                error = new TypedIOPort(this, "error", false, true);
159                error.setTypeEquals(BaseType.STRING);
160                new Parameter(error, "_showName", BooleanToken.TRUE);
161
162                _attachText("_iconDescription", "<svg>\n" + "<rect x=\"0\" y=\"0\" "
163                                + "width=\"75\" height=\"50\" style=\"fill:blue\"/>\n"
164                                + "<text x=\"5\" y=\"30\""
165                                + "style=\"font-size:14; fill:yellow; font-family:SansSerif\">"
166                                + "SshExec</text>\n" + "</svg>\n");
167        }
168
169        // //////////////// Public ports and parameters ///////////////////////
170
171        /**
172         * Source in user@host:path format. If user is not provided, the local
173         * username will be used. If host is "local" or empty string, the path is
174         * handled as local path.
175         */
176        public PortParameter source;
177
178        /**
179         * Target in user@host:path format. If user is not provided, the local
180         * username will be used. If host is "local:" or empty string, the path is
181         * handled as local path.
182         */
183        public PortParameter target;
184
185        /**
186         * The flag of successful copy. It is a port of type Boolean token.
187         */
188        public TypedIOPort succ;
189
190        /**
191         * The string representation of all the errors that happened during the
192         * execution of the actor, if there are any. A port of type String token.
193         */
194        public TypedIOPort error;
195
196        /**
197         * Specifying whether directories can be copied recursively.
198         */
199        public Parameter recursive;
200
201        // /////////////////////////////////////////////////////////////////
202        // // public methods ////
203
204        /**
205         * Perform copying.
206         * 
207         * @exception IllegalActionException
208         *                If it is thrown by the send() method sending out the
209         *                token.
210         */
211        public void fire() throws IllegalActionException {
212                super.fire();
213
214                // get inputs
215                source.update();
216                String strSource = ((StringToken) source.getToken()).stringValue()
217                                .trim();
218
219                target.update();
220                String strTarget = ((StringToken) target.getToken()).stringValue()
221                                .trim();
222
223                boolean recursiveFlag = ((BooleanToken) recursive.getToken())
224                                .booleanValue();
225
226                /*
227                 * process source string
228                 */
229                String userSource;
230                String hostSource;
231                //int portSource = 22;
232                int portSource = -1;
233                String pathSource;
234                boolean relativeToCurrentSource = false; // true: local file, relative
235                                                                                                        // to current dir
236
237                if (strSource.indexOf(":\\") == -1) { // not windows path like D:\Temp
238
239                        // get USER
240                        int atPos = strSource.indexOf('@');
241                        if (atPos >= 0)
242                                userSource = strSource.substring(0, atPos);
243                        else
244                                userSource = System.getProperty("user.name");
245
246                        // get HOST
247                        int colonPos = strSource.indexOf(':');
248                        if (colonPos >= 0)
249                                if (atPos >= 0)
250                                        hostSource = strSource.substring(atPos + 1, colonPos);
251                                else
252                                        hostSource = strSource.substring(0, colonPos);
253                        else {
254                                hostSource = new String("local");
255                                relativeToCurrentSource = true;
256                        }
257
258                        // get PORT (default 22 is already set)
259                        String subS = strSource.substring(colonPos + 1);
260                        if (colonPos >= 0) {
261                                colonPos = subS.indexOf(':'); // look for second occurence of :
262                                if (colonPos >= 0) {
263                                        String portStr = subS.substring(0, colonPos);
264                                        if (!portStr.trim().equals("")) {
265                                                try {
266                                                        portSource = Integer.parseInt(portStr);
267                                                } catch (java.lang.NumberFormatException ex) {
268                                                        throw new IllegalActionException(
269                                                                        "The port should be a number or omitted in source path "
270                                                                                        + strSource);
271                                                }
272                                        }
273                                }
274                        }
275                        pathSource = subS.substring(colonPos + 1); // the rest of the string
276
277                } else { // windows path means local path
278                        userSource = System.getProperty("user.name");
279                        hostSource = new String("local");
280                        pathSource = strSource;
281                        relativeToCurrentSource = true;
282                }
283
284                if (isDebugging)
285                        log
286                                        .debug("Source: user=[" + userSource + "], host=["
287                                                        + hostSource + "], port=[" + portSource
288                                                        + "], path=[" + pathSource + "]");
289
290                /*
291                 * process target string
292                 */
293                String userTarget;
294                String hostTarget;
295                //int portTarget = 22;
296                int portTarget = -1;
297                String pathTarget;
298                boolean relativeToCurrentTarget = false; // true: local file, relative
299                                                                                                        // to current dir
300
301                if (strTarget.indexOf(":\\") == -1) { // not windows path like D:\Temp
302
303                        // get USER
304                        int atPos = strTarget.indexOf('@');
305                        if (atPos >= 0)
306                                userTarget = strTarget.substring(0, atPos);
307                        else
308                                userTarget = System.getProperty("user.name");
309
310                        // get HOST
311                        int colonPos = strTarget.indexOf(':');
312                        if (colonPos >= 0)
313                                if (atPos >= 0)
314                                        hostTarget = strTarget.substring(atPos + 1, colonPos);
315                                else
316                                        hostTarget = strTarget.substring(0, colonPos);
317                        else {
318                                hostTarget = new String("local");
319                                relativeToCurrentTarget = true;
320                        }
321
322                        // get PORT (default 22 is already set)
323                        String subS = strTarget.substring(colonPos + 1);
324                        if (colonPos >= 0) {
325                                colonPos = subS.indexOf(':'); // look for second occurence of :
326                                if (colonPos >= 0) {
327                                        String portStr = subS.substring(0, colonPos);
328                                        if (!portStr.trim().equals("")) {
329                                                try {
330                                                        portTarget = Integer.parseInt(portStr);
331                                                } catch (java.lang.NumberFormatException ex) {
332                                                        throw new IllegalActionException(
333                                                                        "The port should be a number or omitted in target path "
334                                                                                        + strTarget);
335                                                }
336                                        }
337                                }
338                        }
339                        pathTarget = subS.substring(colonPos + 1); // the rest of the string
340
341                } else { // windows path means local path
342                        userTarget = System.getProperty("user.name");
343                        hostTarget = new String("local");
344                        pathTarget = strTarget;
345                        relativeToCurrentTarget = true;
346                }
347
348                if (isDebugging)
349                        log
350                                        .debug("Target: user=[" + userTarget + "], host=["
351                                                        + hostTarget + "], port=[" + portTarget
352                                                        + "], path=[" + pathTarget + "]");
353
354                // error check: not both refers to remote place
355                if (!hostSource.equals("local") && !hostTarget.equals("local")) {
356                        String msg = new String(
357                                        "One of the source and target should be local.");
358                        log.error(msg);
359                        succ.send(0, new BooleanToken(false));
360                        error.send(0, new StringToken(msg));
361                        return;
362                }
363
364                // error check: source reference is empty
365                if (pathSource.length() == 0) {
366                        String msg = new String("Source path is empty in " + strSource);
367                        log.error(msg);
368                        succ.send(0, new BooleanToken(false));
369                        error.send(0, new StringToken(msg));
370                        return;
371                }
372
373                // semantic check: target reference is empty: replace with .
374                // SshExec's copyTo does not work with empty path string but it works
375                // the same
376                // on . (scp works the same on empty string and .)
377                if (pathTarget.length() == 0) {
378                        pathTarget = new String(".");
379                }
380
381                // source case of local:relativePath --> relative to $HOME, so
382                // absolutize now
383                if (hostSource.equals("local") && !relativeToCurrentSource
384                                && pathSource.charAt(0) != File.separatorChar) {
385
386                        pathSource = System.getProperty("user.home") + File.separator
387                                        + pathSource;
388                        if (isDebugging)
389                                log.debug("Source path absolutized to $HOME: " + pathSource);
390                }
391
392                // target case of local:relativePath --> relative to $HOME, so
393                // absolutize now
394                if (hostTarget.equals("local") && !relativeToCurrentTarget
395                                && pathTarget.charAt(0) != File.separatorChar) {
396
397                        pathTarget = System.getProperty("user.home") + File.separator
398                                        + pathTarget;
399                        if (isDebugging)
400                                log.debug("Target path absolutized to $HOME: " + pathTarget);
401                }
402
403                // select the appropriate execution setting for the current source and
404                // target
405                ExecInterface execObj; // local or remote copy
406                boolean copyTo = true; // copyTo (local->remote) or copyFrom
407                                                                // (remote->local)
408                try{
409                        if (hostSource.equals("local") && hostTarget.equals("local")) {
410                                // local copy
411                                if (isDebugging)
412                                        log.debug("Execution mode: local using Java File class");
413                                execObj = new LocalExec();
414                                copyTo = true;
415                        } else if (hostSource.equals("local")) {
416                                // create an SshExec object, and set copyTo to true
417                                if (isDebugging)
418                                        log.debug("Execution mode: remote copyTo using ssh");
419                                //execObj = new SshExec(userTarget, hostTarget, portTarget);
420                                execObj = ExecFactory.getExecObject(userTarget, hostTarget, portTarget);
421                                copyTo = true;
422                        } else {
423                                // create an SshExec object, and set copyTo to false
424                                if (isDebugging)
425                                        log.debug("Execution mode: remote copyFrom using ssh");
426                                //execObj = new SshExec(userSource, hostSource, portSource);
427                                execObj = ExecFactory.getExecObject(userSource, hostSource, portSource);
428                                copyTo = false;
429                        }
430                }catch(ExecException e){
431                        String errText = new String("Error connecting to remote host:\n"
432                                        + e.getMessage());
433
434                        log.error(errText);
435                        succ.send(0, new BooleanToken(false));
436                        error.send(0, new StringToken(errText));
437                        return;
438                }
439
440                File lfile;
441                int numberOfCopiedFiles = 0;
442
443                long startTime = System.currentTimeMillis();
444                try {
445                        if (copyTo) {
446                                lfile = new File(pathSource);
447                                numberOfCopiedFiles = execObj.copyTo(lfile, pathTarget,
448                                                recursiveFlag);
449                        } else {
450                                lfile = new File(pathTarget);
451                                numberOfCopiedFiles = execObj.copyFrom(pathSource, lfile,
452                                                recursiveFlag);
453                        }
454
455                } catch (ExecException e) {
456                        String errText = new String("Error at copy execution:\n"
457                                        + e.getMessage());
458
459                        if (isDebugging)
460                                log.debug(errText);
461                        succ.send(0, new BooleanToken(false));
462                        error.send(0, new StringToken(errText));
463                        return;
464                }
465                long endTime = System.currentTimeMillis();
466
467                if (isDebugging)
468                        log.debug("Number of copied files = " + numberOfCopiedFiles
469                                        + ". Time to copy: " + (endTime - startTime) + " msec.");
470
471                if (numberOfCopiedFiles <= 0) {
472                        String errText = new String(
473                                        "No file(s) were copied for unknown reasons\n");
474                        if (isDebugging)
475                                log.debug(errText);
476                        succ.send(0, new BooleanToken(false));
477                        error.send(0, new StringToken(errText));
478                } else {
479                        // finally, good news can be reported
480                        succ.send(0, new BooleanToken(true));
481                        error.send(0, new StringToken(""));
482                }
483
484        } // end-method fire()
485
486        private static final Log log = LogFactory
487                        .getLog(FileCopier.class.getName());
488        private static final boolean isDebugging = log.isDebugEnabled();
489}
490
491// vim: sw=4 ts=4 et