mirror of
https://github.com/lune-org/lune.git
synced 2025-01-05 19:09:10 +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
|
||||||
|
|
||||||
- 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
|
### Changed
|
||||||
|
|
||||||
|
@ -20,10 +36,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed publishing of Lune to crates.io by migrating away from a monorepo
|
- 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 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 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 tab character at the start of a script causing it not to parse correctly. ([#72])
|
||||||
|
|
||||||
[#62]: https://github.com/filiptibell/lune/pull/62
|
[#62]: https://github.com/filiptibell/lune/pull/62
|
||||||
[#68]: https://github.com/filiptibell/lune/pull/66
|
[#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
|
### 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`
|
- Update to Luau version `0.581`
|
||||||
|
|
||||||
## `0.7.1` - June 17th, 2023
|
## `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
|
@type WriteOptions
|
||||||
@within FS
|
@within FS
|
||||||
|
@ -134,6 +186,23 @@ return {
|
||||||
@within FS
|
@within FS
|
||||||
@must_use
|
@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.
|
Checks if a given path is a file.
|
||||||
|
|
||||||
An error will be thrown in the following situations:
|
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 std::path::{PathBuf, MAIN_SEPARATOR};
|
||||||
|
|
||||||
use mlua::prelude::*;
|
use mlua::prelude::*;
|
||||||
use tokio::fs;
|
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> {
|
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
|
||||||
TableBuilder::new(lua)?
|
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("writeDir", fs_write_dir)?
|
||||||
.with_async_function("removeFile", fs_remove_file)?
|
.with_async_function("removeFile", fs_remove_file)?
|
||||||
.with_async_function("removeDir", fs_remove_dir)?
|
.with_async_function("removeDir", fs_remove_dir)?
|
||||||
|
.with_async_function("metadata", fs_metadata)?
|
||||||
.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)?
|
||||||
|
@ -74,27 +79,27 @@ async fn fs_remove_dir(_: &'static Lua, path: String) -> LuaResult<()> {
|
||||||
fs::remove_dir_all(&path).await.map_err(LuaError::external)
|
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> {
|
async fn fs_is_file(_: &'static Lua, path: String) -> LuaResult<bool> {
|
||||||
let path = PathBuf::from(path);
|
match fs::metadata(path).await {
|
||||||
if path.exists() {
|
Err(e) if e.kind() == IoErrorKind::NotFound => Ok(false),
|
||||||
Ok(fs::metadata(path)
|
Ok(meta) => Ok(meta.is_file()),
|
||||||
.await
|
Err(e) => Err(e.into()),
|
||||||
.map_err(LuaError::external)?
|
|
||||||
.is_file())
|
|
||||||
} else {
|
|
||||||
Ok(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fs_is_dir(_: &'static Lua, path: String) -> LuaResult<bool> {
|
async fn fs_is_dir(_: &'static Lua, path: String) -> LuaResult<bool> {
|
||||||
let path = PathBuf::from(path);
|
match fs::metadata(path).await {
|
||||||
if path.exists() {
|
Err(e) if e.kind() == IoErrorKind::NotFound => Ok(false),
|
||||||
Ok(fs::metadata(path)
|
Ok(meta) => Ok(meta.is_dir()),
|
||||||
.await
|
Err(e) => Err(e.into()),
|
||||||
.map_err(LuaError::external)?
|
|
||||||
.is_dir())
|
|
||||||
} else {
|
|
||||||
Ok(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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;
|
mod options;
|
||||||
|
|
||||||
|
pub use metadata::FsMetadata;
|
||||||
pub use options::FsWriteOptions;
|
pub use options::FsWriteOptions;
|
||||||
|
|
|
@ -40,6 +40,7 @@ macro_rules! create_tests {
|
||||||
create_tests! {
|
create_tests! {
|
||||||
fs_files: "fs/files",
|
fs_files: "fs/files",
|
||||||
fs_dirs: "fs/dirs",
|
fs_dirs: "fs/dirs",
|
||||||
|
fs_metadata: "fs/metadata",
|
||||||
fs_move: "fs/move",
|
fs_move: "fs/move",
|
||||||
|
|
||||||
net_request_codes: "net/request/codes",
|
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