From 2799825d290a43c70734308f3e246a52aa56bdf1 Mon Sep 17 00:00:00 2001 From: Benjamin Peinhardt Date: Wed, 5 Aug 2026 01:43:36 -0500 Subject: [PATCH 1/4] replace is_* ffi calls with file_info --- src/simplifile.gleam | 39 +++++++++++++++++++++++++--------- src/simplifile_erl.erl | 48 ------------------------------------------ src/simplifile_js.mjs | 36 ------------------------------- 3 files changed, 29 insertions(+), 94 deletions(-) diff --git a/src/simplifile.gleam b/src/simplifile.gleam index 1b49177..4523abe 100644 --- a/src/simplifile.gleam +++ b/src/simplifile.gleam @@ -410,14 +410,20 @@ pub fn append_bits( /// Checks if the provided filepath exists and is a directory. /// Returns an error if it lacks permissions to read the directory. +/// Returns Ok(False) if the path points to a regular file. +/// Follows symlinks, i.e. returns Ok(True) if the path is a symlink to a directory. /// /// ## Example /// ```gleam /// let assert Ok(True) = is_directory("./test") /// ``` -@external(erlang, "simplifile_erl", "is_directory") -@external(javascript, "./simplifile_js.mjs", "isDirectory") -pub fn is_directory(filepath: String) -> Result(Bool, FileError) +pub fn is_directory(filepath: String) -> Result(Bool, FileError) { + case file_info(filepath) { + Ok(info) -> Ok(file_info_type(info) == Directory) + Error(Enoent) -> Ok(False) + Error(e) -> Error(e) + } +} /// Create a directory at the provided filepath. Returns an error if /// the directory already exists. @@ -477,15 +483,21 @@ pub fn read_directory(at path: String) -> Result(List(String), FileError) /// Checks if the file at the provided filepath exists and is a file. /// Returns an Error if it lacks permissions to read the file. +/// Returns Ok(False) if the path points to a directory. +/// Follows symlinks, i.e. if the path is a symlink to a file, return Ok(True). /// /// ## Example /// ```gleam /// let assert Ok(True) = is_file("./test.txt") /// ``` /// -@external(erlang, "simplifile_erl", "is_file") -@external(javascript, "./simplifile_js.mjs", "isFile") -pub fn is_file(filepath: String) -> Result(Bool, FileError) +pub fn is_file(filepath: String) -> Result(Bool, FileError) { + case file_info(filepath) { + Ok(info) -> Ok(file_info_type(info) == File) + Error(Enoent) -> Ok(False) + Error(e) -> Error(e) + } +} /// Checks if the file at the provided filepath exists and is a symbolic link. /// Returns an Error if it lacks permissions to read the file. @@ -495,9 +507,13 @@ pub fn is_file(filepath: String) -> Result(Bool, FileError) /// let assert Ok(True) = is_symlink("./symlink") /// ``` /// -@external(erlang, "simplifile_erl", "is_symlink") -@external(javascript, "./simplifile_js.mjs", "isSymlink") -pub fn is_symlink(filepath: String) -> Result(Bool, FileError) +pub fn is_symlink(filepath: String) -> Result(Bool, FileError) { + case link_info(filepath) { + Ok(info) -> Ok(file_info_type(info) == Symlink) + Error(Enoent) -> Ok(False) + Error(e) -> Error(e) + } +} /// Creates an empty file at the given filepath. Returns an `Error(Eexist)` /// if the file already exists. @@ -565,7 +581,10 @@ pub fn rename_file(at src: String, to dest: String) -> Result(Nil, FileError) pub fn rename(at src: String, to dest: String) -> Result(Nil, FileError) /// Copy a directory recursively -pub fn copy_directory(at src: String, to dest: String) -> Result(Nil, FileError) { +pub fn copy_directory( + at src: String, + to dest: String, +) -> Result(Nil, FileError) { // Erlang does not provide a built in `copy_dir` function, // and Deno doesn't support Node's `fs.cpSync`, so we'll just roll // our own for now. diff --git a/src/simplifile_erl.erl b/src/simplifile_erl.erl index 6b9b11d..366c4d8 100644 --- a/src/simplifile_erl.erl +++ b/src/simplifile_erl.erl @@ -20,9 +20,6 @@ delete_directory/1, file_info/1, link_info/1, - is_directory/1, - is_file/1, - is_symlink/1, read_bits/1, read_directory/1, rename_file/2, @@ -160,51 +157,6 @@ rename_file(Source, Destination) -> set_permissions_octal(Filename, Permissions) -> posix_result(file:change_mode(Filename, Permissions)). -is_directory(Path) -> - case file:read_file_info(Path) of - {ok, FileInfo} -> - case FileInfo#file_info.type of - directory -> - {ok, true}; - _ -> - {ok, false} - end; - {error, enoent} -> - {ok, false}; - {error, Reason} -> - posix_result({error, Reason}) - end. - -is_file(Path) -> - case file:read_file_info(Path) of - {ok, FileInfo} -> - case FileInfo#file_info.type of - regular -> - {ok, true}; - _ -> - {ok, false} - end; - {error, enoent} -> - {ok, false}; - {error, Reason} -> - posix_result({error, Reason}) - end. - -is_symlink(Path) -> - case file:read_link_info(Path) of - {ok, FileInfo} -> - case FileInfo#file_info.type of - symlink -> - {ok, true}; - _ -> - {ok, false} - end; - {error, enoent} -> - {ok, false}; - {error, Reason} -> - posix_result({error, Reason}) - end. - %% For information on the file_info record refer to %% https://www.erlang.org/doc/apps/kernel/file.html#t:file_info/0 file_info_result(Result) -> diff --git a/src/simplifile_js.mjs b/src/simplifile_js.mjs index d0f656f..1dc91b3 100644 --- a/src/simplifile_js.mjs +++ b/src/simplifile_js.mjs @@ -67,42 +67,6 @@ function toUint8Array(contents) { return buffer; } -/** - * Check whether a file exists at the given path - * - * @param {string} filepath - * @returns {Ok | GError} - */ -export function isFile(filepath) { - try { - return new Ok(fs.statSync(path.normalize(filepath)).isFile()); - } catch (e) { - if (e.code === "ENOENT") { - return new Ok(false); - } else { - return new GError(cast_error(e.code)); - } - } -} - -/** - * Check whether a symbolic link exists at the given path - * - * @param {string} filepath - * @returns {Ok | GError} - */ -export function isSymlink(filepath) { - try { - return new Ok(fs.lstatSync(path.normalize(filepath)).isSymbolicLink()); - } catch (e) { - if (e.code === "ENOENT") { - return new Ok(false); - } else { - return new GError(cast_error(e.code)); - } - } -} - /** * Check whether a directory exists at the given path * From 0f7c6b8d9b756def465d030e1564d3b1fa4c6bdf Mon Sep 17 00:00:00 2001 From: Benjamin Peinhardt Date: Wed, 5 Aug 2026 01:56:17 -0500 Subject: [PATCH 2/4] exists --- src/simplifile.gleam | 18 ++++++++++++++++ test/simplifile_test.gleam | 42 +++++++++++++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/simplifile.gleam b/src/simplifile.gleam index 4523abe..beabb06 100644 --- a/src/simplifile.gleam +++ b/src/simplifile.gleam @@ -515,6 +515,24 @@ pub fn is_symlink(filepath: String) -> Result(Bool, FileError) { } } +/// Checks if anything exists at the path. +/// If `follow_links` is true, it will follow a link, returning false if nothing is there. +/// If `follow_links` is false, it will return true for any symlink. +pub fn exists( + filepath filepath: String, + follow_links follow_links: Bool, +) -> Result(Bool, FileError) { + let lookup = case follow_links { + True -> file_info + False -> link_info + } + case lookup(filepath) { + Ok(_) -> Ok(True) + Error(Enoent) -> Ok(False) + Error(e) -> Error(e) + } +} + /// Creates an empty file at the given filepath. Returns an `Error(Eexist)` /// if the file already exists. /// diff --git a/test/simplifile_test.gleam b/test/simplifile_test.gleam index 7afed15..9a5d2cf 100644 --- a/test/simplifile_test.gleam +++ b/test/simplifile_test.gleam @@ -12,7 +12,7 @@ import simplifile.{ Esrch, Estale, Etxtbsy, Exdev, Execute, File, FilePermissions, NotUtf8, Read, Unknown, Write, append, append_bits, copy, copy_directory, copy_file, create_directory, create_directory_all, create_file, create_link, - create_symlink, delete, delete_all, delete_file, file_info, + create_symlink, delete, delete_all, delete_file, exists, file_info, file_info_permissions, file_info_permissions_octal, file_info_type, file_permissions_to_octal, get_files, is_directory, is_file, is_symlink, link_info, read, read_bits, read_directory, rename, set_permissions, @@ -232,6 +232,46 @@ pub fn is_symlink_test() { let assert Ok(_) = delete(the_symlink) } +pub fn exists_test() { + // Nothing at the path + let assert Ok(False) = exists("./tmp/does_not_exist", follow_links: True) + let assert Ok(False) = exists("./tmp/does_not_exist", follow_links: False) + + // A regular file + let filepath = "./tmp/exists_test.txt" + let assert Ok(_) = + "" + |> write(to: filepath) + let assert Ok(True) = exists(filepath, follow_links: True) + let assert Ok(True) = exists(filepath, follow_links: False) + + // A directory + let assert Ok(True) = exists("./tmp", follow_links: True) + let assert Ok(True) = exists("./tmp", follow_links: False) + + // A symlink pointing at something that exists + let symlink_target = "exists_test_target.txt" + let symlink_to_existing_target = "./tmp/exists_test_valid_symlink" + let assert Ok(_) = + "" + |> write(to: "./tmp/" <> symlink_target) + let assert Ok(_) = create_symlink(symlink_target, symlink_to_existing_target) + let assert Ok(True) = exists(symlink_to_existing_target, follow_links: True) + let assert Ok(True) = exists(symlink_to_existing_target, follow_links: False) + + // A broken symlink: exists when not following links, doesn't when following + let broken_symlink = "./tmp/exists_test_broken_symlink" + let assert Ok(_) = create_symlink("does_not_exist_target", broken_symlink) + let assert Ok(False) = exists(broken_symlink, follow_links: True) + let assert Ok(True) = exists(broken_symlink, follow_links: False) + + // Cleanup + let assert Ok(_) = delete(filepath) + let assert Ok(_) = delete(symlink_to_existing_target) + let assert Ok(_) = delete("./tmp/" <> symlink_target) + let assert Ok(_) = delete(broken_symlink) +} + pub fn create_all_test() { let assert Ok(_) = create_directory_all("./tmp/level1/level2") let assert Ok(True) = is_directory("./tmp/level1") From b9d47fdabf065cdffb5aab4be60e2d1ad3655d49 Mon Sep 17 00:00:00 2001 From: Benjamin Peinhardt Date: Wed, 5 Aug 2026 01:57:56 -0500 Subject: [PATCH 3/4] changelog and version --- CHANGELOG.md | 3 +++ gleam.toml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa4095f..af80306 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ -- +## v2.7.0 - 5 August 2026 +- Add `exists` function for checking if a file, directory, or symlink exists at all at a given path. + ## v2.6.0 - 18 July 2026 - Add `touch` function, which creates a file if it doesn't exist and updates its access and modification times to now if it does. diff --git a/gleam.toml b/gleam.toml index 69d8a32..5f0445e 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,5 +1,5 @@ name = "simplifile" -version = "2.6.0" +version = "2.7.0" description = "Basic file operations that work on all targets" licences = ["Apache-2.0"] From fefacc854ca6de27fca3569d9959b5c03df3ec5b Mon Sep 17 00:00:00 2001 From: Benjamin Peinhardt Date: Wed, 5 Aug 2026 02:06:23 -0500 Subject: [PATCH 4/4] update gleam CI version + tweak comments --- .github/workflows/test.yml | 2 +- src/simplifile.gleam | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1870b05..5fceb45 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: - uses: erlef/setup-beam@v1 with: otp-version: "27" - gleam-version: "1.13.0" + gleam-version: "1.18.1" rebar3-version: "3" # elixir-version: "1.14.2" - run: gleam test diff --git a/src/simplifile.gleam b/src/simplifile.gleam index beabb06..a473a5b 100644 --- a/src/simplifile.gleam +++ b/src/simplifile.gleam @@ -410,8 +410,8 @@ pub fn append_bits( /// Checks if the provided filepath exists and is a directory. /// Returns an error if it lacks permissions to read the directory. -/// Returns Ok(False) if the path points to a regular file. -/// Follows symlinks, i.e. returns Ok(True) if the path is a symlink to a directory. +/// Returns `Ok(False)` if the path points to a regular file. +/// Follows symlinks, i.e. returns `Ok(True)` if the path is a symlink to a directory. /// /// ## Example /// ```gleam @@ -482,9 +482,9 @@ pub fn create_link( pub fn read_directory(at path: String) -> Result(List(String), FileError) /// Checks if the file at the provided filepath exists and is a file. -/// Returns an Error if it lacks permissions to read the file. -/// Returns Ok(False) if the path points to a directory. -/// Follows symlinks, i.e. if the path is a symlink to a file, return Ok(True). +/// Returns an error if it lacks permissions to read the file. +/// Returns `Ok(False)` if the path points to a directory. +/// Follows symlinks, i.e. if the path is a symlink to a file, returns `Ok(True)`. /// /// ## Example /// ```gleam @@ -500,7 +500,7 @@ pub fn is_file(filepath: String) -> Result(Bool, FileError) { } /// Checks if the file at the provided filepath exists and is a symbolic link. -/// Returns an Error if it lacks permissions to read the file. +/// Returns an error if it lacks permissions to read the file. /// /// ## Example /// ```gleam