Clojure for Node Developers: File System
Published: 2023-11-26 17:39:03

---
title: Clojure for Node Developers: File System
published: true
date: 2023-11-23 06:10:11 UTC
tags: 
canonical_url: https://blog.tvaisanen.com/clojure-for-node-developers-file-system
---

This will be a straightforward post with Javascript to Clojure (on **JVM** ) examples of filesystem operations: listing, reading, writing, renaming, and deleting files. Let me know if you're interested in how to use Node packages from Clojurescript.

So, let's get to it.

In Javascript, we'd import the `fs`-module.

```
const fs = require('node:fs/promises')

```

> I'll be running all of the Javascript blocks inside a `(async () => { ...code... })()` closure.

In Clojure, we'd reach out for `clojure.java.io`

```
(require '[clojure.java.io :as io])

```

## Node Filehandle and Java File

In Javascript, we can get a reference to a file with a `Filehandle`.

```
let directory = await fs.open(".");

console.log(directory)

// FileHandle {
// close: [Function: close],
// [Symbol(kHandle)]: FileHandle {},
// [Symbol(kFd)]: 20,
// [Symbol(kRefs)]: 1,
// [Symbol(kClosePromise)]: null
// }

```

`FileHandle` can be either a directory or a file.

```
let directoryStats = await fs.stat(".");
directoryStats.isFile(); // false
directoryStats.isDirectory(); // true

let fileStats = await fs.stat("files/a.txt");
fileStats.isFile()); // true
fileStats.isDirectory(); // false

```

The basic building block in Clojure is `java.io.File`.

> User interfaces and operating systems use system-dependent _pathname strings_ to name files and directories. This class presents an abstract, system-independent view of hierarchical pathnames.
> 
> [Docs: java.io.File](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/io/File.html)

```
(io/file ".")
;; => #object[java.io.File 0x54162701 "."]

```

Similarly to `Filehandle` in JS, `java.io.File` can be either a directory or a file. The `File` has `isDirectory` and `isFile` methods that can be called via Java-interop.

```
(.isDirectory (io/file "."))
;; => true

(.isFile (io/file "."))
;; => false

(.isDirectory (io/file "deps.edn"))
;; => false

(.isFile (io/file "deps.edn"))
;; => true

```

Let's see what other properties the `java.io.File` has with the [`bean`](https://clojuredocs.org/clojure.core/bean) function.

> bean
> 
> Takes a Java object and returns a read-only implementation of the map abstraction based upon its JavaBean properties.
> 
> [Source](https://github.com/clojure/clojure/blob/clojure-1.11.1/src/clj/clojure/core_proxy.clj#L403)

```
(bean (io/file "."))
;; => 
{:path ".",
 :freeSpace 146245873664,
 :parent nil,
 :directory true,
 :parentFile nil,
 :name ".",
 :file false,
 :canonicalFile
 #object[java.io.File 0x4f2abe7e "/home/tvaisanen/projects/tmp"],
 :absolute false,
 :absoluteFile
 #object[java.io.File 0x5594224b "/home/tvaisanen/projects/tmp/."],
 :hidden true,
 :class java.io.File,
 :canonicalPath "/home/tvaisanen/projects/tmp",
 :usableSpace 124331769856,
 :totalSpace 429923737600,
 :absolutePath "/home/tvaisanen/projects/tmp/."}

```

## Listing Files

Given we have the following file structure.

```
 tree files
files
 a.txt
 b.txt
 c.txt
 nest
     a.txt

```

In Javascript, we'd typically write.

```
let dir = await fs.readdir("files");
console.log(dir);
// ['a.txt', 'b.txt', 'c.txt', 'nest']

```

And in Clojure, we'll take the folder we want to list as `java.io.File` and list all the files with [`file-seq`](https://github.com/clojure/clojure/blob/clojure-1.11.1/src/clj/clojure/core.clj#L4973).

```
(file-seq (io/file "files"))
;; =>
(#object[java.io.File 0x7e2b40d7 "files"]
 #object[java.io.File 0x22aaad8f "files/a.txt"]
 #object[java.io.File 0x3730fe92 "files/b.txt"]
 #object[java.io.File 0x62aaf1b "files/c.txt"]
 #object[java.io.File 0x22236af1 "files/nest"]
 #object[java.io.File 0x55126def "files/nest/a.txt"])

```

I'd expect this to return files similarly `fs.readdir`, but now we have the nested files in the listing. Let's see why that happens and if we could get it to work similarly to the listing in Javascript.

If we take a look at the `file-seq` [source code](https://github.com/clojure/clojure/blob/clojure-1.11.1/src/clj/clojure/core.clj#L4973)

```
(defn file-seq
  "A tree seq on java.io.Files"
  {:added "1.0"
   :static true}
  [dir]
    (tree-seq
     (fn [^java.io.File f] (. f (isDirectory)))
     (fn [^java.io.File d] (seq (. d (listFiles))))
     dir))

```

The docstring says that the function returns a tree seq, which sounds like a non-flat structure. And if we look deeper into `tree-seq` function and get the docstring.

> Returns a lazy sequence of the nodes in a tree, via a depth-first walk. ...

And here's the explanation of what is happening: "a depth-first walk." This function will go first till the end of the file tree and start coming back towards the root directory. The first argument is a function that returns a boolean value to decide whether the node (read file or directory) needs to be traversed (read looked into). If it returns true, the same file is passed to the second function to list its nodes (read files and subdirectories). Based on this, the tree-seq will do a depth-first search and, therefore, read all the subdirectories.

You might have already noticed that the second function argument calls `(. d (listFiles))` to get the files for our directory `d`. Let's use this to mimic the JS version `readdir`.

```
(seq (. (io/file "files") listFiles))
;; =>
(#object[java.io.File 0x356bf4d3 "files/a.txt"]
 #object[java.io.File 0x13c28931 "files/b.txt"]
 #object[java.io.File 0x444349ab "files/c.txt"]
 #object[java.io.File 0x2e64f09c "files/nest"])

```

At this point, the expected result is almost identical to the Javascript's `fs.readdir`. We have the files listed in the directory, but instead of file names, we have `java.io.File` instances. Let's iterate over the files and once again use Java-interop with the `getName` method.

```
(for [file (seq (. (io/file "files") listFiles))]
  (.getName file))
;; => ("a.txt" "b.txt" "c.txt" "nest")

```

The `fs.readdir` function will throw if called with a filename that is not a directory. If we try to create a file sequence from a `java.io.File` that is not a directory, we get only the file itself in a list.

```
(file-seq (io/file "files/a.txt"))
;; => (#object[java.io.File 0x37eeb5e7 "files/a.txt"])

```

Let's combine what we've learned from the previous examples into a new function `read-dir` that takes a path, checks that it is not a file, and then lists the files and directories in the folder but not the subpages.

```
(defn read-dir [d]
  (let [directory (io/file d)]
    (when (. directory isFile)
      (throw (ex-info "Path is not a directory" {:path directory})))
    (doall
     (for [file (seq (. directory listFiles))]
       (.getName file)))))

(read-dir "files")
;; => ("a.txt" "b.txt" "c.txt" "nest")

(read-dir "files/a.txt")
;; Unhandled clojure.lang.ExceptionInfo
;; Path is not a directory
;; {:path #object[java.io.File 0x7c7a2da9 "files/a.txt"]}

```

That's enough on listing files for now. Next, let's look into reading the files.

## Read Files

Let's see the content for a couple of the example files.

```
 cat files/a.txt
AAAAA
 cat files/b.txt
BBBB
1111
2222
3333

```

In Javascript, we'd typically use the `fs.readFile`.

```
let content = await fs.readFile("files/a.txt", {encoding: "utf8"});
console.log(content);
// AAAAA

```

In Clojure, we default to `slurp`.

```
(slurp (io/file "files/a.txt"))
;; => "AAAAA\n"

```

Slurp also works with the filename since if the argument is a string; it's first coerced (read interpreted as) as a URI and second as a local file.

```
(slurp "files/a.txt")
;; => "AAAAA\n"

```

Both `fs.readdir` and `slurp` read the file into memory at once; therefore, you might want to avoid it in production!

The more responsible way is to read the files as streams line by line.

```
let file = await fs.open('files/b.txt',);

for await (const line of file.readLines()) {
    console.log(line);
}
// BBBB
// 1111
// 2222
// 3333

```

In Clojure, we can use the `java.io.Reader` to create a buffered reader for streaming over the files. `slurp` uses the same mechanism, but instead of reading a file line by line, it reads it all simultaneously.

```
(io/reader "files/a.txt")
;; => #object[java.io.BufferedReader 0x4754cc18 "java.io.BufferedReader@4754cc18"]

```

We can open this stream (read file) to create a lazy line sequence that can be processed one line at a time.

```
(with-open [reader (io/reader "files/b.txt")]
  (doall (for [line (line-seq reader)]
           (str "read: " line))))
;; => ("read: BBBB" "read: 1111" "read: 2222" "read: 3333")

```

Next, to writing files.

## Writing Files

With Javascript, we'd typically do this with `fs.writeFile` or use `fs.createFileStream` for extra control.

```
await fs.writeFile("files/write-with-js.txt", "writing from JS");

let content_01 = await fs.readFile("files/write-with-js.txt", 
                                   {encoding: "utf8"});
console.log(content); 
// writing from JS

```

In Clojure, similarly to `slurp` we have `spit` to write data.

```
(spit "files/new.txt" "New content here")
;; => nil
(slurp "files/new.txt")
;; => "New content here"

```

By default, `spit` overrides the file with the given content. To add to the file, use arguments `:append true`.

```
(spit "files/new.txt" "Append to file")
;; => nil
(slurp "files/new.txt")
;; => "Append to file"
(spit "files/new.txt" " Try again" :append true)
;; => nil
(slurp "files/new.txt")
;; => "Append to file Try again"

```

`spit` wraps the `java.io.writer`, which can be used directly. Similarly to `slurp` `java.io.writer` replaces the file content if `:append true` is not passed as an option.

```
(with-open [writer (io/writer "files/other.txt")]
  (.write writer "text here"))

(slurp "files/other.txt")
;; => "text here"

(with-open [writer (io/writer "files/other.txt" :append true)]
  (.write writer " more text"))
;; => nil

(slurp "files/other.txt")
;; => "text here more text"

```

Let's go through a couple of more use cases.

## Renaming Files

In Javascript, we rename files with `fs.rename`.

```
await fs.rename("files/c.txt", "files/d.txt");

```

In Clojure, we use the `renameTo` method.

```
(.renameTo (io/file "files/new.txt")
           (io/file "files/newer.txt"))
;; => true
(.isFile (io/file "files/new.txt"))
;; => false
(.isFile (io/file "files/newer.txt"))
;; => true

```

## Deleting Files

In Javascript, we delete files with `fs.unlink`

```
 await fs.unlink("files/d.txt");

```

In Clojure, we have `clojure.java.io/delete-file` for the same task.

```
(.isFile (io/file "files/other.txt"))
;; => true
(io/delete-file "files/other.txt")
;; => true
(.isFile (io/file "files/other.txt"))
;; => false

```

## Conclusion

Clojure can do the same tasks as Javascript on the filesystem level since `clojure.java.io` provides utility functions as a layer on top of `java.io` classes. If there's no function matching your need, you can use Java-interop by, for example, using the `java.io.File` methods. If you hit a roadblock, check the following resources for more hints.

- [ClojureDocs: clojure.java.io](https://clojuredocs.org/clojure.java.io)

- [JavaDocs: java.io.File](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/io/File.html)

Once again, I hope you found this helpful. Feel free to reach out and let me know what pain points you might have encountered when learning Clojure and what type of posts would help make the jumpsocial links in the menu.

Thank you for reading.