cljs-uploader

0.1.0


Example app. Explains how upload files using ajax and Clojurescript

dependencies

org.clojure/clojure
1.5.1
domina
1.0.2
compojure
1.1.5
ring
1.1.8



(this space intentionally left almost blank)
 
(ns cljs-uploader.server
  (:require [ring.adapter.jetty :as jetty] 
            [ring.middleware.resource :as resources]
            [ring.middleware.multipart-params :as mp] 
            [ring.util.response :as response] 
            [clojure.java.io :as io] 
            [compojure.core :refer :all] 
            [compojure.route :as route])
  (:gen-class))

Создадим главную страницу, на странице у нас будет находится одна форма с полем для ввода текста и полем для отправки файла.

Также, кнопку для того что-бы можно было отправить форму на сервер.

Подключим сгенерированный js и вызовем метод который запустит наш клиентский код.

(def html-page 
"<!DOCTYPE html>
<html>
  <head>
    <link rel='stylesheet' 
       href='css/page.css' />
  </head>
  <body>
  <div> 
    <form id='form' 
      enctype='multipart/form-data' method='POST'>
      Username: <input name='username' type='text' > 
      <br/><br/>
      <input name='userfile' type='file' /> 
         <br/><br/>
      <input type='button' id='upload-button'
              value='Upload'  />
     </form>
  </div>
  </body>
  <script src='js/cljs.js'></script>
  <script > 
    cljs_uploader.client.init();
  </script>
</html>")

Приватная функция, проверяем что x не nil

(defn- not-nil [x] ((complement nil?) x))

Еще одна приватная функция, проверяет что строка не nil и не пустая

(defn- not-blank [x] ((complement clojure.string/blank?) x))

Функция которая отвечает за сохранение файла на сервере, принимает имя пользователя (которое должно прийти с формы) и мапу с информацией о файле.

В прекондишенах проверяем что имя пользователь не пустое (и не nil) и что информация о файле не nil

Выводим в консоль информацию о пользователе и файле который он отправил (в реальном приложении должно быть наверное что-то более интеллектуальное)

Создадим папку в которую будем класть файлы от пользователей (папка создается только один раз). В принципе проверку на существование папки можно было не делать а просто вызвать (.mkdir (io/file "uploaded")) эффект будет одинаковый, mkdir не должен создавать папку если она уже существует. Но для наглядности я решил добавить проверку.

Скопируем содержимое tmpf в файл с именем fname

Если все прошло успешно - возвращаем строку 'ok'. Если что то пройдет не так ring вернет html c кодом ошибки и ексепшеном

(defn save-file 
  [username {tmpf :tempfile fname :filename}] 
  ;; tmpf - временный файл в который веб  
  ;;        сервер сохранит файл отправленный
  ;;        юзером с формы 
  ;;
  ;; fnmae - название файла 
  ;;         отправленного юзером
  {:pre [(not-blank username)
         (not-nil tmpf) 
         (not-blank fname)]}
  (println "Received " fname " from " username)
  (when (not (.exists (io/file "uploaded"))) 
    (.mkdir (io/file "uploaded")))
  (io/copy tmpf (io/file "uploaded" fname))
  "ok")

Спецификация путей.

GET / - отдаем html-page

POST '/upload' - загружаем файл.

Для этого handler нужно обернуть в mp/wrap-multipart-params

В userfile будет содержатся информация о файле, примерно вот такая:

{:size 15,
 :tempfile #<File /var/folders/ring-multipart-10.tmp>, 
 :content-type text/plain, 
 :filename New Text Document.txt}

Все остальные запросы GET /* будет мапить на содержимое из ресурсов, для загрузки CSS и JS

(defroutes app-routes 
  (GET "/" [] html-page)
  (mp/wrap-multipart-params
    (POST "/upload" [username userfile] (save-file username userfile)))
  (route/resources "/"))
(defn -main [& args]
  (jetty/run-jetty app-routes {:port 3000}))
 
(ns cljs-uploader.client 
  (:import goog.net.IframeIo)
  (:require [domina :as dom]
            [goog.events :as gev]
            [domina.events :as ev]))

Использую API Google closure отправляем форму на сервер. Для этого создаем объект IframeIo и вызываем у него метод sendFromForm, передадим в параметрах объект формы и путь куда будет отправляться запрос.

Для проверки успешности вызываем setErrorChecker, передаем в параметрах функцию которая должна вернуть false если возник ошибка. Для проверки успешности мы проверяем что Сервер вернул строку "ok"

Для иллюстрации успешности установим листенеры на три типа событий:

SUCCESS - когда errorChecker вернет false

ERROR - errorChecker вернет true

COMPLETE - запрос уйдет на сервер, сработает в не зависимости от errorChecker

Детальнее - "api goog.net.IframeIo"

(defn upload [] 
  (let [io (IframeIo.)] 
    (gev/listen io 
                (aget goog.net.EventType "SUCCESS") 
                #(js/alert "SUCCESS!")) 
    (gev/listen io 
                (aget goog.net.EventType "ERROR") 
                #(js/alert "ERROR!")) 
    (gev/listen io 
                (aget goog.net.EventType "COMPLETE") 
                #(js/alert "COMPLETE!")) 
    (.setErrorChecker io #(not= "ok" (.getResponseText io)))
    (.sendFromForm io (dom/by-id "form") "/upload")))

На нажатие на upload-button будм вызывать upload

(defn ^:export init [] 
  (ev/listen! (dom/by-id "upload-button")  
              :click upload))