Implement metadata api for fs builtin

This commit is contained in:
Filip Tibell 2023-07-20 12:25:36 +02:00
parent 2f464f846a
commit bca3de9454
No known key found for this signature in database
7 changed files with 339 additions and 23 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 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

View file

@ -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:

View file

@ -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<LuaTable> {
TableBuilder::new(lua)?
@ -13,6 +17,7 @@ pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
.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<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(_: &'static Lua, path: String) -> LuaResult<bool> {
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<bool> {
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()),
}
}

153
src/lune/lua/fs/metadata.rs Normal file
View 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,
}
}

View file

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

View file

@ -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",

70
tests/fs/metadata.luau Normal file
View file

@ -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)