diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bd6942..33217f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## Unreleased +- Add `FileInfo` and `file_info_type` to get the file type from a `FileInfo` without checking the file system again +- Add `file_info_permissions` and `file_info_permissions_octal` to get the currently set permissions of a file or directory. +- Improve performance of `get_files` - Add `link_info` function to get `FileInfo` values without following symlinks ## v2.0.1 - 27 June 2024 diff --git a/src/simplifile.gleam b/src/simplifile.gleam index 29161e9..73f593e 100644 --- a/src/simplifile.gleam +++ b/src/simplifile.gleam @@ -210,6 +210,55 @@ pub type FileInfo { ) } +/// Extract the file permissions from a given FileInfo value in their octal representation. +/// +/// ## Example +/// ```gleam +/// use info <- result.try(simplifile.file_info("src/app.gleam")) +/// simplifile.file_info_permissions_octal(info) +/// // --> 0o644 +/// ``` +pub fn file_info_permissions_octal(from file_info: FileInfo) -> Int { + int.bitwise_and(file_info.mode, 0o777) +} + +/// Extract the `FilePermissions` from a given FileInfo value. +pub fn file_info_permissions(from file_info: FileInfo) -> FilePermissions { + octal_to_file_permissions(file_info_permissions_octal(file_info)) +} + +/// An enumeration of different types of files. +pub type FileType { + /// A regular file + File + /// A directory + Directory + /// A symbolic link + Symlink + /// Another special file type present on some systems, lika a socket or device + Other +} + +/// Extract the file type from a given FileInfo value. +/// +/// ## Example +/// ```gleam +/// use info <- result.try(simplifile.file_info("src/app.gleam")) +/// simplifile.file_info_type(info) +/// // --> simplifile.File +/// ``` +pub fn file_info_type(from file_info: FileInfo) -> FileType { + // S_IFMT and related constants; + // see https://www.man7.org/linux/man-pages/man7/inode.7.html + // see https://github.com/nodejs/node/blob/main/typings/internalBinding/constants.d.ts#L147 + case int.bitwise_and(file_info.mode, 0o170000) { + 0o100000 -> File + 0o40000 -> Directory + 0o120000 -> Symlink + _ -> Other + } +} + /// Get information about a file at a given path /// /// When the given `filepath` points to a symlink, this function will follow @@ -526,19 +575,15 @@ pub fn get_files(in directory: String) -> Result(List(String), FileError) { use contents <- result.try(read_directory(directory)) use acc, content <- list.try_fold(over: contents, from: []) let path = filepath.join(directory, content) + use info <- result.try(file_info(path)) - case is_file(path) { - Error(e) -> Error(e) - Ok(True) -> Ok([path, ..acc]) - Ok(False) -> - case is_directory(path) { - Error(e) -> Error(e) - Ok(False) -> Ok(acc) - Ok(True) -> { - use nested_files <- result.try(get_files(path)) - Ok(list.append(acc, nested_files)) - } - } + case file_info_type(info) { + File -> Ok([path, ..acc]) + Directory -> { + use nested_files <- result.try(get_files(path)) + Ok(list.append(acc, nested_files)) + } + _ -> Ok(acc) } } @@ -557,6 +602,21 @@ fn permission_to_integer(permission: Permission) -> Int { } } +fn integer_to_permissions(integer: Int) -> Set(Permission) { + case int.bitwise_and(integer, 7) { + 7 -> set.from_list([Read, Write, Execute]) + 6 -> set.from_list([Read, Write]) + 5 -> set.from_list([Read, Execute]) + 3 -> set.from_list([Write, Execute]) + 4 -> set.from_list([Read]) + 2 -> set.from_list([Write]) + 1 -> set.from_list([Execute]) + 0 -> set.new() + // since we bitwise_and, these are all possible values. + _ -> panic + } +} + /// Represents a set of file permissions for a given file pub type FilePermissions { FilePermissions( @@ -581,6 +641,19 @@ pub fn file_permissions_to_octal(permissions: FilePermissions) -> Int { + make_permission_digit(permissions.other) } +fn octal_to_file_permissions(octal: Int) -> FilePermissions { + FilePermissions( + user: octal + |> int.bitwise_shift_right(6) + |> integer_to_permissions, + group: octal + |> int.bitwise_shift_right(3) + |> integer_to_permissions, + other: octal + |> integer_to_permissions, + ) +} + /// Sets the permissions for a given file /// /// # Example diff --git a/test/simplifile_test.gleam b/test/simplifile_test.gleam index 6ad80f4..4669106 100644 --- a/test/simplifile_test.gleam +++ b/test/simplifile_test.gleam @@ -4,17 +4,18 @@ import gleam/set import gleeunit import gleeunit/should import simplifile.{ - Eacces, Eagain, Ebadf, Ebadmsg, Ebusy, Edeadlk, Edeadlock, Edquot, Eexist, - Efault, Efbig, Eftype, Einval, Eio, Eisdir, Eloop, Emfile, Emlink, Emultihop, - Enametoolong, Enfile, Enobufs, Enodev, Enoent, Enolck, Enolink, Enomem, Enospc, - Enosr, Enostr, Enosys, Enotblk, Enotdir, Enotsup, Enxio, Eopnotsupp, Eoverflow, - Eperm, Epipe, Erange, Erofs, Espipe, Esrch, Estale, Etxtbsy, Exdev, Execute, - FilePermissions, NotUtf8, Read, Unknown, Write, append, append_bits, - copy_directory, copy_file, create_directory, create_directory_all, create_file, - create_symlink, delete, delete_all, file_info, file_permissions_to_octal, - get_files, is_directory, is_file, is_symlink, link_info, read, read_bits, - read_directory, rename_directory, rename_file, set_permissions, - set_permissions_octal, write, write_bits, + Directory, Eacces, Eagain, Ebadf, Ebadmsg, Ebusy, Edeadlk, Edeadlock, Edquot, + Eexist, Efault, Efbig, Eftype, Einval, Eio, Eisdir, Eloop, Emfile, Emlink, + Emultihop, Enametoolong, Enfile, Enobufs, Enodev, Enoent, Enolck, Enolink, + Enomem, Enospc, Enosr, Enostr, Enosys, Enotblk, Enotdir, Enotsup, Enxio, + Eopnotsupp, Eoverflow, Eperm, Epipe, Erange, Erofs, Espipe, Esrch, Estale, + Etxtbsy, Exdev, Execute, File, FilePermissions, NotUtf8, Read, Unknown, Write, + append, append_bits, copy_directory, copy_file, create_directory, + create_directory_all, create_file, create_symlink, delete, delete_all, + 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_directory, rename_file, + set_permissions, set_permissions_octal, write, write_bits, } pub fn main() { @@ -422,6 +423,36 @@ pub fn permissions_octal_test() { let assert Ok(Nil) = delete("./tmp/permissions") } +pub fn file_info_get_permissions_test() { + let filepath = "./tmp/permissions/test.sh" + let assert Ok(Nil) = create_directory("./tmp/permissions") + let assert Ok(Nil) = write("echo \"Hello from a file\"", to: filepath) + + // This is the equivalent of `chmod 777 ./tmp/permissions/test.sh` + let all = set.from_list([Read, Write, Execute]) + let all = FilePermissions(user: all, group: all, other: all) + let assert Ok(Nil) = set_permissions(filepath, all) + let assert Ok(info) = file_info(filepath) + + file_info_permissions(info) + |> should.equal(all) + + file_info_permissions_octal(info) + |> should.equal(0o777) + + let assert Ok(Nil) = set_permissions_octal(filepath, 0o625) + let assert Ok(info) = file_info(filepath) + + file_info_permissions(info) + |> should.equal(FilePermissions( + user: set.from_list([Read, Write]), + group: set.from_list([Write]), + other: set.from_list([Read, Execute]), + )) + + let assert Ok(Nil) = delete("./tmp/permissions") +} + pub fn get_files_with_slash_test() { let assert Ok(files) = get_files(in: "./test/") files @@ -473,6 +504,27 @@ pub fn file_info_test() { let assert Ok(_info) = file_info("./test.sh") } +pub fn file_info_type_test() { + let filepath = "./tmp/file_info_type_test.txt" + let assert Ok(_) = write(to: filepath, contents: "") + let assert Ok(info) = file_info(filepath) + file_info_type(info) + |> should.equal(File) + + let assert Ok(info) = file_info("./tmp") + file_info_type(info) + |> should.equal(Directory) + + let linkpath = "./tmp/file_info_type_symlink" + let assert Ok(_) = create_symlink("file_info_type_test.txt", linkpath) + let assert Ok(info) = file_info(linkpath) + file_info_type(info) + |> should.equal(File) + // let assert Ok(info) = link_info(linkpath) + // file_info_type(info) + // |> should.equal(Symlink) +} + pub fn link_info_test() { let target_path = "./tmp/the_target" let symlink_path = "./tmp/the_symlink"