Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion gleam.toml
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
61 changes: 49 additions & 12 deletions src/simplifile.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
48 changes: 0 additions & 48 deletions src/simplifile_erl.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) ->
Expand Down
36 changes: 0 additions & 36 deletions src/simplifile_js.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
42 changes: 41 additions & 1 deletion test/simplifile_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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")
Expand Down
Loading