Implement copying api for fs builtin

This commit is contained in:
Filip Tibell 2023-07-20 19:29:21 +02:00
parent bca3de9454
commit b0f23a406b
No known key found for this signature in database
7 changed files with 260 additions and 3 deletions

View file

@ -12,7 +12,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added support for running directories with an `init.luau` or `init.lua` file in them in the CLI.
- Added `fs.copy` to recursively copy files and directories.
Example usage:
```lua
local fs = require("@lune/fs")
fs.writeDir("myCoolDir")
fs.writeFile("myCoolDir/myAwesomeFile.json", "{}")
fs.copy("myCoolDir", "myCoolDir2")
assert(fs.isDir("myCoolDir2"))
assert(fs.isFile("myCoolDir2/myAwesomeFile.json"))
assert(fs.readFile("myCoolDir2/myAwesomeFile.json") == "{}")
```
- Added `fs.metadata` to get metadata about files and directories.
Example usage:
@ -30,6 +46,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
print(meta.permissions) --> { readOnly: false }
```
- Added support for running directories with an `init.luau` or `init.lua` file in them in the CLI.
### Changed
- Update to Luau version `0.583`

View file

@ -5,7 +5,7 @@ use mlua::prelude::*;
use tokio::fs;
use crate::lune::lua::{
fs::{FsMetadata, FsWriteOptions},
fs::{copy, FsMetadata, FsWriteOptions},
table::TableBuilder,
};
@ -21,6 +21,7 @@ pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
.with_async_function("isFile", fs_is_file)?
.with_async_function("isDir", fs_is_dir)?
.with_async_function("move", fs_move)?
.with_async_function("copy", fs_copy)?
.build_readonly()
}
@ -117,7 +118,7 @@ async fn fs_move(
let path_to = PathBuf::from(to);
if !options.overwrite && path_to.exists() {
return Err(LuaError::RuntimeError(format!(
"A file or directory alreadys exists at the path '{}'",
"A file or directory already exists at the path '{}'",
path_to.display()
)));
}
@ -126,3 +127,10 @@ async fn fs_move(
.map_err(LuaError::external)?;
Ok(())
}
async fn fs_copy(
_: &'static Lua,
(from, to, options): (String, String, FsWriteOptions),
) -> LuaResult<()> {
copy(from, to, options).await
}

155
src/lune/lua/fs/copy.rs Normal file
View file

@ -0,0 +1,155 @@
use std::collections::VecDeque;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use mlua::prelude::*;
use tokio::fs;
use super::FsWriteOptions;
pub struct CopyContents {
// Vec<(relative depth, path)>
pub dirs: Vec<(usize, PathBuf)>,
pub files: Vec<(usize, PathBuf)>,
}
async fn get_contents_at(root: PathBuf, _options: FsWriteOptions) -> LuaResult<CopyContents> {
let mut dirs = Vec::new();
let mut files = Vec::new();
let mut queue = VecDeque::new();
let normalized_root = fs::canonicalize(&root).await.map_err(|e| {
LuaError::RuntimeError(format!("Failed to canonicalize root directory path\n{e}"))
})?;
// Push initial children of the root path into the queue
let mut entries = fs::read_dir(&normalized_root).await?;
while let Some(entry) = entries.next_entry().await? {
queue.push_back((1, entry.path()));
}
// Go through the current queue, pushing to it
// when we find any new descendant directories
// FUTURE: Try to do async reading here concurrently to speed it up a bit
while let Some((current_depth, current_path)) = queue.pop_front() {
let meta = fs::metadata(&current_path).await?;
if meta.is_symlink() {
return Err(LuaError::RuntimeError(format!(
"Symlinks are not yet supported, encountered at path '{}'",
current_path.display()
)));
} else if meta.is_dir() {
// FUTURE: Add an option in FsWriteOptions for max depth and limit it here
let mut entries = fs::read_dir(&current_path).await?;
while let Some(entry) = entries.next_entry().await? {
queue.push_back((current_depth + 1, entry.path()));
}
dirs.push((current_depth, current_path));
} else {
files.push((current_depth, current_path));
}
}
// Ensure that all directory and file paths are relative to the root path
// SAFETY: Since we only ever push dirs and files relative to the root, unwrap is safe
for (_, dir) in dirs.iter_mut() {
*dir = dir.strip_prefix(&normalized_root).unwrap().to_path_buf()
}
for (_, file) in files.iter_mut() {
*file = file.strip_prefix(&normalized_root).unwrap().to_path_buf()
}
// FUTURE: Deduplicate paths such that these directories:
// - foo/
// - foo/bar/
// - foo/bar/baz/
// turn into a single foo/bar/baz/ and let create_dir_all do the heavy lifting
Ok(CopyContents { dirs, files })
}
async fn ensure_no_dir_exists(path: impl AsRef<Path>) -> LuaResult<()> {
let path = path.as_ref();
match fs::metadata(&path).await {
Ok(meta) if meta.is_dir() => Err(LuaError::RuntimeError(format!(
"A directory already exists at the path '{}'",
path.display()
))),
_ => Ok(()),
}
}
async fn ensure_no_file_exists(path: impl AsRef<Path>) -> LuaResult<()> {
let path = path.as_ref();
match fs::metadata(&path).await {
Ok(meta) if meta.is_file() => Err(LuaError::RuntimeError(format!(
"A file already exists at the path '{}'",
path.display()
))),
_ => Ok(()),
}
}
pub async fn copy(
source: impl AsRef<Path>,
target: impl AsRef<Path>,
options: FsWriteOptions,
) -> LuaResult<()> {
let source = source.as_ref();
let target = target.as_ref();
// Check if we got a file or directory - we will handle them differently below
let (is_dir, is_file) = match fs::metadata(&source).await {
Ok(meta) => (meta.is_dir(), meta.is_file()),
Err(e) if e.kind() == ErrorKind::NotFound => {
return Err(LuaError::RuntimeError(format!(
"No file or directory exists at the path '{}'",
source.display()
)))
}
Err(e) => return Err(e.into()),
};
if !is_file && !is_dir {
return Err(LuaError::RuntimeError(format!(
"The given path '{}' is not a file or a directory",
source.display()
)));
}
// Perform copying:
//
// 1. If we are not allowed to overwrite, make sure nothing exists at the target path
// 2. If we are allowed to overwrite, remove any previous entry at the path
// 3. Write all directories first
// 4. Write all files
if !options.overwrite {
if is_file {
ensure_no_file_exists(target).await?;
} else if is_dir {
ensure_no_dir_exists(target).await?;
}
}
if is_file {
fs::copy(source, target).await?;
} else if is_dir {
let contents = get_contents_at(source.to_path_buf(), options).await?;
if options.overwrite {
fs::remove_dir_all(target).await?;
}
// FUTURE: Write dirs / files concurrently
// to potentially speed these operations up
for (_, dir) in &contents.dirs {
fs::create_dir_all(target.join(dir)).await?;
}
for (_, file) in &contents.files {
fs::copy(source.join(file), target.join(file)).await?;
}
}
Ok(())
}

View file

@ -1,5 +1,7 @@
mod copy;
mod metadata;
mod options;
pub use copy::copy;
pub use metadata::FsMetadata;
pub use options::FsWriteOptions;

View file

@ -1,5 +1,6 @@
use mlua::prelude::*;
#[derive(Debug, Clone, Copy)]
pub struct FsWriteOptions {
pub(crate) overwrite: bool,
}

View file

@ -39,6 +39,7 @@ macro_rules! create_tests {
create_tests! {
fs_files: "fs/files",
fs_copy: "fs/copy",
fs_dirs: "fs/dirs",
fs_metadata: "fs/metadata",
fs_move: "fs/move",

72
tests/fs/copy.luau Normal file
View file

@ -0,0 +1,72 @@
local TEMP_DIR_PATH = "bin/"
local TEMP_ROOT_PATH = TEMP_DIR_PATH .. "fs_copy_test"
local TEMP_ROOT_PATH_2 = TEMP_DIR_PATH .. "fs_copy_test_2"
local fs = require("@lune/fs")
-- Generate test data & make sure our bin dir exists
local binary = ""
for _ = 1, 1024 do
binary ..= string.char(math.random(1, 127))
end
fs.writeDir(TEMP_DIR_PATH)
if fs.isDir(TEMP_ROOT_PATH) then
fs.removeDir(TEMP_ROOT_PATH)
end
if fs.isDir(TEMP_ROOT_PATH_2) then
fs.removeDir(TEMP_ROOT_PATH_2)
end
--[[
Create a file structure like this:
-> fs_copy_test
-- -> foo (dir)
-- -- -> bar (dir)
-- -- -- -> baz (file)
-- -- -> fizz (file)
-- -- -> buzz (file)
]]
fs.writeDir(TEMP_ROOT_PATH)
fs.writeDir(TEMP_ROOT_PATH .. "/foo")
fs.writeDir(TEMP_ROOT_PATH .. "/foo/bar")
fs.writeFile(TEMP_ROOT_PATH .. "/foo/bar/baz", binary)
fs.writeFile(TEMP_ROOT_PATH .. "/foo/fizz", binary)
fs.writeFile(TEMP_ROOT_PATH .. "/foo/buzz", binary)
-- Copy the entire structure
fs.copy(TEMP_ROOT_PATH, TEMP_ROOT_PATH_2)
-- Verify the copied structure
assert(fs.isDir(TEMP_ROOT_PATH_2), "Missing copied dir - root/")
assert(fs.isDir(TEMP_ROOT_PATH_2 .. "/foo"), "Missing copied dir - root/foo/")
assert(fs.isDir(TEMP_ROOT_PATH_2 .. "/foo/bar"), "Missing copied dir - root/foo/bar/")
assert(fs.isFile(TEMP_ROOT_PATH_2 .. "/foo/bar/baz"), "Missing copied file - root/foo/bar/baz")
assert(fs.isFile(TEMP_ROOT_PATH_2 .. "/foo/fizz"), "Missing copied file - root/foo/fizz")
assert(fs.isFile(TEMP_ROOT_PATH_2 .. "/foo/buzz"), "Missing copied file - root/foo/buzz")
-- Make sure the copied files are correct
assert(
fs.readFile(TEMP_ROOT_PATH_2 .. "/foo/bar/baz") == binary,
"Invalid copied file - root/foo/bar/baz"
)
assert(
fs.readFile(TEMP_ROOT_PATH_2 .. "/foo/fizz") == binary,
"Invalid copied file - root/foo/fizz"
)
assert(
fs.readFile(TEMP_ROOT_PATH_2 .. "/foo/buzz") == binary,
"Invalid copied file - root/foo/buzz"
)
-- Finally, clean up after us for any subsequent tests
fs.removeDir(TEMP_ROOT_PATH)
fs.removeDir(TEMP_ROOT_PATH_2)