Permalink
Browse files

adding tests and a few fixes

  • Loading branch information...
0 parents commit 6475a7042c87df06cebb240f0d67fcb8add8e606 @miner committed Jun 3, 2012
Showing with 182 additions and 0 deletions.
  1. +10 −0 .gitignore
  2. +23 −0 README.md
  3. +7 −0 project.clj
  4. +115 −0 src/miner/ftp.clj
  5. +27 −0 test/miner/ftp_test.clj
@@ -0,0 +1,10 @@
+/target
+/lib
+/classes
+/checkouts
+pom.xml
+*.jar
+*.class
+.lein-deps-sum
+.lein-failures
+.lein-plugins
@@ -0,0 +1,23 @@
+# clj-ftp
+
+Wrapper over Apache Commons Net to provide easy access from Clojure.
+
+Note: FTP is considered insecure. Data and passwords are sent in the
+clear so someone could sniff packets on your network and discover
+your password. However, FTP access is useful for dealing with anonymous
+FTP servers and situations where security is not an issue.
+
+
+## Usage
+
+ (require '[miner.ftp :as ftp])
+
+ (ftp/with-ftp [client "ftp://anonymous:pwd@ftp.example.com/pub"]
+ (ftp/client-get client "interesting.txt" "stuff.txt"))
+
+
+## License
+
+Copyright © 2012 Stephen E. Miner
+
+Distributed under the Eclipse Public License, the same as Clojure.
@@ -0,0 +1,7 @@
+(defproject clj-ftp "0.1.0"
+ :description "Clojure wrapper on Apache Commons Net for FTP"
+ :url "http://github.com/miner/clj-ftp"
+ :license {:name "Eclipse Public License"
+ :url "http://www.eclipse.org/legal/epl-v10.html"}
+ :dependencies [[org.clojure/clojure "1.4.0"]
+ [fs "1.0.0"] [commons-net "3.1"]])
@@ -0,0 +1,115 @@
+
+;; Latest Commons Net Update:
+;; http://commons.apache.org/net/api-3.1/org/apache/commons/net/ftp/FTPClient.html
+
+;; Uses Apache Commons Net 3.1. Does not support SFTP.
+
+;; FTP is considered insecure. Data and passwords are sent in the
+;; clear so someone could sniff packets on your network and discover
+;; your password. However, FTP access is useful for dealing with anonymous
+;; FTP servers and situations where security is not an issue.
+
+(ns miner.ftp
+ (:import (org.apache.commons.net.ftp FTP FTPClient FTPFile FTPReply)
+ (java.net URL)
+ (java.io File IOException))
+ (:require [fs.core :as fs]
+ [clojure.java.io :as io]))
+
+(defn open [url]
+ (let [^FTPClient client (FTPClient.)
+ ^URL url (io/as-url url)]
+ (.connect client
+ (.getHost url)
+ (if (= -1 (.getPort url))
+ (.getDefaultPort url)
+ (.getPort url)))
+ (let [reply (.getReplyCode client)]
+ (if (not (FTPReply/isPositiveCompletion reply))
+ (do (.disconnect client)
+ ;; should log instead of println
+ (println "Connection refused")
+ nil)
+ client))))
+
+(defmacro with-ftp [[client url & extra-bindings] & body]
+ `(let [u# (io/as-url ~url)
+ ^FTPClient ~client (open u#)
+ ~@extra-bindings]
+ (when ~client
+ (try
+ (if (.getUserInfo u#)
+ (let [[^String uname# ^String pass#] (.split (.getUserInfo u#) ":" 2)]
+ (.login ~client uname# pass#)))
+ (.changeWorkingDirectory ~client (.getPath u#))
+ (.setFileType ~client FTP/BINARY_FILE_TYPE)
+ ~@body
+ (catch IOException e# (println "Error:" (.getMessage e#)) nil)
+ (finally (when (.isConnected ~client)
+ (try
+ (.disconnect ~client)
+ (catch IOException e2# nil))))))))
+
+
+(defn client-list-all [client]
+ (map #(.getName ^FTPFile %) (.listFiles client)))
+
+(defn client-list-files [client]
+ (map #(.getName ^FTPFile %) (filter #(.isFile ^FTPFile %) (.listFiles client))))
+
+(defn client-list-directories [client]
+ (map #(.getName ^FTPFile %) (filter #(.isDirectory ^FTPFile %) (.listFiles client))))
+
+(defn client-get
+ "Get a file (must be within a with-ftp)"
+ ([client fname] (client-get client fname (fs/base-name fname)))
+
+ ([client fname local-name]
+ (with-open [outstream (java.io.FileOutputStream. (io/as-file local-name))]
+ (.retrieveFile ^FTPClient client ^String fname ^java.io.OutputStream outstream))))
+
+(defn client-put
+ "Put a file (must be within a with-ftp)"
+ ([client fname] (client-put client fname (fs/base-name fname)))
+
+ ([client fname remote] (with-open [instream (java.io.FileInputStream. (io/as-file fname))]
+ (.storeFile ^FTPClient client ^String remote ^java.io.InputStream instream))))
+
+(defn client-cd [client dir]
+ (.changeWorkingDirectory ^FTPClient client ^String dir))
+
+(defn client-pwd [client]
+ (.printWorkingDirectory ^FTPClient client))
+
+(defn client-mkdir [client subdir]
+ (.makeDirectory ^FTPClient client ^String subdir))
+
+;; Regular mkdir can only make one level at a time; mkdirs makes nested paths in the correct order
+(defn client-mkdirs [client subpath]
+ (doseq [d (reductions (fn [path item] (str path File/separator item)) (fs/split subpath))]
+ (client-mkdir client d)))
+
+(defn client-delete [client fname]
+ "Delete a file (must be within a with-ftp)"
+ (.deleteFile ^FTPClient client ^String fname))
+
+;; convience methods for one-shot results
+
+(defn retrieve-file
+ ([url fname] (retrieve-file url fname (fs/base-name fname)))
+ ([url fname local-file]
+ (with-ftp [client url]
+ (client-get client fname (io/as-file local-file)))))
+
+(defn list-all [url]
+ (with-ftp [client url]
+ (client-list-all url)))
+
+(defn list-files [url]
+ (with-ftp [client url]
+ (client-list-files client)))
+
+(defn list-directories [url]
+ (with-ftp [client url]
+ (client-list-directories client)))
+
@@ -0,0 +1,27 @@
+(ns miner.ftp-test
+ (:use clojure.test
+ miner.ftp)
+ (:require [fs.core :as fs]))
+
+(deftest listing
+ (is (pos? (count (list-files "ftp://anonymous:user%40example.com@ftp.gnu.org/gnu/emacs")))))
+
+(deftest retrieve-file-one-shot
+ (let [tmp (fs/temp-file "ftp-")]
+ (retrieve-file "ftp://anonymous:user%40example.com@ftp.gnu.org/gnu/emacs" "README.otherversions" tmp)
+ (is (fs/exists? tmp))
+ (when (fs/exists? tmp)
+ (fs/delete tmp))))
+
+(deftest get-file-client
+ (let [tmp (fs/temp-file "ftp-")]
+ (with-ftp [client "ftp://anonymous:user%40example.com@ftp.gnu.org/gnu/emacs"]
+ (client-cd client "..")
+ (is (.endsWith (client-pwd client) "gnu"))
+ (is (pos? (count (client-list-all client))))
+ (client-cd client "emacs")
+ (is (.endsWith (client-pwd client) "emacs"))
+ (client-get client "README.otherversions" tmp))
+ (is (fs/exists? tmp)
+ (when (fs/exists? tmp)
+ (fs/delete tmp)))))

0 comments on commit 6475a70

Please sign in to comment.