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
|
||||||
|
|
||||||
- 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.
|
- Added `fs.metadata` to get metadata about files and directories.
|
||||||
|
|
||||||
Example usage:
|
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 }
|
print(meta.permissions) --> { readOnly: false }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- Added support for running directories with an `init.luau` or `init.lua` file in them in the CLI.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Update to Luau version `0.583`
|
- Update to Luau version `0.583`
|
||||||
|
|
|
@ -5,7 +5,7 @@ use mlua::prelude::*;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
use crate::lune::lua::{
|
use crate::lune::lua::{
|
||||||
fs::{FsMetadata, FsWriteOptions},
|
fs::{copy, FsMetadata, FsWriteOptions},
|
||||||
table::TableBuilder,
|
table::TableBuilder,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
|
||||||
.with_async_function("isFile", fs_is_file)?
|
.with_async_function("isFile", fs_is_file)?
|
||||||
.with_async_function("isDir", fs_is_dir)?
|
.with_async_function("isDir", fs_is_dir)?
|
||||||
.with_async_function("move", fs_move)?
|
.with_async_function("move", fs_move)?
|
||||||
|
.with_async_function("copy", fs_copy)?
|
||||||
.build_readonly()
|
.build_readonly()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +118,7 @@ async fn fs_move(
|
||||||
let path_to = PathBuf::from(to);
|
let path_to = PathBuf::from(to);
|
||||||
if !options.overwrite && path_to.exists() {
|
if !options.overwrite && path_to.exists() {
|
||||||
return Err(LuaError::RuntimeError(format!(
|
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()
|
path_to.display()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
@ -126,3 +127,10 @@ async fn fs_move(
|
||||||
.map_err(LuaError::external)?;
|
.map_err(LuaError::external)?;
|
||||||
Ok(())
|
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 metadata;
|
||||||
mod options;
|
mod options;
|
||||||
|
|
||||||
|
pub use copy::copy;
|
||||||
pub use metadata::FsMetadata;
|
pub use metadata::FsMetadata;
|
||||||
pub use options::FsWriteOptions;
|
pub use options::FsWriteOptions;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use mlua::prelude::*;
|
use mlua::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct FsWriteOptions {
|
pub struct FsWriteOptions {
|
||||||
pub(crate) overwrite: bool,
|
pub(crate) overwrite: bool,
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,7 @@ macro_rules! create_tests {
|
||||||
|
|
||||||
create_tests! {
|
create_tests! {
|
||||||
fs_files: "fs/files",
|
fs_files: "fs/files",
|
||||||
|
fs_copy: "fs/copy",
|
||||||
fs_dirs: "fs/dirs",
|
fs_dirs: "fs/dirs",
|
||||||
fs_metadata: "fs/metadata",
|
fs_metadata: "fs/metadata",
|
||||||
fs_move: "fs/move",
|
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