在Clojure的Ring Web应用程序中生成和流式传输zip文件

9
我有一个Ring处理程序需要完成以下任务:
  • 压缩几个文件
  • 将Zip流传输到客户端
现在我已经做得差不多了,但只有第一个压缩条目被传输,之后就会停止/中断。我感觉这与刷新/传输有关的问题出在哪里。
这是我的(compojure)处理程序:
(GET "/zip" {:as request}
            :query-params [order-id   :- s/Any]
            (stream-lessons-zip (read-string order-id) (:db request) (:auth-user request)))

这里是stream-lessons-zip函数:

(defn stream-lessons-zip
  []
  (let [lessons ...];... not shown

  {:status 200
   :headers {"Content-Type" "application/zip, application/octet-stream"
             "Content-Disposition" (str "attachment; filename=\"files.zip\"")
   :body (futil/zip-lessons lessons)}))

我使用管道输入流来进行流式传输,代码如下:

(defn zip-lessons
 "Returns an inputstream (piped-input-stream) to be used directly in Ring HTTP responses"
[lessons]
(let [paths (map #(select-keys % [:file_path :file_name]) lessons)]
(ring-io/piped-input-stream
  (fn [output-stream]
    ; build a zip-output-stream from a normal output-stream
    (with-open [zip-output-stream (ZipOutputStream. output-stream)]
      (doseq [{:keys [file_path file_name] :as p} paths]
        (let [f (cio/file file_path)]
          (.putNextEntry zip-output-stream (ZipEntry. file_name)) 
          (cio/copy f zip-output-stream)
          (.closeEntry zip-output-stream))))))))

我确认“lessons”向量包含4个条目,但zip文件只包含1个条目。此外,Chrome似乎没有“完成”下载,即它认为它仍在下载。

我该怎么解决这个问题?


1
我尝试了你的代码简化版本,它能够正常工作。我认为可能是一些中间件造成了问题。你可以尝试在没有或最小化的中间件集合下运行你的应用程序,看看是否有效。 - Piotrek Bzdyl
1
你不会正在使用http-kit吧?我曾经尝试使用它来流式下载,但是遇到了问题。我认为它不支持流式下载,而ring-jetty则支持。 - Russell
我猜这是一个复制/粘贴错误,但你的stream-lessons-zip实现显示为无参数函数,但当你在处理程序中调用它时,你传递了三个参数? - Russell
啊,我确实使用http-kit!感谢Russel提到这一点,也感谢他指出代码在概念上是可以的,除了一些复制/粘贴错误。我会将此添加到答案中。 - Marten Sytema
@MartenSytema 或许你可以更新一下这个问题是否已经解决了? - FuzzyAmi
显示剩余3条评论
2个回答

1

听起来使用阻塞IO生成有状态流在http-kit中不受支持。非有状态流可以通过以下方式完成:

http://www.http-kit.org/server.html#async

一份介绍使用阻塞IO实现有状态流的PR未被接受:

https://github.com/http-kit/http-kit/pull/181

听起来可以尝试使用ByteArrayOutputStream将zip文件完全渲染到内存中,然后返回生成的缓冲区。如果此端点的流量不高且生成的zip文件不大(< 1 gb),则可能有效。


1

所以,已经过去了几年,但是那段代码仍在生产环境中运行(也就是它能正常工作)。当时我让它工作了,但忘记在这里提及(而且老实说,我忘记了它为什么能工作,因为那时候非常地试错)。

现在的代码如下:

(defn zip-lessons
  "Returns an inputstream (piped-input-stream) to be used directly in Ring HTTP responses"
  [lessons {:keys [firstname surname order_favorite_name company_name] :as annotation
            :or {order_favorite_name ""
                 company_name ""
                 firstname ""
                 surname ""}}]
  (debug "zipping lessons" (count lessons))
  (let [paths (map #(select-keys % [:file_path :file_name :folder_number]) lessons)]
    (ring-io/piped-input-stream
      (fn [output-stream]
        ; build a zip-output-stream from a normal output-stream
        (with-open [zip-output-stream (ZipOutputStream. output-stream)]
          (doseq [{:keys [file_path file_name folder_number] :as p} paths]
            (let [f (cio/as-file file_path)
                  baos (ByteArrayOutputStream.)]
              (if (.exists f)
                (do
                  (debug "Adding entry to zip:" file_name "at" file_path)
                  (let [zip-entry (ZipEntry. (str (if folder_number (str folder_number "/") "") file_name))]
                    (.putNextEntry zip-output-stream zip-entry)

                   
                    (.close baos)
                    (.writeTo baos zip-output-stream)
                    (.closeEntry zip-output-stream)
                    (.flush zip-output-stream)
                    (debug "flushed")))
                (warn "File '" file_name "' at '" file_path "' does not exist, not adding to zip file!"))))
          (.flush zip-output-stream)
          (.flush output-stream)
          (.finish zip-output-stream)
          (.close zip-output-stream))))))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接