Wrapping FTP in Emacs

Emacs provides some very nice abstractions for working with remote files. With emacs package ange-ftp find-file can access remote files transparently. You simply enter (for example) /ftp:<user>@<host>:/www/blog/index.php and behind the scenes emacs handles setting up an ftp connection to the remote server and downloading the file. When you save emacs uploads the file to the server. If the ftp connection has timed out then it reconnects automatically. All this is done transparently and it is almost like working with a local file. It is yet another reason to use emacs. However, sometimes that is not exactly what you want.

If you are working with an external website usually you have a development version running locally. After tweaking a page and seeing how it looks then you upload it to your remote server. Wouldn’t it be nice if you could upload a file to the correct location with a single key rather than resorting to an external program? Of course, you could do all the development remotely without too much pain using ange-ftp as described above, but working locally is faster and saves a lot of bandwidth.

ange-ftp provides some nice functions for connecting to an ftp server and assigning a password to particular user/host combination.

For a reminder on how to find useful emacs functions go here

(require 'ange-ftp)

We set up some variables for the username, hostname and password and also a variable to reference the ftp process.

(defvar *ftp-process* nil)
(defvar *ftp-user* "some-user")
(defvar *ftp-host* "ftp.some-host.com")
(defvar *ftp-password* "...")

I tried using the ftp client supplied with windows without much success so eventually I downloaded the ftp client mentioned at the ange-ftp wikipage
ftp://ftp.gnu.org/old-gnu/emacs/windows/contrib/ftp-for-win32.zip

(setq ange-ftp-ftp-program-name "c:/bin/ftp.exe")

(ange-ftp-set-passwd *ftp-host* *ftp-user* *ftp-password*)

The first function we need is one that connects to the remote server. This is a simple wrapper around ange-ftp-get-process. We check if we are already connected before trying to reconnect. How do we know if we are already connected? We could check the status of the process – if it is ‘run then the process is running. The problem with this strategy is that with many ftp-clients, when a connection is timed out, the ftp process is not disconnected until it tries to send another command. This means that we need to track timeouts ourselves.

Add a constant for how long it takes the remote server to disconnect us if there is no activity and a variable to store when we last did something.

(defconst *ftp-max-timeout* 100)

(defvar *ftp-last-action* 0)

Then we need some functions to check if we have timed out and set when we last sent a command to the server.

(defun ftp-has-timed-out-p ()
(> (- (time-to-seconds (current-time)) *ftp-last-action*)
*ftp-max-timeout*))

(defun ftp-set-last-action ()
(setq *ftp-last-action* (time-to-seconds (current-time))))

Add a predicate to check if we are connected and then we can easily implement the ftp-connect function.

(defun ftp-connected-p (proc)
(and proc
(equal (process-status proc) 'run)
(ftp-has-timed-out-p)))

(defun ftp-connect (host user)
(when (ftp-has-timed-out-p)
(ftp-kill-process host user *ftp-process*)
(sit-for 0)
(sleep-for 1))
(when (not (ftp-connected-p *ftp-process*))
(setq *ftp-process*
(ange-ftp-get-process host user))
(ange-ftp-set-binary-mode host user))
(ftp-set-last-action)
*ftp-process*)

If we have timed out then we kill the existing process, wait for the display to update with (sit-for 0) and wait for a second before trying to reconnect. You might notice we used an ftp-kill-process function that hasn’t yet been defined.

(defun ftp-kill-process (host user proc)
(when (ftp-connected-p proc)
(kill-process proc)
(setq proc nil)
(set-buffer (get-buffer (ange-ftp-ftp-process-buffer host user)))
(insert "\nTerminated"))
nil)

Finally we provide a wrapper around ange-ftp-raw-send-cmd that connects us if we are disconnected and then sets the last-action to the current time and add commands to make directories and copy files.

(defun ftp-raw-send-cmd-wrapper (cmd)
(let ((proc (ftp-connect *ftp-host* *ftp-user*)))
(ange-ftp-raw-send-cmd proc cmd)
(ftp-set-last-action))
t)

(defun ftp-copy-file (from to)
(ftp-raw-send-cmd-wrapper
(format "put %s %s" (expand-file-name from) to)))

(defun ftp-mkdir (dir)
(ftp-raw-send-cmd-wrapper (format "mkdir %s" dir)))

Hopefully you get the impression from the above code that writing elisp is similar to writing any other code – you decide on the abstractions you want and then you implement them. So why bother learning it? Firstly, there is a huge body of code that implements a lot of functionality already written in elisp. For example, if I had to implement everything that ange-ftp provides, that would have been a lot of code. Secondly, if I wrote the ftp-wrapper in perl then I would have to execute the script manually (or perhaps write an elisp binding anyway) which be more inconvenient than simply pressing a key while in my editor.

In the second part, we will see how to add some very simple functions to upload files to remote locations based on where the local file is.

The final version of the ftp-wrapper is linked from the
original version of this page.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s


%d bloggers like this: