mirror of
https://github.com/lune-org/lune.git
synced 2024-12-12 04:50:36 +00:00
Implement metadata api for fs builtin
This commit is contained in:
parent
2f464f846a
commit
bca3de9454
7 changed files with 339 additions and 23 deletions
28
CHANGELOG.md
28
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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
153
src/lune/lua/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,
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
mod metadata;
|
||||
mod options;
|
||||
|
||||
pub use metadata::FsMetadata;
|
||||
pub use options::FsWriteOptions;
|
||||
|
|
|
@ -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
70
tests/fs/metadata.luau
Normal 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)
|
Loading…
Reference in a new issue