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/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"] diff --git a/src/simplifile.gleam b/src/simplifile.gleam index 1b49177..a473a5b 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. @@ -476,28 +482,56 @@ 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 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 /// 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. +/// Returns an error if it lacks permissions to read the file. /// /// ## Example /// ```gleam /// 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) + } +} + +/// 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. @@ -565,7 +599,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 * 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")