Skip to content

Commit c097a30

Browse files
authored
Copy and move (#38)
* rename, copy, performance updates * oof * remove unused ffi * update changelog * more changelog
1 parent d6dfb6b commit c097a30

File tree

3 files changed

+105
-18
lines changed

3 files changed

+105
-18
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# Changelog
22

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

59
## v2.1.0 - 28 August 2024
610
- Add `FileInfo` and `file_info_type` to get the file type from a `FileInfo` without checking the file system again

src/simplifile.gleam

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,9 @@ pub fn is_directory(filepath: String) -> Result(Bool, FileError)
413413
pub fn create_directory(filepath: String) -> Result(Nil, FileError)
414414

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

498+
/// Copy a file or a directory to a new path. Copies directories recursively.
499+
/// Performance note: This function does work to determine if the src path
500+
/// points to a file or a directory. Consider using one of the the dedicated
501+
/// functions `copy_file` or `copy_directory` if you already know which one you need.
502+
pub fn copy(src src: String, dest dest: String) -> Result(Nil, FileError) {
503+
use src_info <- result.try(file_info(src))
504+
case file_info_type(src_info) {
505+
File -> copy_file(src, dest)
506+
Directory -> copy_directory(src, dest)
507+
Symlink ->
508+
Error(Unknown(
509+
"This is an internal bug where the `file_info` is somehow returning info about a simlink. Please file an issue on the simplifile repo.",
510+
))
511+
Other ->
512+
Error(Unknown("Unknown file type (not file, directory, or simlink)"))
513+
}
514+
}
515+
495516
/// Copy a file at a given path to another path.
496517
/// Note: destination should include the filename, not just the directory
497518
pub fn copy_file(at src: String, to dest: String) -> Result(Nil, FileError) {
@@ -505,10 +526,16 @@ fn do_copy_file(src: String, dest: String) -> Result(Int, FileError)
505526

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

534+
/// Rename a file or directory.
535+
@external(erlang, "simplifile_erl", "rename_file")
536+
@external(javascript, "./simplifile_js.mjs", "renameFile")
537+
pub fn rename(at src: String, to dest: String) -> Result(Nil, FileError)
538+
512539
/// Copy a directory recursively
513540
pub fn copy_directory(at src: String, to dest: String) -> Result(Nil, FileError) {
514541
// Erlang does not provide a built in `copy_dir` function,
@@ -526,31 +553,34 @@ fn do_copy_directory(src: String, dest: String) -> Result(Nil, FileError) {
526553
let src_path = filepath.join(src, segment)
527554
let dest_path = filepath.join(dest, segment)
528555

529-
case is_file(src_path), is_directory(src_path) {
530-
Ok(True), Ok(False) -> {
556+
use src_info <- result.try(file_info(src_path))
557+
case file_info_type(src_info) {
558+
File -> {
531559
// For a file, create the file in the new directory
532560
use content <- result.try(read_bits(src_path))
533561
content
534562
|> write_bits(to: dest_path)
535563
}
536-
Ok(False), Ok(True) -> {
564+
Directory -> {
537565
// Create the target directory and recurse
538566
use _ <- result.try(create_directory(dest_path))
539567
do_copy_directory(src_path, dest_path)
540568
}
541-
Error(e), _ | _, Error(e) -> {
542-
Error(e)
543-
}
544-
Ok(False), Ok(False) | Ok(True), Ok(True) -> {
545-
// We're really not sure how that one happened.
546-
Error(Unknown("Unknown error copying directory"))
547-
}
569+
// Theoretically this shouldn't happen, as the file info function
570+
// will follow a simlink.
571+
Symlink ->
572+
Error(Unknown(
573+
"This is an internal bug where the `file_info` is somehow returning info about a simlink. Please file an issue on the simplifile repo.",
574+
))
575+
Other ->
576+
Error(Unknown("Unknown file type (not file, directory, or simlink)"))
548577
}
549578
})
550579
Ok(Nil)
551580
}
552581

553582
/// Copy a directory recursively and then delete the old one.
583+
@deprecated("Use the `rename` function, which can rename a file or a directory.")
554584
pub fn rename_directory(
555585
at src: String,
556586
to dest: String,

test/simplifile_test.gleam

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ import simplifile.{
1010
Enomem, Enospc, Enosr, Enostr, Enosys, Enotblk, Enotdir, Enotsup, Enxio,
1111
Eopnotsupp, Eoverflow, Eperm, Epipe, Erange, Erofs, Espipe, Esrch, Estale,
1212
Etxtbsy, Exdev, Execute, File, FilePermissions, NotUtf8, Read, Unknown, Write,
13-
append, append_bits, copy_directory, copy_file, create_directory,
13+
append, append_bits, copy, copy_directory, copy_file, create_directory,
1414
create_directory_all, create_file, create_symlink, delete, delete_all,
1515
file_info, file_info_permissions, file_info_permissions_octal, file_info_type,
1616
file_permissions_to_octal, get_files, is_directory, is_file, is_symlink,
17-
link_info, read, read_bits, read_directory, rename_directory, rename_file,
18-
set_permissions, set_permissions_octal, write, write_bits,
17+
link_info, read, read_bits, read_directory, rename, set_permissions,
18+
set_permissions_octal, write, write_bits,
1919
}
2020

2121
pub fn main() {
@@ -224,7 +224,7 @@ pub fn copy_test() {
224224
pub fn rename_test() {
225225
let assert Ok(_) = write("Hello", to: "./tmp/to_be_renamed.txt")
226226
let assert Ok(Nil) =
227-
rename_file("./tmp/to_be_renamed.txt", to: "./tmp/renamed.txt")
227+
rename("./tmp/to_be_renamed.txt", to: "./tmp/renamed.txt")
228228
let assert Ok(False) = is_file("./tmp/to_be_renamed.txt")
229229
let assert Ok(True) = is_file("./tmp/renamed.txt")
230230
let assert Ok(_) = delete("./tmp/renamed.txt")
@@ -266,8 +266,7 @@ pub fn rename_directory_test() {
266266
|> write(to: "./tmp/to_be_copied_dir/nested_dir/file.txt")
267267

268268
// Copy the directory
269-
let assert Ok(_) =
270-
rename_directory("./tmp/to_be_copied_dir", to: "./tmp/copied_dir")
269+
let assert Ok(_) = rename("./tmp/to_be_copied_dir", to: "./tmp/copied_dir")
271270

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

549-
/// I visually inspected this info to make sure it matched on all targets.
550-
/// TODO: Add a better test setup for validating file info functionality.
551548
pub fn clear_directory_test() {
552549
let assert Ok(_) = create_directory_all("./tmp/clear_dir")
553550
let assert Ok(_) = create_directory_all("./tmp/clear_dir/nested_dir")
@@ -672,3 +669,59 @@ pub fn describe_error_test() {
672669
let assert "Unknown error: Something went wrong" =
673670
simplifile.describe_error(Unknown("Something went wrong"))
674671
}
672+
673+
pub fn file_info_follows_simlinks_recursively_test() {
674+
let assert Ok(_) = create_file("./tmp/base.txt")
675+
let assert Ok(_) = create_symlink(from: "./tmp/layer_1.txt", to: "./base.txt")
676+
let assert Ok(_) =
677+
create_symlink(from: "./tmp/layer_2.txt", to: "./layer_1.txt")
678+
679+
let assert Ok(fi) = file_info("./tmp/layer_2.txt")
680+
fi |> file_info_type |> should.equal(File)
681+
682+
let assert Ok(_) = create_directory("./tmp/base_dir")
683+
let assert Ok(_) = create_symlink(from: "./tmp/layer_1_dir", to: "./base_dir")
684+
let assert Ok(_) =
685+
create_symlink(from: "./tmp/layer_2_dir", to: "./layer_1_dir")
686+
687+
let assert Ok(fi) = file_info("./tmp/layer_2_dir")
688+
fi |> file_info_type |> should.equal(Directory)
689+
}
690+
691+
pub fn copy_can_copy_whatever_test() {
692+
let og_file = "./tmp/toodaloofile.txt"
693+
let og_dir = "./tmp/toodaloofile_dir"
694+
let symlink_to_file = "./tmp/toodaloolink.txt"
695+
696+
let assert Ok(_) = write("Hello", to: og_file)
697+
let assert Ok(_) = create_directory(og_dir)
698+
let assert Ok(_) = write("Hello subfile", to: og_dir <> "/subfile.txt")
699+
let assert Ok(_) =
700+
create_symlink(from: symlink_to_file, to: "./toodaloofile.txt")
701+
702+
let assert Ok(_) = copy(og_file, "./tmp/toodaloo_copied.txt")
703+
let assert Ok("Hello") = read("./tmp/toodaloo_copied.txt")
704+
705+
let assert Ok(_) = copy(og_dir, "./tmp/toodaloo_copied_dir")
706+
let assert Ok(["subfile.txt"]) = read_directory("./tmp/toodaloo_copied_dir")
707+
let assert Ok("Hello subfile") = read("./tmp/toodaloo_copied_dir/subfile.txt")
708+
709+
let assert Ok(_) = copy(symlink_to_file, "./tmp/copied_link.txt")
710+
let assert Ok("Hello") = read("./tmp/copied_link.txt")
711+
}
712+
713+
pub fn rename_file_succeeds_at_renaming_a_directory_test() {
714+
// I am so dumb lol
715+
716+
let dir = "./tmp/i_am_a_dir"
717+
let file = dir <> "/i_am_a_file.txt"
718+
let assert Ok(_) = create_directory_all(dir)
719+
let assert Ok(_) = write("Hello", to: file)
720+
let new_dir = "./tmp/i_am_also_a_dir"
721+
722+
let assert Ok(_) = rename(at: dir, to: new_dir)
723+
724+
let assert Ok(fi) = file_info(new_dir)
725+
fi |> file_info_type |> should.equal(Directory)
726+
read(new_dir <> "/i_am_a_file.txt") |> should.be_ok |> should.equal("Hello")
727+
}

0 commit comments

Comments
 (0)