;;-*-mode:lisp-*-
;; name:    ftp
;; purpose: convenient subroutines for handling file transfer protocol
;;
;; $Id: ftp.scm,v 1.1 1996/10/17 18:40:18 gjc Exp $

(require-so (so-ext 'regex))
(require-so (so-ext 'ss))

(define *ftp-debug* t)

(define *ftp-setup-cmds*
  '("TYPE I"))

(define (ftp-fixhostname str)
  (let ((new (string-append str ""))
	(j 0)
	(bad1 (aref "-" 0))
	(bad2 (aref "." 0))
	(good (aref "_" 0)))
    (while (< j (length new))
      (if (or (= (aref new j) bad1)
	      (= (aref new j) bad2))
	  (aset new j good))
      (set! j (+ 1 j)))
    new))

(define (ftp-get-arg host arg)
  (let ((value nil)
	(key (string-append (ftp-fixhostname host) "_" arg)))
    (cond ((and (set! value (getenv key))
		(> (length value) 0))
	   value)
	  ('else
	   (cond ((and (equal? arg "PASS")
		       (symbol-bound? 'getpass))
		  (fflush)
		  (set! value (getpass (string-append key ": "))))
		 ('else
		  (writes nil key ": ")
		  (fflush)
		  (set! value (readline))))
	   (if value (putenv (string-append key "=" value)))
	   value))))

(define (ftp-setup host set)
  (let ((c (set (ftp-open-control host)))
	(result nil))
    (set! result (ftp-cmd-login c
				(lambda () (ftp-get-arg host "USER"))
				(lambda () (ftp-get-arg host "PASS"))
				(lambda () (ftp-get-arg host "ACCT"))))
    (if (or (not result)
	    (= 5 (trunc (/ (car result) 100))))
	(error "ftp-cmd-login" (cadr result)))
    (mapcar (lambda (x)
	      (ftp-cmd c x))
	    *ftp-setup-cmds*)))

(define (ftp-teardown c set)
  (ftp-close-control c)
  (set nil))

(define (ftp.cp host1 host2 cmds)
  (let ((c1 nil)
	(c2 nil))
    (define (setc1 v) (set! c1 v))
    (define (setc2 v) (set! c2 v))
    (*catch 'errobj
	    (ftp-setup host1 setc1)
	    (ftp-setup host2 setc2)
	    (mapcar (lambda (x) 
		      (ftp-pasv-copy c1
				     (nth 0 x)
				     c2
				     (nth 2 x)
				     (nth 1 x)))
		    cmds)
	    (ftp-teardown c1 setc1)
	    (ftp-teardown c2 setc2))
    (and c1 (s-close c1))
    (and c2 (s-close c2))))

(define (ftp-send-command c . args)
  (define flag nil)
  (cond (*ftp-debug*
	 (writes nil "" c "---> ")))
  (mapcar (lambda (str)
	    (cond ((not *ftp-debug*))
		  (flag
		   (writes nil "XXXXXXXXXXXXXX"))
		  ('else
		   (writes nil str)
		   (if (equal? str "PASS ") (set! flag t))))
	    (s-puts str c))
	  args)
  (cond (*ftp-debug*
	 (writes nil "\n")
	 (fflush)))
  (s-puts "\r\n" c)
  (s-force-output c))

(define *ftp-response-pat* (regcomp "^([0-9]+)(\-| |$)(.*)$" REG_EXTENDED))

(define (ftp-read-response c)
  (s-force-output c)
  (if *ftp-debug*
      (writes nil c " "))
  (let ((line (s-gets c))
	(match nil)
	(code nil)
	(continue t))
    (cond (*ftp-debug*
	   (if line (writes nil line) (writes nil "*EOF* on command\n"))
	   (fflush)))
    (cond ((not line)
	   nil)
	  ((not (pair? (set! match (regexec *ftp-response-pat* line))))
	   (error "bad response format"))
	  ('else
	   (set! code (string->number (substring line
						 (car (nth 1 match))
						 (cdr (nth 1 match)))))
	   (cond ((not (equal? "-" (substring line
					       (car (nth 2 match))
					       (cdr (nth 2 match)))))
		  (list code
			(substring line
				   (car (nth 3 match))
				   (cdr (nth 3 match)))))
		 ('else
		  (while (and continue
			      (set! line (s-gets c)))
		    (cond (*ftp-debug*
			   (if line (writes nil line)
			     (writes nil "*EOF* on command\n"))
			   (fflush)))
		    (cond ((not line)
			   (set! continue nil))
			  ((not (pair? (set! match (regexec *ftp-response-pat*
							    line)))))
			  ((and (equal? code
					(string->number
					 (substring line
						    (car (nth 1 match))
						    (cdr (nth 1 match)))))
				(not (equal? "-"
					     (substring line
							(car (nth 2 match))
							(cdr (nth 2 match))))))
			   (set! continue nil))))
		  (list code
			(if (pair? match)
			    (substring line
				       (car (nth 3 match))
				       (cdr (nth 3 match)))))))))))
(define (ftp-open-control host)
  (let ((c (s-open host "ftp")))
    (ftp-read-response c)
    c))

(define (ftp-cmd c . args)
  (apply ftp-send-command (cons c args))
  (ftp-read-response c))
    
(define (ftp-close-control c)
  (ftp-cmd c "QUIT")
  (s-close c))

(define *ftp-pasv-pat*
  (regcomp
   "([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+)" REG_EXTENDED))

(define (ftp-cmd-pasv c)
  (let ((res (ftp-cmd c "PASV"))
	(match nil))
    (if (not (and (pair? res) (= 227 (car res))))
	(error "PASV command failed" res))
    (if (and (cadr res)
	     (pair? (set! match (regexec *ftp-pasv-pat* (cadr res)))))
	(mapcar (lambda (x)
		  (substring (cadr res) (car x) (cdr x)))
		(cdr match)))))

(define (ftp-cmd-login c username . args)
  (define (str v)
    (if (or (not v) (string? v))
	v
      (v)))
  (let ((res nil))
    (cond ((or (not (set! res (ftp-cmd c "USER " (str username))))
	       (not (= 331 (car res))))
	   res)
	  ((or (not (set! res (ftp-cmd c "PASS " (str (car args)))))
	       (not (= 332 (car res))))
	   res)
	  ('else
	   (ftp-cmd c "ACCT " (str (cadr args)))))))

(define (ftp-pasv-copy c1 file1 c2 file2 direction)
  (or (memq direction '(-> <-))
      (error "invalid direction specification" direction))
  (let ((p (or (ftp-cmd-pasv c1)
	       (error "pasv command failed to return port info")))
	(res1 nil)
	(res2 nil))
    (set! res2 (ftp-cmd c2
			"PORT "
			(nth 0 p) "," 
			(nth 1 p) "," 
			(nth 2 p) "," 
			(nth 3 p) "," 
			(nth 4 p) "," 
			(nth 5 p)))
    (or (and (pair? res2) (= 200 (car res2)))
	(error "PORT command failed" res2))
    (cond ((eq? direction '->)
	   (ftp-send-command c1 "RETR " file1)
	   (set! res2 (ftp-cmd c2 "STOR " file2)))
	  ('else
	   (ftp-send-command c1 "STOR " file1)
	   (set! res2 (ftp-cmd c2 "RETR " file2))))
    (set! res1 (ftp-read-response c1))
    (if (and (pair? res1) (= (car res1) 150))
	(set! res1 (ftp-read-response c1)))
    (if (and (pair? res2) (= (car res2) 150))
	(set! res2 (ftp-read-response c2)))
    (list res1 res2)))

