Compare commits

..

38 commits
v0.1.1 ... main

Author SHA1 Message Date
Renovate Bot
d90a1c42c8 fix(deps): update rust crate secrecy to 0.10
Some checks failed
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/cron/woodpecker Pipeline was successful
2024-09-17 23:16:55 +00:00
Renovate Bot
9943a0fe4e chore(deps): update docker.io/rust docker tag to v1.81
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/cron/woodpecker Pipeline was successful
2024-09-05 23:46:32 +00:00
Renovate Bot
a2e91a871b fix(deps): update rust crate derive_more to 1.0.0-beta
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/cron/woodpecker Pipeline was successful
2024-08-01 18:15:41 +00:00
Renovate Bot
ca7be5c484 chore(deps): update docker.io/rust docker tag to v1.80
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/cron/woodpecker Pipeline was successful
2024-07-25 20:45:42 +00:00
Renovate Bot
7335e78552 chore(deps): update docker.io/woodpeckerci/plugin-docker-buildx docker tag to v4.2
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/cron/woodpecker Pipeline was successful
2024-07-16 16:30:56 +00:00
Renovate Bot
9c819fd856 chore(deps): update docker.io/woodpeckerci/plugin-docker-buildx docker tag to v4.1
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/cron/woodpecker Pipeline was successful
2024-07-09 20:00:36 +00:00
Renovate Bot
3f805b3ad5 chore(deps): update docker.io/rust docker tag to v1.79
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/cron/woodpecker Pipeline was successful
2024-06-13 17:45:39 +00:00
274f70e485 feat: network: add from impl to help discard unit NetResponses
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/cron/woodpecker Pipeline was successful
2024-06-06 07:09:43 +01:00
d0aee99c83 feat(fs): add dir_read()
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
ci/woodpecker/cron/woodpecker Pipeline was successful
2024-05-17 12:08:56 +01:00
Renovate Bot
93c74487a1 chore(deps): update docker.io/woodpeckerci/plugin-docker-buildx docker tag to v4
Some checks failed
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
ci/woodpecker/cron/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline failed
2024-05-07 10:47:42 +00:00
Renovate Bot
f261474ed0 chore(deps): update docker.io/rust docker tag to v1.78
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/cron/woodpecker Pipeline was successful
2024-05-02 17:45:41 +00:00
faa9f291d4 build(woodpecker): use latest release plugin
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline failed
ci/woodpecker/cron/woodpecker Pipeline was successful
2024-04-30 07:50:55 +01:00
a84248eca3 fix(fs): make FileSystems Clone and Debug
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-04-30 07:50:55 +01:00
595a376a62 fix(fs): add missing std::error::Error impl for fs::Error
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline failed
ci/woodpecker/cron/woodpecker Pipeline failed
2024-04-28 19:11:12 +01:00
6995dbedcb build(woodpecker): forgejo releases are marked as stable
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-04-28 15:40:25 +01:00
a83fe8b581 build(woodpecker): accept any tag format
All checks were successful
ci/woodpecker/tag/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-04-28 15:32:01 +01:00
d58e46d65e test(fs): add tests
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2024-04-28 15:24:09 +01:00
b32c10f080 refactor(fs/tests): group into modules 2024-04-28 15:24:08 +01:00
a9057a8831 feat(fs): properly validates paths are within base directory 2024-04-28 15:24:06 +01:00
12d55b98e5 feat(fs): add dir_create and dir_create_all
Closes kemitix/kxio#25
2024-04-28 14:30:23 +01:00
a77ed422eb refactor(fs): reorder trait methods 2024-04-28 12:48:32 +01:00
9c76ddc3e1 feat(fs): extract fs::like internal module 2024-04-28 12:37:15 +01:00
483d274e5b feat: enable fs and network features by default 2024-04-28 12:34:51 +01:00
396d4b77bc chore: Version set to 1.1.0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2024-04-28 11:40:03 +01:00
68f419459f build(woodpecker): enable fs and network for build and test
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-04-28 11:36:43 +01:00
9339958996 build(woodpecker): whitespace 2024-04-28 11:34:19 +01:00
8d506131ca build(justfile): add validate dev branch recipe
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-04-28 11:29:44 +01:00
2e3827e0e0 feat(fs): new fs module to replace filesystem 2024-04-28 11:29:44 +01:00
36070f59cc docs(readme): add details about current status and future plans 2024-04-28 11:20:39 +01:00
8d3f5df378 feat(fs): add new contructors for real and temp 2024-04-28 11:20:37 +01:00
a917b21f50 refactor(fs): rename internal structs
trait *Env => *Like
struct *Env => * (remove suffix)
2024-04-28 08:18:49 +01:00
a65689a51c chore(deps): prevent renovate creating pointless PRs
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/cron/woodpecker Pipeline was successful
Signed-off-by: kemitix <kemitix@noreply.kemitix.net>
2024-04-27 14:29:48 +01:00
6354681ac1 chore: Version set to 1.0.0
Some checks failed
ci/woodpecker/tag/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/cron/woodpecker Pipeline failed
2024-04-16 07:36:00 +01:00
edb21d67df build(woodpecker): run ci on next branch too
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-04-16 07:21:19 +01:00
07eaa09d11 build: Add .git-next.toml config file
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-04-16 07:06:50 +01:00
ff69ed2f23 feat!: split into non-default features fs and network
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Closes kemitix/kxio#11
2024-04-16 07:06:50 +01:00
939230038c fix: reduce logging level
All checks were successful
ci/woodpecker/tag/woodpecker Pipeline was successful
ci/woodpecker/cron/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
Some info changed to debug. Tests left at info.
2024-04-10 11:36:33 +01:00
fac1b38828 build(woodpecker): restore missing cargo build command
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Merge build and lint steps
2024-04-10 07:36:54 +01:00
18 changed files with 695 additions and 58 deletions

6
.git-next.toml Normal file
View file

@ -0,0 +1,6 @@
[branches]
main = "main"
next = "next"
dev = "dev"
[options]

View file

@ -1,9 +1,8 @@
steps: steps:
update-builder-image: update-builder-image:
when: when:
- event: cron - event: cron
image: docker.io/woodpeckerci/plugin-docker-buildx:3.2 image: docker.io/woodpeckerci/plugin-docker-buildx:4.2
settings: settings:
username: kemitix username: kemitix
repo: git.kemitix.net/kemitix/kxio-builder repo: git.kemitix.net/kemitix/kxio-builder
@ -13,60 +12,48 @@ steps:
registry: git.kemitix.net registry: git.kemitix.net
password: password:
from_secret: woodpecker-docker-push from_secret: woodpecker-docker-push
todo_check: todo_check:
# INFO: https://woodpecker-ci.org/plugins/TODO-Checker # INFO: https://woodpecker-ci.org/plugins/TODO-Checker
image: codeberg.org/epsilon_02/todo-checker:1.1 image: codeberg.org/epsilon_02/todo-checker:1.1
when: when:
- event: push - event: push
branch: main branch: [main, next]
settings: settings:
# kxio-woodpecker-todo-checker - read:issue # kxio-woodpecker-todo-checker - read:issue
repository_token: '4acf14f93747e044aa2d1397367741b53f3d4f8f' repository_token: "4acf14f93747e044aa2d1397367741b53f3d4f8f"
prefix_regex: "(#|//) (TODO|FIXME): " prefix_regex: "(#|//) (TODO|FIXME): "
debug: false debug: false
build: lint_and_build:
when: when:
- event: push - event: push
branch: main branch: [main, next]
- event: tag - event: tag
ref: refs/tags/v*
image: git.kemitix.net/kemitix/kxio-builder:latest
environment:
CARGO_TERM_COLOR: always
lint:
when:
- event: push
branch: main
- event: tag
ref: refs/tags/v*
image: git.kemitix.net/kemitix/kxio-builder:latest image: git.kemitix.net/kemitix/kxio-builder:latest
environment: environment:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
commands: commands:
- ls -l /usr/local/cargo/bin/ - ls -l /usr/local/cargo/bin/
- cargo fmt --all -- --check - cargo fmt --all -- --check
- cargo clippy -- -D warnings -W clippy::nursery -W clippy::unwrap_used -W clippy::expect_used - cargo clippy --features "fs,network" -- -D warnings -W clippy::nursery -W clippy::unwrap_used -W clippy::expect_used
- cargo build --features "fs,network"
test: test:
when: when:
- event: push - event: push
branch: main branch: [main, next]
- event: tag - event: tag
ref: refs/tags/v*
image: git.kemitix.net/kemitix/kxio-builder:latest image: git.kemitix.net/kemitix/kxio-builder:latest
environment: environment:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
commands: commands:
- cargo test - cargo test --features "fs,network"
publish_to_crates_io: publish_to_crates_io:
when: when:
- event: tag - event: tag
ref: refs/tags/v* image: docker.io/rust:1.81
image: docker.io/rust:1.77
commands: commands:
- cargo login "$CARGO_REGISTRY_TOKEN" - cargo login "$CARGO_REGISTRY_TOKEN"
- cargo publish --registry crates-io --no-verify - cargo publish --registry crates-io --no-verify
@ -75,12 +62,11 @@ steps:
publish_to_forgejo: publish_to_forgejo:
when: when:
- event: tag - event: tag
ref: refs/tags/v*
# INFO: https://woodpecker-ci.org/plugins/Gitea%20Release # INFO: https://woodpecker-ci.org/plugins/Gitea%20Release
image: docker.io/woodpeckerci/plugin-gitea-release:0.3 image: docker.io/woodpeckerci/plugin-gitea-release:latest
settings: settings:
base_url: https://git.kemitix.net base_url: https://git.kemitix.net
api_key: api_key:
from_secret: FORGEJO_RELEASE_PLUGIN from_secret: FORGEJO_RELEASE_PLUGIN
target: main target: main
prerelease: true prerelease: false

View file

@ -1,6 +1,6 @@
[package] [package]
name = "kxio" name = "kxio"
version = "0.1.1" version = "1.2.0"
edition = "2021" edition = "2021"
authors = ["Paul Campbell <pcampbell@kemitix.net>"] authors = ["Paul Campbell <pcampbell@kemitix.net>"]
description = "Provides injectable Filesystem and Network resources to make code more testable" description = "Provides injectable Filesystem and Network resources to make code more testable"
@ -8,6 +8,11 @@ license = "MIT"
repository = "https://git.kemitix.net/kemitix/kxio" repository = "https://git.kemitix.net/kemitix/kxio"
exclude = [".cargo_home"] exclude = [".cargo_home"]
[features]
default = ["fs", "network"]
fs = []
network = []
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
@ -18,7 +23,7 @@ tracing = "0.1"
async-trait = "0.1" async-trait = "0.1"
http = "1.1" http = "1.1"
reqwest = "0.12" reqwest = "0.12"
secrecy = "0.8" secrecy = "0.10"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
serde-xml-rs = "0.6" serde-xml-rs = "0.6"
@ -26,6 +31,14 @@ thiserror = "1.0"
# fs # fs
tempfile = "3.10" tempfile = "3.10"
path-clean = "1.0"
# boilerplate
derive_more = { version = "1.0.0-beta", features = [
"from",
"display",
"constructor",
] }
[dev-dependencies] [dev-dependencies]
# testing # testing

View file

@ -3,3 +3,38 @@
[![status-badge](https://ci.kemitix.net/api/badges/53/status.svg)](https://ci.kemitix.net/repos/53) [![status-badge](https://ci.kemitix.net/api/badges/53/status.svg)](https://ci.kemitix.net/repos/53)
Provides injectable Filesystem and Network resources to make code more testable. Provides injectable Filesystem and Network resources to make code more testable.
### FileSystem
There are two FileSystem implementation: [filesystem] and [fs].
- [filesystem] is the legacy implementation and will be removed in a future version.
- [fs] is the current version and is intended to stand-in for and extend the [std::fs] module from the Standard Library.
#### std::fs alternatives
| To Do | [std::fs] | [kxio::fs::FileSystem] | |
| ----- | ---------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| [ ] | canonicalize | path_canonicalize | Returns the canonical, absolute form of a path with all intermediate components normalized and symbolic links resolved. |
| [ ] | copy | file_copy | Copies the contents of one file to another. This function will also copy the permission bits of the original file to the destination file. |
| [ ] | create_dir | dir_create | Creates a new, empty directory at the provided path |
| [ ] | create_dir_all | dir_create_all | Recursively create a directory and all of its parent components if they are missing. |
| [ ] | hard_link | link_create | Creates a new hard link on the filesystem. |
| [ ] | metadata | path_metadata | Given a path, query the file system to get information about a file, directory, etc. |
| [ ] | read | file_read | Read the entire contents of a file into a bytes vector. |
| [ ] | read_dir | dir_read | Returns an iterator over the entries within a directory. |
| [ ] | read_link | link_read | Reads a symbolic link, returning the file that the link points to. |
| [x] | read_to_string | file_read_to_string | Read the entire contents of a file into a string. |
| [ ] | remove_dir | dir_remove | Removes an empty directory. |
| [ ] | remove_dir_all | dir_remove_all | Removes a directory at this path, after removing all its contents. Use carefully! |
| [ ] | remove_file | file_remove | Removes a file from the filesystem. |
| [ ] | rename | path_rename | Rename a file or directory to a new name, replacing the original file if to already exists. |
| [ ] | set_permissions | path_set_permissions | Changes the permissions found on a file or a directory. |
| [ ] | symlink_metadata | link_metadata | Query the metadata about a file without following symlinks. |
| [x] | write | file_write | Write a slice as the entire contents of a file. |
### Network
The entire [network] module needs to be completly rewritten
It's use is strongly discouraged.
A new [net] module will likely be its replacement.

View file

@ -1,3 +1,8 @@
install-hooks: install-hooks:
@echo "Installing git hooks" @echo "Installing git hooks"
git config core.hooksPath .git-hooks git config core.hooksPath .git-hooks
validate-dev-branch:
git rebase -i origin/main -x 'cargo build --features "fs,network"'
git rebase -i origin/main -x 'cargo test --features "fs,network"'
git rebase -i origin/main -x 'cargo clippy --features "fs,network" -- -D warnings -W clippy::nursery -W clippy::unwrap_used -W clippy::expect_used'

View file

@ -1,6 +1,10 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [ "extends": ["config:recommended"],
"config:recommended" "packageRules": [
{
"matchManagers": ["cargo"],
"rangeStrategy": "replace"
}
] ]
} }

View file

@ -1,4 +1,4 @@
#![allow(unused)] #![allow(deprecated)]
use std::{ use std::{
ops::Deref, ops::Deref,
@ -7,24 +7,34 @@ use std::{
}; };
use tempfile::{tempdir, TempDir}; use tempfile::{tempdir, TempDir};
use tracing::info; use tracing::{debug, info};
pub fn real(cwd: Option<PathBuf>) -> FileSystem {
let cwd = cwd.unwrap_or_default();
FileSystem::Real(RealFileSystem::new(cwd))
}
pub fn temp() -> std::io::Result<FileSystem> {
TempFileSystem::new().map(FileSystem::Temp)
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[deprecated(since = "1.1.0", note = "Use [kxio::fs::FileSystem] instead")]
pub enum FileSystem { pub enum FileSystem {
Real(RealFileSystemEnv), Real(RealFileSystem),
Temp(TempFileSystemEnv), Temp(TempFileSystem),
} }
impl FileSystem { impl FileSystem {
#[deprecated(since = "1.1.0", note = "Use [kxio::filesystem::real()] instead")]
pub fn new_real(cwd: Option<PathBuf>) -> Self { pub fn new_real(cwd: Option<PathBuf>) -> Self {
let cwd = cwd.unwrap_or_default(); real(cwd)
Self::Real(RealFileSystemEnv::new(cwd))
} }
#[deprecated(since = "1.1.0", note = "Use [kxio::filesystem::temp()] instead")]
pub fn new_temp() -> std::io::Result<Self> { pub fn new_temp() -> std::io::Result<Self> {
TempFileSystemEnv::new().map(Self::Temp) temp()
} }
} }
impl Deref for FileSystem { impl Deref for FileSystem {
type Target = dyn FileSystemEnv; type Target = dyn FileSystemLike;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
match self { match self {
@ -34,7 +44,7 @@ impl Deref for FileSystem {
} }
} }
pub trait FileSystemEnv: Sync + Send + std::fmt::Debug { pub trait FileSystemLike: Sync + Send + std::fmt::Debug {
fn cwd(&self) -> &PathBuf; fn cwd(&self) -> &PathBuf;
fn in_cwd(&self, name: &str) -> PathBuf { fn in_cwd(&self, name: &str) -> PathBuf {
@ -46,7 +56,7 @@ pub trait FileSystemEnv: Sync + Send + std::fmt::Debug {
use std::io::{LineWriter, Write}; use std::io::{LineWriter, Write};
let path = self.in_cwd(file_name); let path = self.in_cwd(file_name);
info!("writing to {:?}", path); debug!("writing to {:?}", path);
let file = File::create(path.clone())?; let file = File::create(path.clone())?;
let mut file = LineWriter::new(file); let mut file = LineWriter::new(file);
file.write_all(content.as_bytes())?; file.write_all(content.as_bytes())?;
@ -63,7 +73,7 @@ pub trait FileSystemEnv: Sync + Send + std::fmt::Debug {
use std::io::Read; use std::io::Read;
let path = self.in_cwd(file_name); let path = self.in_cwd(file_name);
info!("reading from {:?}", path); debug!("reading from {:?}", path);
let mut file = File::open(path)?; let mut file = File::open(path)?;
let mut content = String::new(); let mut content = String::new();
file.read_to_string(&mut content)?; file.read_to_string(&mut content)?;
@ -72,41 +82,47 @@ pub trait FileSystemEnv: Sync + Send + std::fmt::Debug {
} }
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct RealFileSystemEnv { pub struct RealFileSystem {
cwd: PathBuf, cwd: PathBuf,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct TempFileSystemEnv { pub struct TempFileSystem {
cwd: PathBuf, cwd: PathBuf,
temp_dir: Arc<Mutex<TempDir>>,
// Handle to the temporary directory
// When this handle is dropped the directory is deleted
_temp_dir: Arc<Mutex<TempDir>>,
} }
impl FileSystemEnv for TempFileSystemEnv { impl FileSystemLike for TempFileSystem {
fn cwd(&self) -> &PathBuf { fn cwd(&self) -> &PathBuf {
&self.cwd &self.cwd
} }
} }
impl FileSystemEnv for RealFileSystemEnv { impl FileSystemLike for RealFileSystem {
fn cwd(&self) -> &PathBuf { fn cwd(&self) -> &PathBuf {
&self.cwd &self.cwd
} }
} }
impl RealFileSystemEnv { impl RealFileSystem {
const fn new(cwd: PathBuf) -> Self { const fn new(cwd: PathBuf) -> Self {
Self { cwd } Self { cwd }
} }
} }
impl TempFileSystemEnv { impl TempFileSystem {
fn new() -> std::io::Result<Self> { fn new() -> std::io::Result<Self> {
let temp_dir = tempdir()?; let temp_dir = tempdir()?;
info!("temp dir: {:?}", temp_dir.path()); info!("temp dir: {:?}", temp_dir.path());
let cwd = temp_dir.path().to_path_buf(); let cwd = temp_dir.path().to_path_buf();
let temp_dir = Arc::new(Mutex::new(temp_dir)); let temp_dir = Arc::new(Mutex::new(temp_dir));
Ok(Self { cwd, temp_dir }) Ok(Self {
cwd,
_temp_dir: temp_dir,
})
} }
} }
@ -120,13 +136,13 @@ mod tests {
#[test_log::test] #[test_log::test]
fn test_cwd() { fn test_cwd() {
let cwd = PathBuf::from("/tmp"); let cwd = PathBuf::from("/tmp");
let env = RealFileSystemEnv::new(cwd.clone()); let env = RealFileSystem::new(cwd.clone());
assert_eq!(env.cwd(), &cwd); assert_eq!(env.cwd(), &cwd);
} }
#[test_log::test] #[test_log::test]
fn test_create_on_temp_fs() -> std::io::Result<()> { fn test_create_on_temp_fs() -> std::io::Result<()> {
let env = TempFileSystemEnv::new()?; let env = TempFileSystem::new()?;
assert!(env.cwd().exists()); assert!(env.cwd().exists());
Ok(()) Ok(())
} }
@ -134,13 +150,13 @@ mod tests {
#[test_log::test] #[test_log::test]
fn test_create_on_real_fs() { fn test_create_on_real_fs() {
let cwd = PathBuf::from("/tmp"); let cwd = PathBuf::from("/tmp");
let env = RealFileSystemEnv::new(cwd.clone()); let env = RealFileSystem::new(cwd.clone());
assert_eq!(env.cwd(), &cwd); assert_eq!(env.cwd(), &cwd);
} }
#[test_log::test] #[test_log::test]
fn test_write_and_read_file() -> std::io::Result<()> { fn test_write_and_read_file() -> std::io::Result<()> {
let env = TempFileSystemEnv::new()?; let env = TempFileSystem::new()?;
let file_name = "test.txt"; let file_name = "test.txt";
let content = "Hello, World!"; let content = "Hello, World!";
let path = env.write_file(file_name, content)?; let path = env.write_file(file_name, content)?;

37
src/fs/dir_item.rs Normal file
View file

@ -0,0 +1,37 @@
use std::{
fs::{DirEntry, ReadDir},
path::PathBuf,
};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum DirItem {
File(PathBuf),
Dir(PathBuf),
SymLink(PathBuf),
Fifo(PathBuf),
Unsupported(PathBuf),
}
#[derive(Debug, derive_more::Constructor)]
pub struct DirItemIterator(ReadDir);
impl Iterator for DirItemIterator {
type Item = super::Result<DirItem>;
fn next(&mut self) -> Option<Self::Item> {
self.0.next().map(map_dir_item)
}
}
fn map_dir_item(item: std::io::Result<DirEntry>) -> super::Result<DirItem> {
let item = item?;
let file_type = item.file_type()?;
if file_type.is_dir() {
Ok(DirItem::Dir(item.path()))
} else if file_type.is_file() {
Ok(DirItem::File(item.path()))
} else if file_type.is_symlink() {
Ok(DirItem::SymLink(item.path()))
} else {
Ok(DirItem::Unsupported(item.path()))
}
}

23
src/fs/like.rs Normal file
View file

@ -0,0 +1,23 @@
use crate::fs::DirItem;
use super::Result;
use std::path::{Path, PathBuf};
pub trait FileSystemLike {
fn base(&self) -> &Path;
fn dir_create(&self, path: &Path) -> Result<()>;
fn dir_create_all(&self, path: &Path) -> Result<()>;
/// Reads the items in a directory and returns them as an iterator.
fn dir_read(&self, path: &Path) -> Result<Box<dyn Iterator<Item = Result<DirItem>>>>;
fn file_read_to_string(&self, path: &Path) -> Result<String>;
fn file_write(&self, path: &Path, contents: &str) -> Result<()>;
fn path_exists(&self, path: &Path) -> Result<bool>;
fn path_is_dir(&self, path: &Path) -> Result<bool>;
fn path_is_file(&self, path: &Path) -> Result<bool>;
fn path_of(&self, path: PathBuf) -> Result<PathBuf>;
}

56
src/fs/mod.rs Normal file
View file

@ -0,0 +1,56 @@
use std::path::PathBuf;
use derive_more::From;
use crate::fs::like::FileSystemLike;
mod like;
mod real;
mod temp;
mod dir_item;
pub use dir_item::DirItem;
pub use dir_item::DirItemIterator;
#[derive(Debug, From, derive_more::Display)]
pub enum Error {
Io(std::io::Error),
#[display("Path access attempted outside of base ({base:?}): {path:?}")]
PathTraversal {
base: PathBuf,
path: PathBuf,
},
#[display("Path must be a directory: {path:?}")]
NotADirectory {
path: PathBuf,
},
}
impl std::error::Error for Error {}
pub type Result<T> = core::result::Result<T, Error>;
pub const fn new(base: PathBuf) -> FileSystem {
FileSystem::Real(real::new(base))
}
pub fn temp() -> Result<FileSystem> {
temp::new().map(FileSystem::Temp)
}
#[derive(Clone, Debug)]
pub enum FileSystem {
Real(real::RealFileSystem),
Temp(temp::TempFileSystem),
}
impl std::ops::Deref for FileSystem {
type Target = dyn FileSystemLike;
fn deref(&self) -> &Self::Target {
match self {
Self::Real(fs) => fs,
Self::Temp(fs) => fs.deref(),
}
}
}

98
src/fs/real.rs Normal file
View file

@ -0,0 +1,98 @@
use std::path::{Path, PathBuf};
use crate::fs::{DirItem, DirItemIterator};
pub const fn new(base: PathBuf) -> RealFileSystem {
RealFileSystem { base }
}
#[derive(Clone, Debug)]
pub struct RealFileSystem {
base: PathBuf,
}
impl super::FileSystemLike for RealFileSystem {
fn base(&self) -> &Path {
&self.base
}
fn dir_create(&self, path: &Path) -> super::Result<()> {
self.validate(path)?;
std::fs::create_dir(path).map_err(Into::into)
}
fn dir_create_all(&self, path: &Path) -> super::Result<()> {
self.validate(path)?;
std::fs::create_dir_all(path).map_err(Into::into)
}
fn dir_read(
&self,
path: &Path,
) -> super::Result<Box<dyn Iterator<Item = super::Result<DirItem>>>> {
self.validate(path)?;
if !self.path_is_dir(path)? {
return Err(super::Error::NotADirectory {
path: path.to_path_buf(),
});
}
let read_dir = std::fs::read_dir(path)?;
Ok(Box::new(DirItemIterator::new(read_dir)))
}
fn file_read_to_string(&self, path: &Path) -> super::Result<String> {
self.validate(path)?;
std::fs::read_to_string(path).map_err(Into::into)
}
fn file_write(&self, path: &Path, contents: &str) -> super::Result<()> {
self.validate(path)?;
std::fs::write(path, contents).map_err(Into::into)
}
fn path_exists(&self, path: &Path) -> super::Result<bool> {
self.validate(path)?;
Ok(path.exists())
}
fn path_is_dir(&self, path: &Path) -> super::Result<bool> {
self.validate(path)?;
Ok(path.is_dir())
}
fn path_is_file(&self, path: &Path) -> super::Result<bool> {
self.validate(path)?;
Ok(path.is_file())
}
fn path_of(&self, path: PathBuf) -> super::Result<PathBuf> {
let path_of = self.base.as_path().join(path);
self.validate(&path_of)?;
Ok(path_of)
}
}
impl RealFileSystem {
fn validate(&self, path: &Path) -> super::Result<()> {
let path = self.clean_path(path)?;
if !path.starts_with(&self.base) {
return Err(super::Error::PathTraversal {
base: self.base.clone(),
path,
});
}
Ok(())
}
fn clean_path(&self, path: &Path) -> super::Result<PathBuf> {
// let path = path.as_ref();
use path_clean::PathClean;
let abs_path = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()?.join(path)
}
.clean();
Ok(abs_path)
}
}

29
src/fs/temp.rs Normal file
View file

@ -0,0 +1,29 @@
use std::sync::{Arc, Mutex};
use tempfile::TempDir;
pub(super) fn new() -> super::Result<TempFileSystem> {
let temp_dir = tempfile::tempdir()?;
let base = temp_dir.path().to_path_buf();
let temp_dir = Arc::new(Mutex::new(temp_dir));
let real = super::real::new(base);
Ok(TempFileSystem {
real,
_temp_dir: temp_dir,
})
}
#[derive(Clone, Debug)]
pub struct TempFileSystem {
real: super::real::RealFileSystem,
_temp_dir: Arc<Mutex<TempDir>>,
}
impl std::ops::Deref for TempFileSystem {
type Target = dyn super::FileSystemLike;
fn deref(&self) -> &Self::Target {
&self.real
}
}

View file

@ -1,2 +1,11 @@
#[cfg(feature = "fs")]
pub mod filesystem; pub mod filesystem;
#[cfg(feature = "fs")]
pub mod fs;
#[cfg(feature = "network")]
pub mod network; pub mod network;
#[cfg(test)]
mod tests;

View file

@ -36,3 +36,9 @@ impl<T> NetResponse<T> {
self.response_body self.response_body
} }
} }
impl From<NetResponse<()>> for () {
fn from(_value: NetResponse<()>) -> Self {
// ()
}
}

View file

@ -169,7 +169,7 @@ impl NetworkTrait for RealNetwork {
&self, &self,
net_request: NetRequest, net_request: NetRequest,
) -> Result<NetResponse<T>, NetworkError> { ) -> Result<NetResponse<T>, NetworkError> {
tracing::info!("RealNetworkEnv::get({:?})", net_request); tracing::debug!("RealNetworkEnv::get({:?})", net_request);
let url = net_request.url(); let url = net_request.url();
let auth = net_request.auth(); let auth = net_request.auth();
let response_type = net_request.response_type(); let response_type = net_request.response_type();

12
src/tests/filesystem.rs Normal file
View file

@ -0,0 +1,12 @@
type TestResult = Result<(), Box<dyn std::error::Error>>;
#[test]
fn write_read_file_exists() -> TestResult {
let fs = crate::filesystem::temp()?;
let pathbuf = fs.write_file("foo", "content")?;
let c = fs.read_file("foo")?;
assert_eq!(c, "content");
assert!(fs.file_exists(&pathbuf));
Ok(())
}

294
src/tests/fs.rs Normal file
View file

@ -0,0 +1,294 @@
use assert2::let_assert;
use crate::fs;
type TestResult = Result<(), fs::Error>;
mod path_of {
use super::*;
#[test]
fn validate_fails_on_path_traversal() -> TestResult {
let fs = fs::temp()?;
let_assert!(Err(fs::Error::PathTraversal { base, path: _path }) = fs.path_of("..".into()));
assert_eq!(base, fs.base());
Ok(())
}
}
mod file {
use super::*;
#[test]
/// Write to a file, read it, verify it exists, is a file and has the expected contents
fn write_read_file_exists() -> TestResult {
let fs = fs::temp()?;
let pathbuf = fs.base().join("foo");
let_assert!(Ok(_) = fs.file_write(&pathbuf, "content"));
let_assert!(
Ok(c) = fs.file_read_to_string(&pathbuf),
"file_read_to_string"
);
assert_eq!(c, "content");
let_assert!(Ok(exists) = fs.path_exists(&pathbuf));
assert!(exists);
let_assert!(Ok(is_file) = fs.path_is_file(&pathbuf));
assert!(is_file);
Ok(())
}
}
mod dir_create {
use super::*;
#[test]
fn should_create_a_dir() -> TestResult {
let fs = fs::temp()?;
let pathbuf = fs.base().join("subdir");
let_assert!(Ok(_) = fs.dir_create(&pathbuf));
let_assert!(Ok(exists) = fs.path_exists(&pathbuf));
assert!(exists);
let_assert!(Ok(is_dir) = fs.path_is_dir(&pathbuf));
assert!(is_dir);
Ok(())
}
#[test]
fn should_fail_on_path_traversal() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("..").join("foo");
let_assert!(
Err(fs::Error::PathTraversal {
base: _base,
path: _path
}) = fs.dir_create(&path)
);
Ok(())
}
}
mod dir_create_all {
use super::*;
#[test]
fn should_create_a_dir() -> TestResult {
let fs = fs::temp()?;
let pathbuf = fs.base().join("subdir").join("child");
let_assert!(Ok(_) = fs.dir_create_all(&pathbuf));
let_assert!(Ok(exists) = fs.path_exists(&pathbuf));
assert!(exists, "path exists");
let_assert!(Ok(is_dir) = fs.path_is_dir(&pathbuf));
assert!(is_dir, "path is a directory");
Ok(())
}
#[test]
fn should_fail_on_path_traversal() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("..").join("foo");
let_assert!(
Err(fs::Error::PathTraversal {
base: _base,
path: _path
}) = fs.dir_create_all(&path)
);
Ok(())
}
}
mod dir_dir_read {
use crate::fs::DirItem;
use super::*;
#[test]
fn should_return_dir_items() -> TestResult {
let fs = fs::temp()?;
let file1 = fs.base().join("file-1");
let dir = fs.base().join("dir");
let file2 = dir.join("file-2");
fs.file_write(&file1, "file-1")?;
fs.dir_create(&dir)?;
fs.file_write(&file2, "file-2")?;
let items = fs
.dir_read(fs.base())?
.filter_map(|i| i.ok())
.collect::<Vec<_>>();
assert_eq!(items.len(), 2);
assert!(items.contains(&DirItem::File(file1)));
assert!(items.contains(&DirItem::Dir(dir)));
Ok(())
}
#[test]
fn should_fail_on_not_a_dir() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("file");
fs.file_write(&path, "contents")?;
let_assert!(Err(fs::Error::NotADirectory { path: _path }) = fs.dir_read(&path));
Ok(())
}
#[test]
fn should_fail_on_path_traversal() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("..").join("foo");
let_assert!(
Err(fs::Error::PathTraversal {
base: _base,
path: _path
}) = fs.dir_read(&path)
);
Ok(())
}
}
mod path_exists {
use super::*;
#[test]
fn should_be_true_when_it_exists() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("foo");
let_assert!(Ok(_) = fs.file_write(&path, "bar"));
let_assert!(Ok(exists) = fs.path_exists(&path));
assert!(exists);
Ok(())
}
#[test]
fn should_be_false_when_it_does_not_exist() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("foo");
let_assert!(Ok(exists) = fs.path_exists(&path));
assert!(!exists);
Ok(())
}
#[test]
fn should_fail_on_path_traversal() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("..").join("foo");
let_assert!(
Err(fs::Error::PathTraversal {
base: _base,
path: _path
}) = fs.path_exists(&path)
);
Ok(())
}
}
mod path_is_dir {
use super::*;
#[test]
fn should_be_true_when_is_a_dir() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("foo");
let_assert!(Ok(_) = fs.dir_create(&path));
let_assert!(Ok(is_dir) = fs.path_is_dir(&path));
assert!(is_dir);
Ok(())
}
#[test]
fn should_be_false_when_is_a_file() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("foo");
let_assert!(Ok(_) = fs.file_write(&path, "bar"));
let_assert!(Ok(is_dir) = fs.path_is_dir(&path));
assert!(!is_dir);
Ok(())
}
#[test]
#[ignore]
fn should_be_false_when_is_a_link() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("foo");
// TODO create a link
// let_assert!(Ok(_) = fs.file_write(&path, "bar"));
let_assert!(Ok(is_dir) = fs.path_is_dir(&path));
assert!(!is_dir);
Ok(())
}
#[test]
fn should_fail_on_path_traversal() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("..").join("foo");
let_assert!(
Err(fs::Error::PathTraversal {
base: _base,
path: _path
}) = fs.path_is_dir(&path)
);
Ok(())
}
}
mod path_is_file {
use super::*;
#[test]
fn should_be_true_when_is_a_file() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("foo");
let_assert!(Ok(_) = fs.file_write(&path, "bar"));
let_assert!(Ok(is_file) = fs.path_is_file(&path));
assert!(is_file);
Ok(())
}
#[test]
fn should_be_false_when_is_a_dir() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("foo");
let_assert!(Ok(_) = fs.dir_create(&path));
let_assert!(Ok(is_file) = fs.path_is_file(&path));
assert!(!is_file);
Ok(())
}
#[test]
#[ignore]
fn should_be_false_when_is_a_link() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("foo");
// TODO create a link
// let_assert!(Ok(_) = fs.file_write(&path, "bar"));
let_assert!(Ok(is_file) = fs.path_is_file(&path));
assert!(!is_file);
Ok(())
}
#[test]
fn should_fail_on_path_traversal() -> TestResult {
let fs = fs::temp()?;
let path = fs.base().join("..").join("foo");
let_assert!(
Err(fs::Error::PathTraversal {
base: _base,
path: _path
}) = fs.path_is_file(&path)
);
Ok(())
}
}

8
src/tests/mod.rs Normal file
View file

@ -0,0 +1,8 @@
#[cfg(feature = "fs")]
pub mod filesystem;
#[cfg(feature = "fs")]
pub mod fs;
// #[cfg(feature = "network")]
// pub mod network;