mirror of
https://github.com/lune-org/lune.git
synced 2024-12-12 13:00:37 +00:00
Add back fs builtin
This commit is contained in:
parent
780f155377
commit
67e1d6c7fc
5 changed files with 472 additions and 0 deletions
155
src/lune/builtins/fs/copy.rs
Normal file
155
src/lune/builtins/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::options::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(())
|
||||
}
|
153
src/lune/builtins/fs/metadata.rs
Normal file
153
src/lune/builtins/fs/metadata.rs
Normal file
|
@ -0,0 +1,153 @@
|
|||
use std::{
|
||||
fmt,
|
||||
fs::{FileType as StdFileType, Metadata as StdMetadata, Permissions as StdPermissions},
|
||||
io::Result as IoResult,
|
||||
str::FromStr,
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FsMetadataKind {
|
||||
None,
|
||||
File,
|
||||
Dir,
|
||||
Symlink,
|
||||
}
|
||||
|
||||
impl fmt::Display for FsMetadataKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::None => "none",
|
||||
Self::File => "file",
|
||||
Self::Dir => "dir",
|
||||
Self::Symlink => "symlink",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for FsMetadataKind {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.trim().to_ascii_lowercase().as_ref() {
|
||||
"none" => Ok(Self::None),
|
||||
"file" => Ok(Self::File),
|
||||
"dir" => Ok(Self::Dir),
|
||||
"symlink" => Ok(Self::Symlink),
|
||||
_ => Err("Invalid metadata kind"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StdFileType> for FsMetadataKind {
|
||||
fn from(value: StdFileType) -> Self {
|
||||
if value.is_file() {
|
||||
Self::File
|
||||
} else if value.is_dir() {
|
||||
Self::Dir
|
||||
} else if value.is_symlink() {
|
||||
Self::Symlink
|
||||
} else {
|
||||
panic!("Encountered unknown filesystem filetype")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'lua> IntoLua<'lua> for FsMetadataKind {
|
||||
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
|
||||
if self == Self::None {
|
||||
Ok(LuaValue::Nil)
|
||||
} else {
|
||||
self.to_string().into_lua(lua)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FsPermissions {
|
||||
pub(crate) read_only: bool,
|
||||
}
|
||||
|
||||
impl From<StdPermissions> for FsPermissions {
|
||||
fn from(value: StdPermissions) -> Self {
|
||||
Self {
|
||||
read_only: value.readonly(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'lua> IntoLua<'lua> for FsPermissions {
|
||||
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
|
||||
let tab = lua.create_table_with_capacity(0, 1)?;
|
||||
tab.set("readOnly", self.read_only)?;
|
||||
tab.set_readonly(true);
|
||||
Ok(LuaValue::Table(tab))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FsMetadata {
|
||||
pub(crate) kind: FsMetadataKind,
|
||||
pub(crate) exists: bool,
|
||||
pub(crate) created_at: Option<f64>,
|
||||
pub(crate) modified_at: Option<f64>,
|
||||
pub(crate) accessed_at: Option<f64>,
|
||||
pub(crate) permissions: Option<FsPermissions>,
|
||||
}
|
||||
|
||||
impl FsMetadata {
|
||||
pub fn not_found() -> Self {
|
||||
Self {
|
||||
kind: FsMetadataKind::None,
|
||||
exists: false,
|
||||
created_at: None,
|
||||
modified_at: None,
|
||||
accessed_at: None,
|
||||
permissions: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'lua> IntoLua<'lua> for FsMetadata {
|
||||
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
|
||||
let tab = lua.create_table_with_capacity(0, 5)?;
|
||||
tab.set("kind", self.kind)?;
|
||||
tab.set("exists", self.exists)?;
|
||||
tab.set("createdAt", self.created_at)?;
|
||||
tab.set("modifiedAt", self.modified_at)?;
|
||||
tab.set("accessedAt", self.accessed_at)?;
|
||||
tab.set("permissions", self.permissions)?;
|
||||
tab.set_readonly(true);
|
||||
Ok(LuaValue::Table(tab))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StdMetadata> for FsMetadata {
|
||||
fn from(value: StdMetadata) -> Self {
|
||||
Self {
|
||||
kind: value.file_type().into(),
|
||||
exists: true,
|
||||
// FUTURE: Turn these into DateTime structs instead when that's implemented
|
||||
created_at: system_time_to_timestamp(value.created()),
|
||||
modified_at: system_time_to_timestamp(value.modified()),
|
||||
accessed_at: system_time_to_timestamp(value.accessed()),
|
||||
permissions: Some(FsPermissions::from(value.permissions())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn system_time_to_timestamp(res: IoResult<SystemTime>) -> Option<f64> {
|
||||
match res {
|
||||
Ok(t) => match t.duration_since(SystemTime::UNIX_EPOCH) {
|
||||
Ok(d) => Some(d.as_secs_f64()),
|
||||
Err(_) => None,
|
||||
},
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
128
src/lune/builtins/fs/mod.rs
Normal file
128
src/lune/builtins/fs/mod.rs
Normal file
|
@ -0,0 +1,128 @@
|
|||
use std::io::ErrorKind as IoErrorKind;
|
||||
use std::path::{PathBuf, MAIN_SEPARATOR};
|
||||
|
||||
use mlua::prelude::*;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::lune::util::TableBuilder;
|
||||
|
||||
mod copy;
|
||||
mod metadata;
|
||||
mod options;
|
||||
|
||||
use copy::copy;
|
||||
use metadata::FsMetadata;
|
||||
use options::FsWriteOptions;
|
||||
|
||||
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
|
||||
TableBuilder::new(lua)?
|
||||
.with_async_function("readFile", fs_read_file)?
|
||||
.with_async_function("readDir", fs_read_dir)?
|
||||
.with_async_function("writeFile", fs_write_file)?
|
||||
.with_async_function("writeDir", fs_write_dir)?
|
||||
.with_async_function("removeFile", fs_remove_file)?
|
||||
.with_async_function("removeDir", fs_remove_dir)?
|
||||
.with_async_function("metadata", fs_metadata)?
|
||||
.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()
|
||||
}
|
||||
|
||||
async fn fs_read_file(lua: &Lua, path: String) -> LuaResult<LuaString> {
|
||||
let bytes = fs::read(&path).await.into_lua_err()?;
|
||||
lua.create_string(bytes)
|
||||
}
|
||||
|
||||
async fn fs_read_dir(_: &Lua, path: String) -> LuaResult<Vec<String>> {
|
||||
let mut dir_strings = Vec::new();
|
||||
let mut dir = fs::read_dir(&path).await.into_lua_err()?;
|
||||
while let Some(dir_entry) = dir.next_entry().await.into_lua_err()? {
|
||||
if let Some(dir_path_str) = dir_entry.path().to_str() {
|
||||
dir_strings.push(dir_path_str.to_owned());
|
||||
} else {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"File path could not be converted into a string: '{}'",
|
||||
dir_entry.path().display()
|
||||
)));
|
||||
}
|
||||
}
|
||||
let mut dir_string_prefix = path;
|
||||
if !dir_string_prefix.ends_with(MAIN_SEPARATOR) {
|
||||
dir_string_prefix.push(MAIN_SEPARATOR);
|
||||
}
|
||||
let dir_strings_no_prefix = dir_strings
|
||||
.iter()
|
||||
.map(|inner_path| {
|
||||
inner_path
|
||||
.trim()
|
||||
.trim_start_matches(&dir_string_prefix)
|
||||
.to_owned()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Ok(dir_strings_no_prefix)
|
||||
}
|
||||
|
||||
async fn fs_write_file(_: &Lua, (path, contents): (String, LuaString<'_>)) -> LuaResult<()> {
|
||||
fs::write(&path, &contents.as_bytes()).await.into_lua_err()
|
||||
}
|
||||
|
||||
async fn fs_write_dir(_: &Lua, path: String) -> LuaResult<()> {
|
||||
fs::create_dir_all(&path).await.into_lua_err()
|
||||
}
|
||||
|
||||
async fn fs_remove_file(_: &Lua, path: String) -> LuaResult<()> {
|
||||
fs::remove_file(&path).await.into_lua_err()
|
||||
}
|
||||
|
||||
async fn fs_remove_dir(_: &Lua, path: String) -> LuaResult<()> {
|
||||
fs::remove_dir_all(&path).await.into_lua_err()
|
||||
}
|
||||
|
||||
async fn fs_metadata(_: &Lua, path: String) -> LuaResult<FsMetadata> {
|
||||
match fs::metadata(path).await {
|
||||
Err(e) if e.kind() == IoErrorKind::NotFound => Ok(FsMetadata::not_found()),
|
||||
Ok(meta) => Ok(FsMetadata::from(meta)),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn fs_is_file(_: &Lua, path: String) -> LuaResult<bool> {
|
||||
match fs::metadata(path).await {
|
||||
Err(e) if e.kind() == IoErrorKind::NotFound => Ok(false),
|
||||
Ok(meta) => Ok(meta.is_file()),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn fs_is_dir(_: &Lua, path: String) -> LuaResult<bool> {
|
||||
match fs::metadata(path).await {
|
||||
Err(e) if e.kind() == IoErrorKind::NotFound => Ok(false),
|
||||
Ok(meta) => Ok(meta.is_dir()),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn fs_move(_: &Lua, (from, to, options): (String, String, FsWriteOptions)) -> LuaResult<()> {
|
||||
let path_from = PathBuf::from(from);
|
||||
if !path_from.exists() {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"No file or directory exists at the path '{}'",
|
||||
path_from.display()
|
||||
)));
|
||||
}
|
||||
let path_to = PathBuf::from(to);
|
||||
if !options.overwrite && path_to.exists() {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"A file or directory already exists at the path '{}'",
|
||||
path_to.display()
|
||||
)));
|
||||
}
|
||||
fs::rename(path_from, path_to).await.into_lua_err()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fs_copy(_: &Lua, (from, to, options): (String, String, FsWriteOptions)) -> LuaResult<()> {
|
||||
copy(from, to, options).await
|
||||
}
|
31
src/lune/builtins/fs/options.rs
Normal file
31
src/lune/builtins/fs/options.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use mlua::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct FsWriteOptions {
|
||||
pub(crate) overwrite: bool,
|
||||
}
|
||||
|
||||
impl<'lua> FromLua<'lua> for FsWriteOptions {
|
||||
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
|
||||
Ok(match value {
|
||||
LuaValue::Nil => Self { overwrite: false },
|
||||
LuaValue::Boolean(b) => Self { overwrite: b },
|
||||
LuaValue::Table(t) => {
|
||||
let overwrite: Option<bool> = t.get("overwrite")?;
|
||||
Self {
|
||||
overwrite: overwrite.unwrap_or(false),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(LuaError::FromLuaConversionError {
|
||||
from: value.type_name(),
|
||||
to: "FsWriteOptions",
|
||||
message: Some(format!(
|
||||
"Invalid write options - expected boolean or table, got {}",
|
||||
value.type_name()
|
||||
)),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ use std::str::FromStr;
|
|||
|
||||
use mlua::prelude::*;
|
||||
|
||||
mod fs;
|
||||
mod luau;
|
||||
mod serde;
|
||||
mod stdio;
|
||||
|
@ -9,6 +10,7 @@ mod task;
|
|||
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
pub enum LuneBuiltin {
|
||||
Fs,
|
||||
Luau,
|
||||
Task,
|
||||
Serde,
|
||||
|
@ -21,6 +23,7 @@ where
|
|||
{
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Fs => "fs",
|
||||
Self::Luau => "luau",
|
||||
Self::Task => "task",
|
||||
Self::Serde => "serde",
|
||||
|
@ -30,6 +33,7 @@ where
|
|||
|
||||
pub fn create(&self, lua: &'lua Lua) -> LuaResult<LuaMultiValue<'lua>> {
|
||||
let res = match self {
|
||||
Self::Fs => fs::create(lua),
|
||||
Self::Luau => luau::create(lua),
|
||||
Self::Task => task::create(lua),
|
||||
Self::Serde => serde::create(lua),
|
||||
|
@ -49,6 +53,7 @@ impl FromStr for LuneBuiltin {
|
|||
type Err = String;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.trim().to_ascii_lowercase().as_str() {
|
||||
"fs" => Ok(Self::Fs),
|
||||
"luau" => Ok(Self::Luau),
|
||||
"task" => Ok(Self::Task),
|
||||
"serde" => Ok(Self::Serde),
|
||||
|
|
Loading…
Reference in a new issue