Skip to content

Commit

Permalink
Copy and move (#38)
Browse files Browse the repository at this point in the history
* rename, copy, performance updates

* oof

* remove unused ffi

* update changelog

* more changelog
  • Loading branch information
bcpeinhardt authored Sep 24, 2024
1 parent d6dfb6b commit c097a30
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 18 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Changelog

## Unreleased
- Add `copy` function which can copy a file or a directory.
- Deprecate `rename_file` and `rename_directory` in favor of `rename` which does both.
- Refactor `copy_directory` to make fewer file system calls.
- Add some helpful docs for creating symlinks.

## v2.1.0 - 28 August 2024
- Add `FileInfo` and `file_info_type` to get the file type from a `FileInfo` without checking the file system again
Expand Down
50 changes: 40 additions & 10 deletions src/simplifile.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,9 @@ pub fn is_directory(filepath: String) -> Result(Bool, FileError)
pub fn create_directory(filepath: String) -> Result(Nil, FileError)

/// Create a symbolic link called symlink pointing to target.
/// Footgun Alert: the target path is relative to *the symlink*,
/// not the current working directory. I will likely be updating
/// the label on the next major version to reflect that.
///
/// ## Example
/// ```gleam
Expand Down Expand Up @@ -492,6 +495,24 @@ pub fn create_directory_all(dirpath: String) -> Result(Nil, FileError) {
@external(javascript, "./simplifile_js.mjs", "createDirAll")
fn do_create_dir_all(dirpath: String) -> Result(Nil, FileError)

/// Copy a file or a directory to a new path. Copies directories recursively.
/// Performance note: This function does work to determine if the src path
/// points to a file or a directory. Consider using one of the the dedicated
/// functions `copy_file` or `copy_directory` if you already know which one you need.
pub fn copy(src src: String, dest dest: String) -> Result(Nil, FileError) {
use src_info <- result.try(file_info(src))
case file_info_type(src_info) {
File -> copy_file(src, dest)
Directory -> copy_directory(src, dest)
Symlink ->
Error(Unknown(
"This is an internal bug where the `file_info` is somehow returning info about a simlink. Please file an issue on the simplifile repo.",
))
Other ->
Error(Unknown("Unknown file type (not file, directory, or simlink)"))
}
}

/// Copy a file at a given path to another path.
/// Note: destination should include the filename, not just the directory
pub fn copy_file(at src: String, to dest: String) -> Result(Nil, FileError) {
Expand All @@ -505,10 +526,16 @@ fn do_copy_file(src: String, dest: String) -> Result(Int, FileError)

/// Rename a file at a given path to another path.
/// Note: destination should include the filename, not just the directory
@deprecated("This function can move a file or a directory, so it's being renamed `rename`.")
@external(erlang, "simplifile_erl", "rename_file")
@external(javascript, "./simplifile_js.mjs", "renameFile")
pub fn rename_file(at src: String, to dest: String) -> Result(Nil, FileError)

/// Rename a file or directory.
@external(erlang, "simplifile_erl", "rename_file")
@external(javascript, "./simplifile_js.mjs", "renameFile")
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) {
// Erlang does not provide a built in `copy_dir` function,
Expand All @@ -526,31 +553,34 @@ fn do_copy_directory(src: String, dest: String) -> Result(Nil, FileError) {
let src_path = filepath.join(src, segment)
let dest_path = filepath.join(dest, segment)

case is_file(src_path), is_directory(src_path) {
Ok(True), Ok(False) -> {
use src_info <- result.try(file_info(src_path))
case file_info_type(src_info) {
File -> {
// For a file, create the file in the new directory
use content <- result.try(read_bits(src_path))
content
|> write_bits(to: dest_path)
}
Ok(False), Ok(True) -> {
Directory -> {
// Create the target directory and recurse
use _ <- result.try(create_directory(dest_path))
do_copy_directory(src_path, dest_path)
}
Error(e), _ | _, Error(e) -> {
Error(e)
}
Ok(False), Ok(False) | Ok(True), Ok(True) -> {
// We're really not sure how that one happened.
Error(Unknown("Unknown error copying directory"))
}
// Theoretically this shouldn't happen, as the file info function
// will follow a simlink.
Symlink ->
Error(Unknown(
"This is an internal bug where the `file_info` is somehow returning info about a simlink. Please file an issue on the simplifile repo.",
))
Other ->
Error(Unknown("Unknown file type (not file, directory, or simlink)"))
}
})
Ok(Nil)
}

/// Copy a directory recursively and then delete the old one.
@deprecated("Use the `rename` function, which can rename a file or a directory.")
pub fn rename_directory(
at src: String,
to dest: String,
Expand Down
69 changes: 61 additions & 8 deletions test/simplifile_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import simplifile.{
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,
append, append_bits, copy, 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,
link_info, read, read_bits, read_directory, rename, set_permissions,
set_permissions_octal, write, write_bits,
}

pub fn main() {
Expand Down Expand Up @@ -224,7 +224,7 @@ pub fn copy_test() {
pub fn rename_test() {
let assert Ok(_) = write("Hello", to: "./tmp/to_be_renamed.txt")
let assert Ok(Nil) =
rename_file("./tmp/to_be_renamed.txt", to: "./tmp/renamed.txt")
rename("./tmp/to_be_renamed.txt", to: "./tmp/renamed.txt")
let assert Ok(False) = is_file("./tmp/to_be_renamed.txt")
let assert Ok(True) = is_file("./tmp/renamed.txt")
let assert Ok(_) = delete("./tmp/renamed.txt")
Expand Down Expand Up @@ -266,8 +266,7 @@ pub fn rename_directory_test() {
|> write(to: "./tmp/to_be_copied_dir/nested_dir/file.txt")

// Copy the directory
let assert Ok(_) =
rename_directory("./tmp/to_be_copied_dir", to: "./tmp/copied_dir")
let assert Ok(_) = rename("./tmp/to_be_copied_dir", to: "./tmp/copied_dir")

// Assert the contents are correct
let assert Ok("Hello") = read("./tmp/copied_dir/file.txt")
Expand Down Expand Up @@ -546,8 +545,6 @@ pub fn link_info_test() {
|> should.not_equal(6)
}

/// I visually inspected this info to make sure it matched on all targets.
/// TODO: Add a better test setup for validating file info functionality.
pub fn clear_directory_test() {
let assert Ok(_) = create_directory_all("./tmp/clear_dir")
let assert Ok(_) = create_directory_all("./tmp/clear_dir/nested_dir")
Expand Down Expand Up @@ -672,3 +669,59 @@ pub fn describe_error_test() {
let assert "Unknown error: Something went wrong" =
simplifile.describe_error(Unknown("Something went wrong"))
}

pub fn file_info_follows_simlinks_recursively_test() {
let assert Ok(_) = create_file("./tmp/base.txt")
let assert Ok(_) = create_symlink(from: "./tmp/layer_1.txt", to: "./base.txt")
let assert Ok(_) =
create_symlink(from: "./tmp/layer_2.txt", to: "./layer_1.txt")

let assert Ok(fi) = file_info("./tmp/layer_2.txt")
fi |> file_info_type |> should.equal(File)

let assert Ok(_) = create_directory("./tmp/base_dir")
let assert Ok(_) = create_symlink(from: "./tmp/layer_1_dir", to: "./base_dir")
let assert Ok(_) =
create_symlink(from: "./tmp/layer_2_dir", to: "./layer_1_dir")

let assert Ok(fi) = file_info("./tmp/layer_2_dir")
fi |> file_info_type |> should.equal(Directory)
}

pub fn copy_can_copy_whatever_test() {
let og_file = "./tmp/toodaloofile.txt"
let og_dir = "./tmp/toodaloofile_dir"
let symlink_to_file = "./tmp/toodaloolink.txt"

let assert Ok(_) = write("Hello", to: og_file)
let assert Ok(_) = create_directory(og_dir)
let assert Ok(_) = write("Hello subfile", to: og_dir <> "/subfile.txt")
let assert Ok(_) =
create_symlink(from: symlink_to_file, to: "./toodaloofile.txt")

let assert Ok(_) = copy(og_file, "./tmp/toodaloo_copied.txt")
let assert Ok("Hello") = read("./tmp/toodaloo_copied.txt")

let assert Ok(_) = copy(og_dir, "./tmp/toodaloo_copied_dir")
let assert Ok(["subfile.txt"]) = read_directory("./tmp/toodaloo_copied_dir")
let assert Ok("Hello subfile") = read("./tmp/toodaloo_copied_dir/subfile.txt")

let assert Ok(_) = copy(symlink_to_file, "./tmp/copied_link.txt")
let assert Ok("Hello") = read("./tmp/copied_link.txt")
}

pub fn rename_file_succeeds_at_renaming_a_directory_test() {
// I am so dumb lol

let dir = "./tmp/i_am_a_dir"
let file = dir <> "/i_am_a_file.txt"
let assert Ok(_) = create_directory_all(dir)
let assert Ok(_) = write("Hello", to: file)
let new_dir = "./tmp/i_am_also_a_dir"

let assert Ok(_) = rename(at: dir, to: new_dir)

let assert Ok(fi) = file_info(new_dir)
fi |> file_info_type |> should.equal(Directory)
read(new_dir <> "/i_am_a_file.txt") |> should.be_ok |> should.equal("Hello")
}

0 comments on commit c097a30

Please sign in to comment.