Every now and then I find myself needing to serve a directory over HTTP - for instance, to quickly transfer files to another machine in the same LAN (especially when that machine turns out to be a phone).
To accomplish that, I used to run Python's
SimpleHTTPServer directly from Fish shell, wrapped in a helper function:
function serve_this; python -m SimpleHTTPServer; end
I'm in process of migrating more and more of my workflow to Emacs - including file management and shell operations - so I naturally started to wonder, how to implement that particular utility in my favorite operating system.
Serving files from Emacs
Emacs is (obviously) perfectly capable of running a HTTP server, and someone (obviously) wrote a package for it - named, appropriately,
Since it's available on ELPA (the default Emacs package repository), it was a good choice to base the implementation on.
The following is the code for basic implementation of the utility, ready to be copied into your config. Further on, I discuss it in more details, and follow up with aesthetic improvements and eshell integration. The complete code with all the improvements can be assembled from pieces posted here, or taken from this gist.
(Note that the code depends on
web-server - ensure it's installed and loaded before evaluating this source block. Myself, I just
put it in the
:config section of
(defvar my/file-server nil "Is the file server running? Holds an instance if so.") (defalias 'my/send-directory-list 'ws-send-directory-list) (defun my/ws-start (handlers port &optional log-buffer &rest network-args) "Like `ws-start', but unbroken for Emacs 25+." (let ((server (make-instance 'ws-server :handlers handlers :port port)) (log (when log-buffer (get-buffer-create log-buffer)))) (setf (process server) (apply #'make-network-process :name "ws-server" :service (port server) :filter 'ws-filter :server t :nowait nil :family 'ipv4 :coding 'no-conversion :plist (append (list :server server) (when log (list :log-buffer log))) :log (when log (lambda (proc request message) (let ((c (process-contact request)) (buf (plist-get (process-plist proc) :log-buffer))) (with-current-buffer buf (goto-char (point-max)) (insert (format "%s\t%s\t%s\t%s" (format-time-string ws-log-time-format) (first c) (second c) message)))))) network-args)) (push server ws-servers) server)) (defun my/serve-directory (directory port) (interactive "DDirectory: \nnPort: ") ;; Based on http://eschulte.github.io/emacs-web-server/File-Server.html#File-Server. (if my/file-server (message "File server is already running!") (progn (setf my/file-server (lexical-let ((docroot directory)) (my/ws-start (lambda (request) (with-slots (process headers) request (let* ((path (substring (cdr (assoc :GET headers)) 1)) (expanded (ws-in-directory-p docroot path))) (if (and expanded (file-exists-p expanded)) (if (file-directory-p expanded) (my/send-directory-list process expanded) (ws-send-file process (expand-file-name path docroot))) (ws-send-404 process))))) port nil ;no log buffer :host "0.0.0.0"))) (message "Serving directory %s on port %d" directory port)))) (defun my/stop-server () "Stop the file server if running." (interactive) (if my/file-server (progn (ws-stop my/file-server) (setf my/file-server nil) (message "Stopped the file server.")) (message "No file server is running.")))
Evaluating the above code is enough to get the basic functionality, as presented below:
Serving directories is as simple as running
M-x my/serve-directory, and specifying directory
and server port. Stopping the server can be accomplished with
web-server package doesn't work correctly by default under Emacs 26 (and possibly 25). This is due to apparent
bug in handling of calls to
make-network-process (see here, here). The simplest way to patch it was to copy the original
ws-start function and modify the
make-network-process call to use
nil as the value of
If you're using Emacs 25 and
:nowait nil works for you, consider removing the
my/ws-start function and using
ws-start function instead.
(Note that if only
ws-start prepended its
network-args list in the
apply call instead of putting it at the end,
I would be able to override the
:nowait value by just passing it as an argument. Alas, I had to fix the entire
Sending the directory listing
The HTTP handler passed as an argument to
my/ws-start uses the path passed in the request to determine whether
the target resource is a file or directory. To send a directory, the code above uses another function from the
ws-send-directory-list (through the alias
my/send-directory-list). Let's look at that function's signature:
(defun ws-send-directory-list (proc directory &optional match) "Send a listing of files in DIRECTORY to PROC. Optional argument MATCH is passed to `directory-files' and may be used to limit the files sent." ...)
The docstring tells us about the
match argument being passed to
directory-files Emacs function. From documentation:
If MATCH is non-nil, mention only file names that match the regexp MATCH.
This argument can be used to filter out files and directories from the returned listing. For instance,
the example file server implementation I'm building on uses
"^[^\.]" to filter out the
and (possibly unintentionally) dotfiles. In my implementation I left it out, however, to allow the improved
directory listing code (below) to filter out
Prettifying the file list
Let's compare that with the default output of Python's
Pythons is definitely prettier. Fortunately, it's simple to improve on the code above, and also add some extra useful information - like sizes and last modification dates for each entry. This requires writing a custom implementation for outputting directory listing.
(defalias 'my/send-directory-list 'ws-send-directory-list) with
the following function:
(defun my/send-directory-list (proc directory &optional match) "Send a listing of files in DIRECTORY to PROC. Optional argument MATCH is passed to `directory-files' and may be used to limit the files sent." (ws-response-header proc 200 (cons "Content-type" "text/html")) (process-send-string proc (concat ;; header "<!DOCTYPE html>\n" "<html><head><title>Directory listing for " directory "</title></head><body>" "<h2>Directory listing for <tt>" directory "</tt></h2>" "<table>" ;; "Up one level" link "<tr><td></td><td></td><td><a href='../'>Up one level</a></td></tr>" ;; Actual directory listing (mapconcat (lambda (f-and-attr) (let* ((name (first f-and-attr)) (attr (rest f-and-attr)) (full (expand-file-name name directory)) (end (if (file-directory-p full) "/" "")) (url (url-encode-url (concat name end))) (modtime (format-time-string "%Y-%m-%d %T %z" (file-attribute-modification-time attr))) (size (file-attribute-size attr))) (format "<tr><td><small>%s</small></td><td><small>%s</small></td><td><a href='%s'>%s%s</a></td></tr>" modtime size url name end))) (remove-if (lambda (entry) (or (string= (car entry) ".") (string= (car entry) ".."))) (directory-files-and-attributes directory nil match)) "\n") ;; Footer "</table></body></html>")))
Previously, I mentioned that
web-server's implementation uses
directory-files function to list files. The improved code
uses its sibling -
directory-files-and-attributes - which returns entries in the form of
(name . attributes). Compare:
(directory-files "/tmp/test-directory") ;; -> ("." ".." ".emacs" ".gitconfig" "a-subdirectory") (directory-files-and-attributes "/tmp/test-directory") ;; -> (("." t 3 1000 1000 (23305 35088 307349 257000) (23305 35068 419401 610000) (23305 35068 419401 610000) 4096 "drwxrwxr-x" t 5636571 2065) (".." t 25 0 0 (23305 34969 731662 280000) (23305 35020 155528 902000) (23305 35020 155528 902000) 12288 "drwxrwxrwt" t 5636097 2065) (".emacs" "/home/temporal/repos/conffiles/emacs25/.emacs" 1 1000 1000 (23305 35053 639440 555000) (23305 35053 635440 565000) (23305 35053 635440 565000) 45 "lrwxrwxrwx" t 5636573 2065) (".gitconfig" nil 1 1000 1000 (23304 15357 454324 784000) (23151 13649 999692 371000) (23305 35068 419401 610000) 277 "-rw-r--r--" t 5636575 2065) ("a-subdirectory" t 2 1000 1000 (23305 35025 19516 58000) (23305 35025 19516 58000) (23305 35025 19516 58000) 4096 "drwxrwxr-x" t 5636572 2065))
directory-files-and-attributes returns plenty of useful attributes - including access/modification/status change times,
permission strings in format like
ls -l, and an indication whether an entry is a file, directory, or a symbolic link. Emacs also defines
helper functions for convenient access to returned attributes - see
M-x apropos file-attribute-* for a list.
Convenience and integrating with eshell
Ultimately, I wanted to replicate the convenience of typing
serve_this in the shell. Nowadays, I do most of my shell work in
eshell, which has a very convenient feature - any elisp function with a name starting with
eshell/foo) is available in eshell
as a command (e.g.
foo). The code below uses this to implement serving current directory and stopping the server as eshell
serve-this can accept an optional number, if you want to pick the port on
which to serve files.
The code also implements a
M-x serve-this interactive command for easy access from anywhere within Emacs.
(defun my/serve-this (port) "Start a file server on a `PORT', serving the content of directory associated with the current buffer's file." (interactive "nPort: ") (my/serve-directory (if (buffer-file-name) (file-name-directory (buffer-file-name)) (expand-file-name default-directory)) port)) ;; Eshell utilities. (defconst my/default-directory-server-port 8123) (defun eshell/serve-this (&optional port) (my/serve-this (or port my/default-directory-server-port))) (defalias 'eshell/stop-server 'my/stop-server)
That's it! A complete and relatively nice-looking solution for serving directories and files over HTTP, written entirely within Emacs!
The above solution is pretty much complete, and it's enough for my typical usage patterns. I have, however, some potential ideas for improvements.
my/ws-start both accept an optional
log-buffer argument. It can be used to make the server
process use (or create) a new buffer, in which it'll log requests. I don't need it, but it's trivial to modify
to make use of it. The argument can be a buffer object, or a string - if that string doesn't name an existing buffer, a new
buffer will be created. This buffer probably doesn't need undo functionality, so you may want to consider passing a string
starting with space - per documentation of
get-buffer-create, a new buffer with such name will not keep undo information.
Yet another feature that I personally don't need, but if you wanted, you could make
on a list or hash table of server instances, keyed by port number, instead of sharing the
my/file-server global variable.