From bca3de94544de3b7e86fcdfecaebd3a99a34d204 Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Thu, 20 Jul 2023 12:25:36 +0200 Subject: [PATCH] Implement metadata api for fs builtin --- CHANGELOG.md | 28 +++++-- docs/typedefs/FS.luau | 69 ++++++++++++++++ src/lune/builtins/fs.rs | 39 +++++---- src/lune/lua/fs/metadata.rs | 153 ++++++++++++++++++++++++++++++++++++ src/lune/lua/fs/mod.rs | 2 + src/tests.rs | 1 + tests/fs/metadata.luau | 70 +++++++++++++++++ 7 files changed, 339 insertions(+), 23 deletions(-) create mode 100644 src/lune/lua/fs/metadata.rs create mode 100644 tests/fs/metadata.luau diff --git a/CHANGELOG.md b/CHANGELOG.md index 758946b..4399abe 100644 --- a/CHANGELOG.md +++ b/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 support for running directories with an `init.luau` or `init.lua` file in them in the CLI. +- Added `fs.metadata` to get metadata about files and directories. + + Example usage: + + ```lua + local fs = require("@lune/fs") + + fs.writeFile("myAwesomeFile.json", "{}") + + local meta = fs.metadata("myAwesomeFile.json") + + print(meta.exists) --> true + print(meta.kind) --> "file" + print(meta.createdAt) --> 1689848548.0577152 (unix timestamp) + print(meta.permissions) --> { readOnly: false } + ``` ### Changed @@ -20,10 +36,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Fixed publishing of Lune to crates.io by migrating away from a monorepo -- Fixed crashes when writing a very deeply nested `Instance` to a file ([#62]) -- Fixed not being able to read & write to WebSocket objects at the same time ([#68]) -- Fixed tab character at the start of a script causing it not to parse correctly ([#72]) +- Fixed publishing of Lune to crates.io by migrating away from a monorepo. +- Fixed crashes when writing a very deeply nested `Instance` to a file. ([#62]) +- Fixed not being able to read & write to WebSocket objects at the same time. ([#68]) +- Fixed tab character at the start of a script causing it not to parse correctly. ([#72]) [#62]: https://github.com/filiptibell/lune/pull/62 [#68]: https://github.com/filiptibell/lune/pull/66 @@ -56,7 +72,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- The `lune --setup` command is now much more user-friendly +- The `lune --setup` command is now much more user-friendly. - Update to Luau version `0.581` ## `0.7.1` - June 17th, 2023 diff --git a/docs/typedefs/FS.luau b/docs/typedefs/FS.luau index 4745a09..76dc235 100644 --- a/docs/typedefs/FS.luau +++ b/docs/typedefs/FS.luau @@ -1,3 +1,55 @@ +export type MetadataKind = "file" | "dir" | "symlink" + +--[=[ + @type MetadataPermissions + @within FS + + Permissions for the given file or directory. + + This is a dictionary that will contain the following values: + + * `readOnly` - If the target path is read-only or not +]=] +export type MetadataPermissions = { + readOnly: boolean, +} + +-- FIXME: We lose doc comments here because of the union type + +--[=[ + @type Metadata + @within FS + + Metadata for the given file or directory. + + This is a dictionary that will contain the following values: + + * `kind` - If the target path is a `file`, `dir` or `symlink` + * `exists` - If the target path exists + * `createdAt` - The timestamp at which the file or directory was created + * `modifiedAt` - The timestamp at which the file or directory was last modified + * `accessedAt` - The timestamp at which the file or directory was last accessed + * `permissions` - Current permissions for the file or directory + + Note that timestamps are relative to the unix epoch, and + may not be accurate if the system clock is not accurate. +]=] +export type Metadata = { + kind: MetadataKind, + exists: true, + createdAt: number, + modifiedAt: number, + accessedAt: number, + permissions: MetadataPermissions, +} | { + kind: nil, + exists: false, + createdAt: nil, + modifiedAt: nil, + accessedAt: nil, + permissions: nil, +} + --[=[ @type WriteOptions @within FS @@ -134,6 +186,23 @@ return { @within FS @must_use + Gets metadata for the given path. + + An error will be thrown in the following situations: + + * The current process lacks permissions to read at `path`. + * Some other I/O error occurred. + + @param path The path to get metadata for + @return Metadata for the path + ]=] + metadata = function(path: string): Metadata + return nil :: any + end, + --[=[ + @within FS + @must_use + Checks if a given path is a file. An error will be thrown in the following situations: diff --git a/src/lune/builtins/fs.rs b/src/lune/builtins/fs.rs index d5dd88f..92c3c28 100644 --- a/src/lune/builtins/fs.rs +++ b/src/lune/builtins/fs.rs @@ -1,9 +1,13 @@ +use std::io::ErrorKind as IoErrorKind; use std::path::{PathBuf, MAIN_SEPARATOR}; use mlua::prelude::*; use tokio::fs; -use crate::lune::lua::{fs::FsWriteOptions, table::TableBuilder}; +use crate::lune::lua::{ + fs::{FsMetadata, FsWriteOptions}, + table::TableBuilder, +}; pub fn create(lua: &'static Lua) -> LuaResult { TableBuilder::new(lua)? @@ -13,6 +17,7 @@ pub fn create(lua: &'static Lua) -> LuaResult { .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)? @@ -74,27 +79,27 @@ async fn fs_remove_dir(_: &'static Lua, path: String) -> LuaResult<()> { fs::remove_dir_all(&path).await.map_err(LuaError::external) } +async fn fs_metadata(_: &'static Lua, path: String) -> LuaResult { + 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(_: &'static Lua, path: String) -> LuaResult { - let path = PathBuf::from(path); - if path.exists() { - Ok(fs::metadata(path) - .await - .map_err(LuaError::external)? - .is_file()) - } else { - Ok(false) + 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(_: &'static Lua, path: String) -> LuaResult { - let path = PathBuf::from(path); - if path.exists() { - Ok(fs::metadata(path) - .await - .map_err(LuaError::external)? - .is_dir()) - } else { - Ok(false) + 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()), } } diff --git a/src/lune/lua/fs/metadata.rs b/src/lune/lua/fs/metadata.rs new file mode 100644 index 0000000..a5e1ddf --- /dev/null +++ b/src/lune/lua/fs/metadata.rs @@ -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 { + 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 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> { + 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 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> { + 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, + pub(crate) modified_at: Option, + pub(crate) accessed_at: Option, + pub(crate) permissions: Option, +} + +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> { + 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 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) -> Option { + match res { + Ok(t) => match t.duration_since(SystemTime::UNIX_EPOCH) { + Ok(d) => Some(d.as_secs_f64()), + Err(_) => None, + }, + Err(_) => None, + } +} diff --git a/src/lune/lua/fs/mod.rs b/src/lune/lua/fs/mod.rs index fcdd352..be3d449 100644 --- a/src/lune/lua/fs/mod.rs +++ b/src/lune/lua/fs/mod.rs @@ -1,3 +1,5 @@ +mod metadata; mod options; +pub use metadata::FsMetadata; pub use options::FsWriteOptions; diff --git a/src/tests.rs b/src/tests.rs index 77c7f36..3dac041 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -40,6 +40,7 @@ macro_rules! create_tests { create_tests! { fs_files: "fs/files", fs_dirs: "fs/dirs", + fs_metadata: "fs/metadata", fs_move: "fs/move", net_request_codes: "net/request/codes", diff --git a/tests/fs/metadata.luau b/tests/fs/metadata.luau new file mode 100644 index 0000000..3819b17 --- /dev/null +++ b/tests/fs/metadata.luau @@ -0,0 +1,70 @@ +local TEMP_DIR_PATH = "bin/" +local TEMP_FILE_PATH = TEMP_DIR_PATH .. "metadata_test" + +local fs = require("@lune/fs") +local task = require("@lune/task") + +-- 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.isFile(TEMP_FILE_PATH) then + fs.removeFile(TEMP_FILE_PATH) +end + +--[[ + 1. File should initially not exist + 2. Write the file + 3. File should now exist +]] + +assert(not fs.metadata(TEMP_FILE_PATH).exists, "File metadata not exists failed") +fs.writeFile(TEMP_FILE_PATH, binary) +assert(fs.metadata(TEMP_FILE_PATH).exists, "File metadata exists failed") + +--[[ + 1. Kind should be `dir` for our temp directory + 2. Kind should be `file` for our temp file +]] + +local metaDir = fs.metadata(TEMP_DIR_PATH) +local metaFile = fs.metadata(TEMP_FILE_PATH) +assert(metaDir.kind == "dir", "Dir metadata kind was invalid") +assert(metaFile.kind == "file", "File metadata kind was invalid") + +--[[ + 1. Capture initial metadata + 2. Wait for a bit so that timestamps can change + 3. Write the file, with an extra newline + 4. Metadata changed timestamp should be different + 5. Metadata created timestamp should be the same different +]] + +local metaBefore = fs.metadata(TEMP_FILE_PATH) +task.wait() +fs.writeFile(TEMP_FILE_PATH, binary .. "\n") +local metaAfter = fs.metadata(TEMP_FILE_PATH) + +assert( + metaAfter.modifiedAt ~= metaBefore.modifiedAt, + "File metadata change timestamp did not change" +) +assert( + metaAfter.createdAt == metaBefore.createdAt, + "File metadata creation timestamp changed from modification" +) + +--[[ + 1. Permissions should exist + 2. Our newly created file should not be readonly +]] +assert(metaAfter.permissions ~= nil, "File metadata permissions are missing") +assert(not metaAfter.permissions.readOnly, "File metadata permissions are readonly") + +-- Finally, clean up after us for any subsequent tests + +fs.removeFile(TEMP_FILE_PATH)