janet-lang / spork

Various Janet utility modules - the official "Contrib" library.
MIT License
113 stars 35 forks source link

Lack of cross-platform function to create temporary directory #189

Open sogaiu opened 1 month ago

sogaiu commented 1 month ago

On more than one occasion, I've wanted to create a (previously non-existent) temporary directory to dump some files in. I'm guessing others have too (^^;

By "cross-platform", I'm mostly meaning various BSDs, Linux, macos, and recent Windows.

Did a bit of searching and have come up with the following links:

Following is a sketch of a possible flow:

  1. find "system" / "user" temporary directory
  2. verify it exists (it could disappear while doing the following step, but we'll ignore that for now)
  3. keep trying to make a new subdirectory of located + verified directory from steps 1 and 2 until success or some number of failures

The focus here is not on hiding things away (like is done in parts of Python's tempfile library), but just to have some place that won't accidentally get used.

Does this sound like:

sogaiu commented 1 month ago

Below is an initial sketch.

# https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppath2w
(defn windows-temp-root
  []
  (os/getenv "TMP"
             (os/getenv "TEMP"
                        (os/getenv "USERPROFILE"
                                   # XXX
                                   (os/getenv "WINDIR")))))

# https://en.cppreference.com/w/cpp/filesystem/temp_directory_path
(defn posix-temp-root
  []
  (os/getenv "TMPDIR"
             (os/getenv "TMP"
                        (os/getenv "TEMP"
                                   (os/getenv "TEMPDIR" "/tmp")))))

(defn temp-root
  []
  (case (os/which)
    # see comment above function definition
    :windows (windows-temp-root)
    # XXX: unsure
    :mingw "/tmp"
    # XXX: unsure, but https://cygwin.com/cygwin-ug-net/setup-env.html
    :cygwin "/tmp"
    # https://ss64.com/mac/syntax-env_vars.html
    :macos (os/getenv "TMPDIR")
    # https://emscripten.org/docs/api_reference/Filesystem-API.html
    :web "/tmp"
    # https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard
    :linux "/tmp"
    # https://www.freebsd.org/cgi/man.cgi?query=hier&sektion=7
    :freebsd "/tmp"
    # https://man.openbsd.org/hier.7
    :openbsd "/tmp"
    # https://man.netbsd.org/hier.7
    :netbsd "/tmp"
    # https://leaf.dragonflybsd.org/cgi/web-man?command=hier&section=7
    :dragonfly "/tmp"
    # based on the *bsd info above, following seems reasonable
    :bsd "/tmp"
    # see comment above function definition
    :posix (posix-temp-root)
    (errorf "unrecognized os: %n" (os/which))))

(defn mk-temp-dir
  ``
  Tries to create a new subdirectory of a system-specific temporary
  directory.  Optional argument `template` is used to specify a
  template for the new subdirectory's name.  Each forward slash (`/`)
  in the template is replaced with some hex value (0-9, a-f) to result
  in a candidate name.  The default value of `template` is `//////`.
  Optional argument `tries` is the maximum number of subdirectory
  creation attempts.  The default value of `tries` is 5.  Upon
  success, returns the full path of the newly created subdirectory.
  ``
  [&opt template tries]
  (default template "//////")
  (default tries 5)
  (def fs-sep (if (= :windows (os/which)) `\` "/"))

  (assert (not (empty? template))
          "template should be a non-empty string")
  (assert (and (nat? tries) (pos? tries))
          (string/format "tries should be a positive integer, not: %d"
                         tries))

  (def tmp-root (temp-root))
  (assert (= :directory (os/stat tmp-root :mode))
          (string/format "failed to find temp root `%s` for os `%s"
                         tmp-root (os/which)))

  (def rng (math/rng (os/cryptorand 8)))
  (defn rand-hex [_] (string/format "%x" (math/rng-int rng 16)))
  (var result nil)
  (for i 0 tries
    (def cand-path
      (string tmp-root fs-sep (string/replace-all "/" rand-hex template)))
    (when (os/mkdir cand-path)
      (set result cand-path)
      (break result)))

  (when (not result)
    (errorf "failed to create new temp directory after %d tries" tries))

  result)

(comment

  (peg/match ~(repeat 6 :h)
             (reverse (mk-temp-dir)))
  # =>
  @[]

  (peg/match ~(sequence (thru "hello-")
                        (repeat 3 :h))
             (mk-temp-dir "hello-///"))
  # =>
  @[]

  (do
    (def [success? _] (protect (mk-temp-dir "")))
    (not success?))
  # =>
  true

  )
sogaiu commented 1 month ago

Perhaps it could be useful to honor certain environment variables if users would prefer use of non-system locations.

The current sketch does something like this for some cases but not all. Candidate env vars for this purpose include:

May be better to avoid this kind of complication initially (^^;