mirror of
https://github.com/lune-org/lune.git
synced 2024-12-12 13:00:37 +00:00
Implement copying api for fs builtin
This commit is contained in:
parent
bca3de9454
commit
b0f23a406b
7 changed files with 260 additions and 3 deletions
20
CHANGELOG.md
20
CHANGELOG.md
|
@ -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`
|
||||
|
|
|
@ -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
155
src/lune/lua/fs/copy.rs
Normal 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(¤t_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(¤t_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(())
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
mod copy;
|
||||
mod metadata;
|
||||
mod options;
|
||||
|
||||
pub use copy::copy;
|
||||
pub use metadata::FsMetadata;
|
||||
pub use options::FsWriteOptions;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use mlua::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct FsWriteOptions {
|
||||
pub(crate) overwrite: bool,
|
||||
}
|
||||
|
|
|
@ -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
72
tests/fs/copy.luau
Normal 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)
|
Loading…
Reference in a new issue