7

It seems like a classical problem but I can't find anything about it "the clojure way".

So, I have a foo/ directory inside resources/ (leiningen project). When jar'd/uberjar'd, this foo/ directory is put at the root of the jar. As files inside jar may not be physically consistent at runtime, you can't use basic copy function to recursively copy the directory to the outside world.

Several solutions for the Java world exist (How to write a Java program which can extract a JAR file and store its data in specified directory (location)? and How to extract directory (and sub directories) from jar resource? for example), but I didn't find any existing solution for Clojure. As a beginner (both Clojure and Java), I'm not sure how to translate above solutions to Clojure. Translating literally line by line from Java to Clojurish Java Interop doesn't seem right. Is there an "official", clojure-idiomatic way to do this ?

Note that I'm using Raynes' fs utilities library. There doesn't seem to be a function to do this directly, but maybe there's some elements I can use to simplify the process ? (besides the obvious basic io sugar)

Community
  • 1
  • 1

4 Answers4

5

I've written cpath-clj a few months back that will list resources on the classpath by URI. You could then try the following:

(require '[cpath-clj.core :as cp]
         '[clojure.java.io :as io])

(doseq [[path uris] (cp/resources (io/resource "foo"))
        :let [uri (first uris)
              relative-path (subs path 1)
              output-file (io/file output-directory relative-path)]]
  (with-open [in (io/input-stream uri)]
    (io/copy in output-file)))

There is some juggling going on since the library was not written with this use case in mind:

  • (cp/resources (io/resource "foo")) will give you the contents of your foo directory - if you had only used (cp/resources "foo") all such directories on the classpath would have been found,
  • theoretically, there can be multiple files with the same path on the classpath, that's why the function returns multiple uris; in our case, only the first one is of interest.
  • path is always starting with a slash, so to get the relative one, we have to remove it.

Maybe that's helpful to you.

xsc
  • 5,983
  • 23
  • 30
  • Thanks ! Not able to test it right now, but that certainly looks promising and a cleaner alternative to what I came up with ! – Logan Braga Feb 23 '15 at 20:35
  • I think this would actually fit into the scope of the library, so if you find a solution that you're happy with, feel free to open a PR and I'll gladly merge it. :) – xsc Feb 25 '15 at 15:55
2
(defn copy-resource! [resource-filename]
  (let [resource-file (io/resource resource-filename)
        tmp-file (io/file "/tmp" resource-filename)]
    (with-open [in (io/input-stream resource-file)] (io/copy in tmp-file))))

(copy-resource! "my-logo.png")
Kris
  • 19,188
  • 9
  • 91
  • 111
1

After trying around some more and getting help from #clojure, I was able to came up with a "kind-of-clojurish-i-guess" translation from my previous first link :

(ns my-ns
  (:require [me.raynes.fs    :as f])
  (:import [java.util.jar JarFile JarEntry]))

(defn extract-dir-from-jar
  "Takes the string path of a jar, a dir name inside that jar and a destination
  dir, and copies the from dir to the to dir."
  [^String jar-dir from to]
  (let [jar (JarFile. jar-dir)]
    (doseq [^JarEntry file (enumeration-seq (.entries jar))]
      (if (.startsWith (.getName file) from)
        (let [f (f/file to (.getName file))]
          (if (.isDirectory file)
            (f/mkdir f)
            (do (f/mkdirs (f/parent f))
                (with-open [is (.getInputStream jar file)
                            os (io/output-stream f)]
                  (io/copy is os)))))))))

I guess I can tidy this up a bit (especially around the .startsWith bit) but at least that works perfectly.

  • You can make it a bit more flexible by replacing `(enumeration-seq (.entries jar))` with `(filter #(re-matches from (.getName %)) (enumeration-seq (.entries jar)))` and having the `from` parameter be a regex. Then you can remove the `(if (.startsWith ...)`. – T.Gounelle Feb 24 '15 at 14:22
0

I am not sure about the idiomatic part but a file in the resource directory can be listed this way

(defn list-resource
  [resource-dir]
  (let [resource (io/file (io/resource resource-dir))]
    (file-seq resource)))

And copying a file from the directory foo in the resource to the current path is as simple as

(io/copy (io/file (io/resource "foo/test.txt")) (io/file "test.txt"))

Combining the two you should be able to write a recursive function that does what you ask

Rohit
  • 79
  • 1
  • 6
  • 3
    Unfortunately, that's the whole point : you can't use io/copy on a file inside a running jar (works inside a repl or with lein run though), because resource files in a jar are not in a 'file' state (so io/file will throw an 'IllegalArgumentException : not a file' if used on a "file" inside a jar). It works with lein run and in a REPL because the resource file is not called from inside the jar, but directly from the filesystem, so it is in a compatible file state. – Logan Braga Feb 21 '15 at 20:31
  • Can you provide some information on why you need to do this? I have seen something similar before, but it isn't a common use case and it is possible that a simpler solution exists for what you want to do – Tim X Feb 21 '15 at 23:18