ring form 上传文件

最近需要form表单提交文件,text复合内容(即multipart/form-data),首先添加其中间件wrap-multipart-params函数,github Wiki上有写,加入之后会发现request 多了个multipart-params的K-V对
例如 :multipart-params {“user” “kimmy”, “up” {:size 261556, :tempfile #, :content-type “image/jpeg”, :filename “A{@D}%])VN6)QCF0}}QM(CI.jpg”}}。
——————————-说点别的————————-
1.如果用的是Compojure,这样获取request

(POST "/upload" request
		(println request))

2.用我写的这个csp-compiler调试很方便,可以直接像jsp调试java代码一样 实时调试输出clj改动响应结果(得瑟一下)
—————————————————————

回到正题,如果用官方的这样得到的:multipart-params,是使用默认的临时文件保存方法,其功能就是把文件们写到临时文件下,并且开启后台线程检测超过某个过期时间,就会删除其。
但是我现在的需求是永久的保存,并且不想浪费这个资源去开线程检测它有没有过期,默认方法是做不到的(它只能传入过期时间的参数,线程照开),所以找到代码

(defn multipart-params-request
  "Adds :multipart-params and :params keys to request.
  See: wrap-multipart-params."
  {:arglists '([request] [request options])
   :added "1.2"}
  [request & [options]]
  (let [store    (or (:store options) @default-store)
        encoding (or (:encoding options)
                     (req/character-encoding request)
                     "UTF-8")
        params   (if (multipart-form? request)
                   (parse-multipart-params request encoding store)
                   {})]
    (merge-with merge request
                {:multipart-params params}
                {:params params})))

核心代码如上,如果想自己写的话,就把函数作为:store的值塞到一个option的map里面,然后改下middleware的参数,这里的upload/file-store参照默认函数temp_file.clj改写的

(-> routes
      wrap-keyword-params
      wrap-nested-params
      wrap-params
      (wrap-multipart-params {:store (upload/file-store)} ))

最后注意,改写的函数是个闭包函数,执行后才返回一个干正事的函数。

csp-compiler Info

自己模仿了jsp做了个csp的编译工具,原理也挺简单,就是将页面静态内容字符输出,动态内容使用<` `>转义执行,最后写成clj文件,再编译。
项目在csp-compiler使用只要 (csp “PATH/XXX.csp”)即可

处理逻辑如下。首先根据csp 函数的file参数找到文件,生成一个该文件名的一个Key(也是其生成clj文件的命名空间)值,并记录下上次修改时间,最新编译时间,文件路径,命名空间,等相关参数作为Value 塞入 *csp-ns-map*这个atom的map(考虑到并发的问题) 每次调用的时候比较时间戳 如果最近有修改的话重新编译该csp,并载入该命名空间,否则直接调用该输出函数。最后剩下的都是一些硬编码,怎么样高效的区分动,静态部分,并作相应的转义。

碰到的问题
1.命名空间的问题(包括其他的ns问题统一整理后再发)
2. 路径问题,在repl中可以访问的,到服务器Wepapps下不行(使用IO/resource 解决)

ring server(jetty)启动

上次 lein ring server 谈到下面那个核心启动方法

(defn start-server-expr [project]
  `(ring.server.leiningen/serve '~(select-keys project [:ring])))

可以看到 又去引用 ring.server.leiningen serve 方法,那么会正式加载(require)所需的命名空间,及找到 handler、init、destroy函数变量,最后传入standalone/serve 方法,如下:

(ns ring.server.standalone
  "Functions to start a standalone Ring server."
  (:use ring.adapter.jetty
        ring.server.options
        ring.middleware.stacktrace
        ring.middleware.reload
        ring.middleware.refresh
        [clojure.java.browse :only (browse-url)]))

(defn- try-port
  "Try running a server under one port or a list of ports. If a list of ports
  is supplied, try each port until it succeeds or runs out of ports."
  [port run-server]
  (if-not (sequential? port)
    (run-server port)
    (try (run-server (first port))
         (catch java.net.BindException ex
           (if-let [port (next port)]
             (try-port port run-server)
             (throw ex))))))

(defn server-port
  "Get the port the server is listening on."
  [server]
  (-> (.getConnectors server)
      (first)
      (.getPort)))

(defn server-host
  "Get the host the server is bound to."
  [server]
  (-> (.getConnectors server)
      (first)
      (.getHost)
      (or "localhost")))

(defn- open-browser-to [server options]
  (browse-url
   (str "http://" (server-host server) ":" (server-port server) (browser-uri options))))

(defmacro ^{:private true} in-thread
  "Execute the body in a new thread and return the Thread object."
  [& body]
  `(doto (Thread. (fn [] ~@body))
     (.start)))

(defn- add-destroy-hook [server destroy]
  "Add a destroy hook to be executed when the server ends."
  (in-thread
   (try (.join server)
        (finally (if destroy (destroy))))))

(defn- add-stacktraces [handler options]
  (if (stacktraces? options)
    ((or (:stacktrace-middleware options)
         wrap-stacktrace) handler)
    handler))

(defn- add-auto-reload [handler options]
  (if (auto-reload? options)
    (wrap-reload handler {:dirs (reload-paths options)})
    handler))

(defn- add-auto-refresh [handler options]
  (if (:auto-refresh? options)
    (wrap-refresh handler)
    handler))

(defn- add-middleware [handler options]
  (-> handler
      (add-auto-refresh options)
      (add-auto-reload options)
      (add-stacktraces options)))

(defn serve
  "Start a web server to run a handler. Takes the following options:
    :port                  - the port to run the server on
    :join?                 - if true, wait for the server to stop
    :init                  - a function to run before the server starts
    :destroy               - a function to run after the server stops
    :open-browser?         - if true, open a web browser after the server starts
    :browser-uri           - the path to browse to when opening a browser
    :stacktraces?          - if true, display stacktraces when an exception is thrown
    :stacktrace-middleware - a middleware that handles stacktraces
    :auto-reload?          - if true, automatically reload source files
    :reload-paths          - seq of src-paths to reload on change - defaults to [\"src\"]
    :auto-refresh?         - if true, automatically refresh browser when source changes
  If join? is false, a Server object is returned."
  {:arglists '([handler] [handler options])}
  [handler & [{:keys [init destroy join?] :as options}]]
  (let [options (assoc options :join? false)
        destroy (if destroy (memoize destroy))
        handler (add-middleware handler options)]
    (if init (init))
    (if destroy
      (. (Runtime/getRuntime)
         (addShutdownHook (Thread. destroy))))
    (try-port (port options)
      (fn [port]
        (let [options (merge {:port port} options)
              server  (run-jetty handler options)
              thread  (add-destroy-hook server destroy)]
          (println "Started server on port" (server-port server))
          (if (open-browser? options)
            (open-browser-to server options))
          (if join?
            (.join thread))
          server)))))

可以看到首先初始化、Runtime挂上结束回调函数(包括下面的,add-destroy-hook 也是等待(join)server 线程结束回调)。(run-jetty handler options)方法是没有被block的 ,因为options里的赋了 :join? false,目的为了主线程得到返回实例化的server之后启动浏览器,然后再挂起阻塞(join)。

至此 所有的运行都交由ring来管理,run-jetty 方法是其jetty的适配器;重载等方法也用的是其中间件reload、refresh、stacktrace。

lein ring server 浅谈

在lepl中 “lein ring server”,启动ring 服务,下面浅谈其调用过程。

lein ring server是个lein 插件调用(lein.ring) 而后根据参数server 调用->(leiningen.ring.server) 这个方式之前的文章也讲过。

;leiningen.ring.server部分代码如下:
(defn load-namespaces
  "Create require forms for each of the supplied symbols. This exists because
  Clojure cannot load and use a new namespace in the same eval form."
  [& syms]
  `(require
    ~@(for [s syms :when s]
        `'~(if-let [ns (namespace s)]
             (symbol ns)
             s))))
(defn start-server-expr [project]
  `(ring.server.leiningen/serve '~(select-keys project [:ring])))
(defn server-task
  "Shared logic for server and server-headless tasks."
  [project options]
  (ensure-handler-set! project)
  (let [project (-> project
                    (assoc-in [:ring :reload-paths] (reload-paths project))
                    (update-in [:ring] merge options))]
    (eval-in-project
     (-> project add-server-dep add-optional-nrepl-dep)
     (if (nrepl? project)
       `(do ~(start-nrepl-expr project) ~(start-server-expr project))
       (start-server-expr project))
     (apply load-namespaces
            (conj (into
                   ['ring.server.leiningen
                    (if (nrepl? project) 'clojure.tools.nrepl.server)]
                   (if (nrepl? project) (nrepl-middleware project)))
                  (-> project :ring :handler)
                  (-> project :ring :init)
                  (-> project :ring :destroy))))))

(defn server
  "Start a Ring server and open a browser."
  ([project]
     (server-task project {}))
  ([project port]
     (server-task project {:port (Integer. port)})))

从(leiningen.ring.server) 的server方法开始说起
前面提取project.clj :ring 值(包含handler xxx)
之后nrepl? 判读存在 :nrepl 值 即是否自定义端口(:nrepl – A map of :start? and (optionally) :port keys. If :start? is true, open up an nREPL server on the given port. :start? defaults to false, :port defaults to an arbitrary free port.),这个无关痛痒。

(start-server-expr project) 将ring的启动代码加入,不是执行。其中ring.server.leiningen/serve 是ring启动的句柄,放到下一篇《ring 启动》再说
然后load-namespaces 加载所需的ns 同样返回的是symbol

最后使用eval-in-project(用的是leinjacker) 其函数定义如下:

(defn eval-in-project
  "Support eval-in-project for both Leiningen 1.x and 2.x.  This code is
  inspired from the code in the lein-swank plug-in."
  [project form init])

可以猜出它大致是初始化namespace,适配lein project 执行form的。

至此,整个lein 启动ring server初步流程就是这样,若要详细跟进,可以看github的源码。

leinigen 与 插件

leinigen 是clojure的项目管理器 类似于maven,抛开它不讲 本身clojure 也可以启动
启动懂clojure 只需要它的jar 即可 使用“java -cp clojure-1.6.0.jar:$CLASSPATH clojure.main”进入repl, *****注意 不加 $CLASSPATH classpath会出错
同样 加入参数“ -m XXX.XX” 就会运行该namespace 下面的 -main函数。

那么lein 又是怎么运行的呢,首先 从官网可以看到 下载下来的只是一个shell文件,也就是所有的lein repl,lein deps 都是先走这个脚本的,然后在运行lein 本身的-main 函数(如下 来自于lein.sh)

export TRAMPOLINE_FILE
"$LEIN_JAVA_CMD" \
"${BOOTCLASSPATH[@]}" \
-Dfile.encoding=UTF-8 \
-Dmaven.wagon.http.ssl.easy=false \
-Dmaven.wagon.rto=10000 \
$LEIN_JVM_OPTS \
-Dleiningen.original.pwd="$ORIGINAL_PWD" \
-Dleiningen.script="$SCRIPT" \
-classpath "$CLASSPATH" \
clojure.main -m leiningen.core.main "$@"

进入 lein -main 函数之后 一切就由lein 来管理了 github 上可以看到 每一个命令 都有其对应的一个clj 文件做处理,并且还支持插件.
如果自己做插件的话 leinigen 根据命名空间来找 插件入口的

(ns leinigen.fly)
(defn fly
"test leinige"
([project]
(println "fly" project))
([project port]
(prinlnt "fly with port" project port)))

并且在project.clj 加入 :eval-in-leiningen true
(如有需求 可以打包上传到clojars 然后 其他项目就可以 :plugins [[XXXXX “XXXX”]] 直接使用了)
命令行直接就可以 输入 lein fly 输出 需要指定的内容了。