diff --git a/.gitignore b/.gitignore index ad67955..34c08f3 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,12 @@ target # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + + +# Added by cargo + +/target + +public/ + +.*env diff --git a/README.md b/README.md index 73204f0..eb4d754 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,58 @@ -# Polytech DevOps - System Programming Project +# Ferrisshare — P2P file transfer -## Rules +Ferrisshare is a small Rust peer-to-peer file transfer toy used for a systems programming project. +It implements a tiny text-based protocol over TCP to send a single file from a sender (CLI) to a receiver (listener). +For a detailed protocol and architecture overview, see [docs/architecture.md](docs/architecture.md). -Students MUST pick one out of the [four proposed topics](topics). +## What it is -Before the project deadline, students MUST turn in an [architecture document](#architecture-and-documentation) and an [implementation](#implementation) for the selected topic, in the form of a [GitHub pull request](#project-contribution). +- A minimal CLI sender (`cli` binary) and a listener/receiver service (`ferrisshare` binary). +- Protocol highlights: `HELLO` to announce a file, `YEET` to send block headers followed by the raw block bytes, `MISSION-ACCOMPLISHED` then `BYE-RIS` to finish. +- Storage: receiver writes to a temporary `*.ferrisshare` file then renames to the final filename. -## Topics +## Quick run (local development) -Four different topics are proposed: +Prerequisites: -1. [A networking port scanner](topics/port-scanner). -1. [A peer-to-peer file transfer protocol](topics/p2p-transfer-protocol). -1. [A web scraper](topics/web-scraper). -1. [A tic-tac-toe AI agent](topics/tic-tac-toe). +- rust toolchain (stable) and cargo +- network access to localhost -## Grade +Build the workspace: -Your work will be evaluated based on the following criteria: +```bash +cargo build --workspace +``` -### Architecture and Documentation +Run the listener (receiver) on port 9000 (default): -**This accounts for 40% of the final grade** +```bash +# In one terminal +cargo run --bin ferrisshare +``` -You MUST provide a short, `markdown` formatted and English written document describing your project architecture. +Send a file with the CLI (sender). Example: send the repository `README.md` to localhost:9000 -It MUST live under a projects top level folder called `docs/`, e.g. `docs/architecture.md`. +```bash +# In another terminal +cargo run --bin cli -- send --addr 127.0.0.1:9000 --file README.md --block-size 2048 +``` -It SHOULD at least contain the following sections: +**Recommended minimal block size** -1. Project definition: What is it? What are the goals of the tool/project? -1. Components and modules: Describe which modules compose your project, and how they interact together. Briefly justify why you architectured it this way. -1. Usage: How can one use it? Give usage examples. +We recommend using a minimal block size of 2048 bytes (as shown in the example above). Larger blocks reduce protocol overhead and typically improve throughput for local transfers. Be aware larger blocks use more memory and may be less forgiving on very unreliable networks — adjust down if you see timeouts or memory pressure. -### Implementation +Logs printed to both terminals show the protocol exchange (HELLO, OK, YEET blocks, OK-HOUSTEN responses, MISSION-ACCOMPLISHED, SUCCESS, BYE-RIS). -**This accounts for 40% of the final grade** +## Notes and troubleshooting -The project MUST be implemented in Rust. +- The listener stores incoming data in `./.ferrisshare` during transfer and renames it to `./` after `MISSION-ACCOMPLISHED`. +- If you change block sizes on the sender, ensure they match the expected file split behavior. +- For debugging, run both binaries locally and watch logs. -The implementation MUST be formatted, build without warnings (including `clippy` warnings) and commented. +## Development -The implementation modules and crates MAY be unit tested. +- Tests: `cargo test` +- Formatting: `cargo fmt` +- Linting: `cargo clippy --all-targets --all-features -- -D warnings` -### Project Contribution - -**This accounts for 20% of the final grade** - -The project MUST be submitted as one single GitHub pull request (PR) against the [current](https://github.com/dev-sys-do/project-2427) repository, for the selected project. - -For example, a student picking the `p2p-transfer-protocol` topic MUST send a PR that adds all deliverables (source code, documentation) to the `topics/p2p-transfer-protocol/` folder. - -All submitted PRs will not be evaluated until the project deadline. They can thus be incomplete, rebased, closed, and modified until the project deadline. - -A pull request quality is evaluated on the following criteria: -* Commit messages: Each git commit message should provide a clear description and explanation of what the corresponding change brings and does. -* History: The pull request git history MUST be linear (no merge points) and SHOULD represent the narrative of the underlying work. It is a representation of the author's logical work in putting the implementation together. - -A very good reference on the topic: https://github.blog/developer-skills/github/write-better-commits-build-better-projects/ - -### Grade Factor - -All proposed topics have a grade factor, describing their relative complexity. - -The final grade is normalized against the selected topic's grade factor: `final_grade = grade * topic_grade_factor`. - -For example, a grade of `8/10` for a topic which grade factor is `1.1` will generate a final grade of `8.8/10`. - - -## Deadline - -All submitted PRs will be evaluated on October 30th, 2025 at 11:00 PM UTC. +If you want more detailed usage or a packaged distribution, tell me which format you prefer and I'll add it. diff --git a/topics/p2p-transfer-protocol/.env.example b/topics/p2p-transfer-protocol/.env.example new file mode 100644 index 0000000..1b636f8 --- /dev/null +++ b/topics/p2p-transfer-protocol/.env.example @@ -0,0 +1,3 @@ +FERRIS_BASE_PATH=./public +FERRIS_PORT=9000 +FERRIS_HOST=0.0.0.0 diff --git a/topics/p2p-transfer-protocol/Cargo.lock b/topics/p2p-transfer-protocol/Cargo.lock new file mode 100644 index 0000000..fc04718 --- /dev/null +++ b/topics/p2p-transfer-protocol/Cargo.lock @@ -0,0 +1,431 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "clap" +version = "4.5.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "ferrisshare" +version = "0.0.0" +dependencies = [ + "anyhow", + "async-trait", + "clap", + "dotenv", + "tokio", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.59.0", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" diff --git a/topics/p2p-transfer-protocol/Cargo.toml b/topics/p2p-transfer-protocol/Cargo.toml new file mode 100644 index 0000000..3a616cc --- /dev/null +++ b/topics/p2p-transfer-protocol/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "ferrisshare" +version = "0.0.0" +edition = "2024" + +[dependencies] +async-trait = "0.1.89" +clap = { version = "4.5.50", features = ["derive"] } +tokio = { version = "1", features = ["net", "rt-multi-thread", "rt", "fs", "io-util", "sync", "macros"] } +anyhow = "1.0" +dotenv = "0.15.0" + +[[bin]] +name = "cli" +path = "src/cli/main.rs" diff --git a/topics/p2p-transfer-protocol/README.md b/topics/p2p-transfer-protocol/README.md index 8d57551..eb4d754 100644 --- a/topics/p2p-transfer-protocol/README.md +++ b/topics/p2p-transfer-protocol/README.md @@ -1,29 +1,58 @@ -# A P2P Transfer Protocol +# Ferrisshare — P2P file transfer -## Description and Goal +Ferrisshare is a small Rust peer-to-peer file transfer toy used for a systems programming project. +It implements a tiny text-based protocol over TCP to send a single file from a sender (CLI) to a receiver (listener). +For a detailed protocol and architecture overview, see [docs/architecture.md](docs/architecture.md). -Build a CLI tool that allows two users on the same network to transfer a single file to each other. -The tool should be able to act as both the sender and the receiver, without a central server. +## What it is -It is expected for a sender to know the IP of the receiver, i.e. there is no discovery protocol. +- A minimal CLI sender (`cli` binary) and a listener/receiver service (`ferrisshare` binary). +- Protocol highlights: `HELLO` to announce a file, `YEET` to send block headers followed by the raw block bytes, `MISSION-ACCOMPLISHED` then `BYE-RIS` to finish. +- Storage: receiver writes to a temporary `*.ferrisshare` file then renames to the final filename. -```shell -# Receiving a file on port 9000 -p2p-tool listen --port 9000 --output ./shared +## Quick run (local development) -# Sending a file -p2p-tool send --file report.pdf --to 192.168.1.100 --port 9000 +Prerequisites: + +- rust toolchain (stable) and cargo +- network access to localhost + +Build the workspace: + +```bash +cargo build --workspace ``` -## Hints and Suggestions +Run the listener (receiver) on port 9000 (default): + +```bash +# In one terminal +cargo run --bin ferrisshare +``` + +Send a file with the CLI (sender). Example: send the repository `README.md` to localhost:9000 + +```bash +# In another terminal +cargo run --bin cli -- send --addr 127.0.0.1:9000 --file README.md --block-size 2048 +``` + +**Recommended minimal block size** + +We recommend using a minimal block size of 2048 bytes (as shown in the example above). Larger blocks reduce protocol overhead and typically improve throughput for local transfers. Be aware larger blocks use more memory and may be less forgiving on very unreliable networks — adjust down if you see timeouts or memory pressure. + +Logs printed to both terminals show the protocol exchange (HELLO, OK, YEET blocks, OK-HOUSTEN responses, MISSION-ACCOMPLISHED, SUCCESS, BYE-RIS). + +## Notes and troubleshooting + +- The listener stores incoming data in `./.ferrisshare` during transfer and renames it to `./` after `MISSION-ACCOMPLISHED`. +- If you change block sizes on the sender, ensure they match the expected file split behavior. +- For debugging, run both binaries locally and watch logs. -- Define and document a simple networking protocol with a few commands. For example - - HELLO: For the sender to offer a file to the receiver. It takes a file size argument. - - ACK: For the receiver to tell the sender it is ready to receive a proposed file. - - NACK: For the receiver to reject a proposed file. - - SEND: Send, for the sender to actually send a file. It also takes a file size argument, that must match the `HELLO` offer. -- Start a receiving thread for every sender connection. +## Development -## Grade Factor +- Tests: `cargo test` +- Formatting: `cargo fmt` +- Linting: `cargo clippy --all-targets --all-features -- -D warnings` -The grade factor for this project is *1*. +If you want more detailed usage or a packaged distribution, tell me which format you prefer and I'll add it. diff --git a/topics/p2p-transfer-protocol/docs/architecture.md b/topics/p2p-transfer-protocol/docs/architecture.md new file mode 100644 index 0000000..992ac8e --- /dev/null +++ b/topics/p2p-transfer-protocol/docs/architecture.md @@ -0,0 +1,200 @@ +# FerrisShare - P2P File Transfer Protocol + +## 1. Project Definition + +### What is FerrisShare? + +FerrisShare is a peer-to-peer (P2P) file transfer command-line tool that enables direct file sharing between two computers on the same network without requiring a central server. Built in Rust, it prioritizes reliability, concurrent connection handling, and a simple custom protocol for efficient file transfers. + +### Goals + +The primary goals of this project are: + +1. **Direct P2P Transfer**: Enable users to send files directly to another machine using only an IP address and port number +2. **Bidirectional Operation**: Support both sending and receiving modes within a single binary +3. **Concurrent Handling**: Allow a receiver to accept multiple simultaneous file transfers from different senders +4. **Reliability**: Implement a simple protocol with handshake verification to ensure successful transfers +5. **Simplicity**: Provide a straightforward CLI interface similar to common networking tools + +- **Resume Support**: Interrupted transfers cannot be resumed +- **Multi-file Transfers**: Each transfer handles exactly one file + +### 1.1 **Choice of Dependencies** + +This project uses a small set of well-established crates chosen to support an async, networked CLI tool implemented in Rust. Below are the main dependencies and why they were selected. + +#### Tokio + +Tokio is the async runtime and is central to the project. Reasons for using Tokio include: + +1. **Asynchronous I/O efficiency** – Tokio leverages Rust’s `async/await` syntax to handle many simultaneous client connections without blocking threads. +2. **Task scheduling and runtime** – Tokio includes a lightweight task scheduler that runs asynchronous functions concurrently on a single or multi-threaded runtime. +3. **Ecosystem integration** – Many crates (like `warp`, `hyper`, `reqwest`, `tokio-tungstenite`) are built on top of Tokio, ensuring compatibility and extensibility. +4. **Performance and safety** – The runtime is optimized for low-latency operations while preserving Rust’s memory- and thread-safety guarantees. + +Practical notes for this repo: + +- Tokio primitives used: `TcpListener`, `TcpStream`, `tokio::spawn`, `tokio::fs`, and `tokio::sync::mpsc`. +- The code creates an `mpsc::channel::(1)` in `src/main.rs` and sends accepted `TcpStream`s from the listener to the handler task. This decouples socket acceptance from protocol handling, provides backpressure (buffer size 1), and keeps a clear service boundary between network IO and command processing. +- When changing concurrency or channel buffer sizes, review the places that consume the channel (network handler) and tests that rely on the current backpressure semantics. + +#### clap + +`clap` (with the `derive` feature) is used for command-line parsing. It provides ergonomic derive-based parsing for flags and subcommands used by the binaries (see `src/cli/main.rs`). Use `clap` to add user-facing options, help text, and subcommands. Keep CLI changes backward-compatible where possible. + +#### dotenv + +`dotenv` is used in `src/main.rs` to load local environment variables from a `.env` file during development. The project uses environment variables for configuration keys (see `src/application/config.rs`): `FERRIS_BASE_PATH`, `FERRIS_PORT`, and `FERRIS_HOST`. `Config::from_env()` provides sensible defaults when vars are absent. + +Other dependencies + +- `async-trait` — used to express async traits for domain ports/interfaces implemented by infra repositories. +- `anyhow` — convenience error handling for higher-level paths or tooling code. + +If you add dependencies, prefer small, widely-used crates and keep Tokio feature flags minimal to avoid pulling unnecessary code. + +### 1.2 **Overview** + +The protocol defines a simple, **text-based command layer** over TCP for transferring a single file between two peers on the same network. It relies on TCP for reliable, ordered delivery, while adding **application-level commands** to coordinate the transfer, manage file chunks, and confirm completion. The connection is **bi-directional**, allowing the receiver to respond directly through the same TCP stream. + +![FerrisShare protocol flow](./ferrisshare_logigramme_v1.png) + +#### **Protocol Commands** + +| Command | Sender | Arguments | Response | Description | +| ------------------------ | ------ | ------------------------------------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------ | +| **HELLO** | Client | ` ` | `OK` / `NOPE ` | Initiates the file transfer and informs the receiver about the file name and size. | +| **OK** | Server | — | — | Confirms acceptance of the file transfer. | +| **NOPE** | Server | `` | — | Refuses the transfer (e.g., file exists, insufficient space). | +| **YEET** | Client | ` ` + binary data | `OK-HOUSTEN ` | Sends one block of the file to the receiver. Blocks are fixed or variable size. | +| **OK-HOUSTEN** | Server | `` | — | Confirms the block was received and written correctly. Optional but recommended for integrity. | +| **MISSION-ACCOMPLISHED** | Client | — | `SUCCESS` / `ERROR` | Marks the end of file transmission. The server verifies that all blocks were received correctly. | +| **BYE-RIS** | Either | — | — | Gracefully terminates or cancels the transfer. | + +## 2. **High-Level Architecture** + +### 2.1 Overview + +FerrisShare is organized following a **hexagonal (ports and adapters)** architecture. +The system is divided into three primary layers: + +1. **Core Domain (`src/core/domain`)** + Defines the business logic, entities, and service traits (ports). + It is _infrastructure-agnostic_ and models how files are transferred, validated, and finalized. + +2. **Application Layer (`src/application`)** + Orchestrates interactions between domain services and infrastructure. + It is responsible for: + + - managing runtime state (via `FerrisShareState`), + - loading configuration from the environment (via `main.rs`), + - wiring dependencies and initializing services (via `main.rs`). + +3. **Infrastructure Layer (`src/infra`)** + Provides concrete implementations of domain ports, such as: + + - file-system repositories (`fs_storage_repository.rs`), + +This separation ensures that **business logic remains pure** and testable while the infrastructure can evolve independently (e.g., changing from filesystem to S3 storage would only require a new repository implementing the same trait). + +--- + +## 3. **Runtime Model** + +FerrisShare uses a bounded Tokio mpsc channel (mpsc::channel::(1)) to forward accepted TcpStream connections from the listener task to the network handler. This decouples socket acceptance from protocol processing, provides backpressure (buffer size = 1) so the listener will await when the handler is busy, and enforces sequential handling of active connections. Do not change the channel semantics or buffer size without review — consumers and tests rely on the current backpressure behavior. + +### 3.1 Execution Flow + +![FerrisShare protocol flow](./ferrisshare_logigramme_v2.png) + +--- + +## 4. **Concurrency Model** + +FerrisShare uses Tokio’s cooperative multitasking model: + +| Component | Concurrency Mechanism | Description | +| --------------- | ----------------------------- | ------------------------------------------------------------------ | +| Listener | `tokio::spawn` task | Accepts TCP connections asynchronously. | +| Handler | `mpsc::Receiver` | Sequentially handles active connections (bounded by channel size). | +| File IO | `tokio::fs` | Asynchronous file operations for write and rename. | +| CPU-bound Tasks | `tokio::task::spawn_blocking` | Used for checksum validation or heavy file operations. | + +> The bounded channel (`size = 1`) acts as a **backpressure control**, ensuring the runtime does not accept more concurrent transfers than it can safely process. + +--- + +## 5. **Storage Design** + +### 5.1 Storage Repository + +The **FSStorageRepository** provides a file-based implementation of the `StorageRepository` trait defined in `core/domain/storage/ports.rs`. + +Responsibilities: + +- Validate and sanitize filenames to prevent directory traversal. +- Create a temporary file with suffix `.ferrisshare`. +- Write incoming blocks asynchronously. +- Rename the file to its final name once all blocks are received. + +Error handling is implemented using a domain-level `StorageError` enum, with variants such as: + +- `InvalidPath` +- `WriteError` +- `FinalizeError` +- `ChecksumMismatch` + +--- + +## 6. **Error Management** + +The architecture distinguishes between **domain errors** and **infrastructure errors**: + +| Layer | Error Type | Description | +| ----------- | ------------------------------------ | ------------------------------------------------------ | +| Domain | `StorageError`, `ProtocolError` | Typed errors expressing semantic issues. | +| Infra | `std::io::Error`, `tokio::io::Error` | Low-level I/O or network errors. | +| Application | `anyhow::Error` | Aggregation or propagation wrapper for untyped errors. | + +Each boundary maps its errors upward in a controlled way. For example: + +```rust +fn write_block(&self, block: YeetBlock) -> Result<(), StorageError> +``` + +is converted to `anyhow::Error` only at the CLI or handler layer. + +--- + +## 7. Testing Strategy + +- Quick protocol smoke-test with netcat: + ```bash + nc 127.0.0.1 9000 + HELLO test.txt 1024 + ``` +- Recommended manual verification steps (use when iterating implementation-by-feature): + 1. Start with the listener and the bounded `mpsc` channel; confirm accepted `TcpStream`s are queued and backpressure occurs when full. + 2. Implement protocol command recognition (parser unit tests). + 3. Implement responder behavior and verify correct textual responses (`OK`, `NOPE`, `OK-HOUSTEN`). + 4. Enforce protocol rules and sequencing in the handler (reject invalid sequences). + 5. Execute command handling (dispatch commands to services and capture outcomes). + 6. Implement the `FSStorageRepository` and validate filename sanitization. + 7. Test reading binary payloads from the stream and async writing to the temp file. + 8. Add the CLI path to read a local file and stream its bytes over the connection; verify end-to-end transfer. + 9. Implement and test the loop over `YEET` blocks to ensure all bytes are written and blocks are acknowledged. +- For each manual step, codify a corresponding unit or integration test to prevent regressions. + +## 8. **Security Considerations** + +- All filenames are sanitized — no absolute or relative (`..`) paths allowed. +- Only local-network communication is assumed; for Internet usage, TLS must be added. +- The server rejects transfers when disk space is insufficient or when the file already exists. +- Protocol commands are ASCII-only to prevent injection or encoding ambiguities. + +--- + +## 9. **Conclusion** + +FerrisShare combines Rust’s async capabilities with a clean domain-driven design to deliver a lightweight, robust P2P file transfer CLI. +Its modular architecture (domain/application/infra separation), use of Tokio primitives, and simple custom protocol make it easy to extend while ensuring predictable runtime behavior and strong safety guarantees. diff --git a/topics/p2p-transfer-protocol/docs/ferrisshare_logigramme_v1.png b/topics/p2p-transfer-protocol/docs/ferrisshare_logigramme_v1.png new file mode 100644 index 0000000..a991a8b Binary files /dev/null and b/topics/p2p-transfer-protocol/docs/ferrisshare_logigramme_v1.png differ diff --git a/topics/p2p-transfer-protocol/docs/ferrisshare_logigramme_v2.png b/topics/p2p-transfer-protocol/docs/ferrisshare_logigramme_v2.png new file mode 100644 index 0000000..22da0af Binary files /dev/null and b/topics/p2p-transfer-protocol/docs/ferrisshare_logigramme_v2.png differ diff --git a/topics/p2p-transfer-protocol/src/application/config.rs b/topics/p2p-transfer-protocol/src/application/config.rs new file mode 100644 index 0000000..5597bc5 --- /dev/null +++ b/topics/p2p-transfer-protocol/src/application/config.rs @@ -0,0 +1,23 @@ +#[derive(Debug)] +pub struct Config { + pub ferris_base_path: String, + pub ferris_port: u16, + pub ferris_host: String, +} + +impl Config { + pub fn from_env() -> Self { + let ferris_base_path = + std::env::var("FERRIS_BASE_PATH").unwrap_or_else(|_| "./public".to_string()); + let ferris_port = std::env::var("FERRIS_PORT") + .unwrap_or_else(|_| "9000".to_string()) + .parse() + .expect("FERRIS_PORT must be a valid u16"); + let ferris_host = std::env::var("FERRIS_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); + Config { + ferris_base_path, + ferris_port, + ferris_host, + } + } +} diff --git a/topics/p2p-transfer-protocol/src/application/ferrisshare_state.rs b/topics/p2p-transfer-protocol/src/application/ferrisshare_state.rs new file mode 100644 index 0000000..f3e3c47 --- /dev/null +++ b/topics/p2p-transfer-protocol/src/application/ferrisshare_state.rs @@ -0,0 +1,24 @@ +use crate::core::domain::{command::ports::CommandService, network::ports::NetworkService}; + +#[derive(Clone, Copy)] +pub struct FerrisShareState +where + C: CommandService, + N: NetworkService, +{ + pub command_service: C, + pub network_service: N, +} + +impl FerrisShareState +where + C: CommandService + Clone + Send + Sync + 'static, + N: NetworkService + Clone + Send + Sync + 'static, +{ + pub fn new(command_service: C, network_service: N) -> Self { + FerrisShareState { + command_service, + network_service, + } + } +} diff --git a/topics/p2p-transfer-protocol/src/application/mod.rs b/topics/p2p-transfer-protocol/src/application/mod.rs new file mode 100644 index 0000000..aa758e8 --- /dev/null +++ b/topics/p2p-transfer-protocol/src/application/mod.rs @@ -0,0 +1,2 @@ +pub mod config; +pub mod ferrisshare_state; diff --git a/topics/p2p-transfer-protocol/src/cli/main.rs b/topics/p2p-transfer-protocol/src/cli/main.rs new file mode 100644 index 0000000..f2a071d --- /dev/null +++ b/topics/p2p-transfer-protocol/src/cli/main.rs @@ -0,0 +1,139 @@ +use std::path::PathBuf; + +use clap::{Args, Parser, Subcommand}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpStream; + +#[derive(Parser)] +#[command(name = "ferris-cli")] +#[command(about = "CLI to communicate with ferrisshare listener", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Send a file to a ferrisshare listener + Send(SendArgs), + /// Simple ping (HELLO) for testing + Hello { + /// remote address (host:port) + #[arg(short, long, default_value = "127.0.0.1:9000")] + addr: String, + /// filename to announce + filename: String, + /// filesize in bytes + filesize: u64, + }, +} + +#[derive(Args)] +struct SendArgs { + /// remote address + #[arg(short, long, default_value = "127.0.0.1:9000")] + addr: String, + + /// file to send + #[arg(short, long)] + file: PathBuf, + + /// block size (default 1024) + #[arg(short = 'b', long, default_value_t = 1024u32)] + block_size: u32, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + + match cli.command { + Commands::Hello { + addr, + filename, + filesize, + } => { + let mut stream = TcpStream::connect(addr).await?; + let line = format!("HELLO {} {}\n", filename, filesize); + stream.write_all(line.as_bytes()).await?; + // read response + let mut buf = vec![0u8; 1024]; + let n = stream.read(&mut buf).await?; + if n > 0 { + let resp = String::from_utf8_lossy(&buf[..n]); + println!("Reply: {}", resp.trim()); + } + } + Commands::Send(args) => { + send_file(args).await?; + } + } + + Ok(()) +} + +async fn send_file(args: SendArgs) -> anyhow::Result<()> { + let file = tokio::fs::File::open(&args.file).await?; + let metadata = file.metadata().await?; + let filesize = metadata.len(); + let filename = args + .file + .file_name() + .and_then(|s| s.to_str()) + .ok_or_else(|| anyhow::anyhow!("Invalid filename"))? + .to_string(); + + let mut stream = TcpStream::connect(&args.addr).await?; + + // send HELLO + let hello = format!("HELLO {} {}\n", filename, filesize); + stream.write_all(hello.as_bytes()).await?; + + // wait for OK response (simple) + let mut resp_buf = vec![0u8; 1024]; + let n = stream.read(&mut resp_buf).await?; + if n > 0 { + println!("Server: {}", String::from_utf8_lossy(&resp_buf[..n]).trim()); + } + + // stream file and send YEET commands + binary blocks + let mut reader = tokio::fs::File::open(&args.file).await?; + let mut index: u64 = 0; + let mut buf = vec![0u8; args.block_size as usize]; + + loop { + let n = reader.read(&mut buf).await?; + if n == 0 { + break; + } + + // send YEET header + // checksum placeholder 0 for now + let yeet = format!("YEET {} {} {}\n", index, n, 0); + stream.write_all(yeet.as_bytes()).await?; + + // send binary block (raw bytes then newline) + stream.write_all(&buf[..n]).await?; + stream.write_all(b"\n").await?; + + // read ack + let n = stream.read(&mut resp_buf).await?; + if n > 0 { + println!("Server: {}", String::from_utf8_lossy(&resp_buf[..n]).trim()); + } + + index += 1; + } + + // send MISSION-ACCOMPLISHED + stream.write_all(b"MISSION-ACCOMPLISHED\n").await?; + let n = stream.read(&mut resp_buf).await?; + if n > 0 { + println!("Server: {}", String::from_utf8_lossy(&resp_buf[..n]).trim()); + } + + // send BYE-RIS + stream.write_all(b"BYE-RIS\n").await?; + + Ok(()) +} diff --git a/topics/p2p-transfer-protocol/src/core/domain/command/entities.rs b/topics/p2p-transfer-protocol/src/core/domain/command/entities.rs new file mode 100644 index 0000000..e9fd5d4 --- /dev/null +++ b/topics/p2p-transfer-protocol/src/core/domain/command/entities.rs @@ -0,0 +1,14 @@ +#[derive(Debug)] +pub enum CommandError { + InvalidCommand, + ExecutionFailed(String), +} + +impl From for String { + fn from(err: CommandError) -> Self { + match err { + CommandError::InvalidCommand => "Invalid command".to_string(), + CommandError::ExecutionFailed(msg) => format!("Command execution failed: {}", msg), + } + } +} diff --git a/topics/p2p-transfer-protocol/src/core/domain/command/mod.rs b/topics/p2p-transfer-protocol/src/core/domain/command/mod.rs new file mode 100644 index 0000000..2c88553 --- /dev/null +++ b/topics/p2p-transfer-protocol/src/core/domain/command/mod.rs @@ -0,0 +1,3 @@ +pub mod entities; +pub mod ports; +pub mod services; diff --git a/topics/p2p-transfer-protocol/src/core/domain/command/ports.rs b/topics/p2p-transfer-protocol/src/core/domain/command/ports.rs new file mode 100644 index 0000000..0f69f15 --- /dev/null +++ b/topics/p2p-transfer-protocol/src/core/domain/command/ports.rs @@ -0,0 +1,19 @@ +use std::sync::Arc; + +use crate::core::domain::{ + command::entities::CommandError, + network::entities::{ProtocolMessage, TransferState}, +}; + +pub trait CommandService: Send + Sync { + fn execute_protocol_command( + &self, + state: Arc>, + msg: &ProtocolMessage, + ) -> impl Future> + Send + Sync; + fn process_binary_data( + &self, + state: Arc>, + data: &[u8], + ) -> impl Future>; +} diff --git a/topics/p2p-transfer-protocol/src/core/domain/command/services.rs b/topics/p2p-transfer-protocol/src/core/domain/command/services.rs new file mode 100644 index 0000000..d9414ae --- /dev/null +++ b/topics/p2p-transfer-protocol/src/core/domain/command/services.rs @@ -0,0 +1,223 @@ +use std::sync::Arc; + +use crate::core::domain::{ + command::{entities::CommandError, ports::CommandService}, + network::entities::{ProtocolMessage, TransferState}, + storage::ports::StorageRepository, +}; + +#[derive(Clone)] +pub struct CommandServiceImpl +where + C: StorageRepository, +{ + storage: C, +} + +impl CommandServiceImpl +where + C: StorageRepository + Clone + Send + Sync + 'static, +{ + pub fn new(storage: C) -> Self { + CommandServiceImpl { storage } + } +} + +impl CommandService for CommandServiceImpl +where + C: StorageRepository + Clone + Send + Sync + 'static, +{ + async fn execute_protocol_command( + &self, + state: Arc>, + msg: &ProtocolMessage, + ) -> Result { + match msg { + ProtocolMessage::Hello { + filename: _filename, + filesize, + } => { + println!("Execute HELLO command."); + let expected_blocks = (*filesize + 1023).div_ceil(1024); + let mut state_guard = state.lock().await; + println!( + "Setting state to Receiving with expected_blocks={}", + expected_blocks + ); + *state_guard = TransferState::Receiving { + current_file: _filename.clone(), + expected_blocks, + focused_block: None, + received_blocks: Vec::with_capacity(expected_blocks as usize), + }; + + drop(state_guard); + + Ok(ProtocolMessage::Ok) + } + ProtocolMessage::Yeet(yeet_block) => { + let mut state_guard = state.lock().await; + let (expected_blocks, focused_block, received_blocks, current_file) = + match &mut *state_guard { + TransferState::Receiving { + expected_blocks, + focused_block, + received_blocks, + current_file, + } => ( + expected_blocks, + focused_block, + received_blocks, + current_file, + ), + _ => { + return Err(CommandError::ExecutionFailed( + "Error transfer state is not equal Receiving".to_string(), + )); + } + }; + + // Ensure we don't exceed the expected number of blocks. + if received_blocks.len() >= *expected_blocks as usize { + println!("Received all expected blocks."); + return Err(CommandError::ExecutionFailed( + "Received block index exceeds expected blocks".to_string(), + )); + } + + if let Some(focused_block) = focused_block + && !received_blocks.contains(&focused_block.index) + { + println!("Received expected block index: {}", focused_block.index); + return Err(CommandError::ExecutionFailed( + "Block not received. Can't proceed with next block.".to_string(), + )); + } + + // Reuse the mutable guard to update the state without locking again. + *state_guard = TransferState::Receiving { + current_file: current_file.clone(), + expected_blocks: *expected_blocks, + focused_block: Some(yeet_block.clone()), + received_blocks: received_blocks.clone(), + }; + + drop(state_guard); + + Ok(ProtocolMessage::Yeet(yeet_block.clone())) + } + ProtocolMessage::MissionAccomplished => { + let mut state_guard = state.lock().await; + let current_file = match &*state_guard { + TransferState::Receiving { current_file, .. } => current_file, + _ => { + return Err(CommandError::ExecutionFailed( + "Error transfer state is not equal Receiving".to_string(), + )); + } + }; + + self.storage.finalize(current_file).await.map_err(|e| { + CommandError::ExecutionFailed(format!("Storage error: {:?}", e)) + })?; + *state_guard = TransferState::Finished; + drop(state_guard); + Ok(ProtocolMessage::Success) + } + ProtocolMessage::ByeRis => { + *state.lock().await = TransferState::Closed; + Ok(ProtocolMessage::ByeRis) + } + _ => Err(CommandError::InvalidCommand), + } + } + + async fn process_binary_data( + &self, + state: Arc>, + data: &[u8], + ) -> Result { + // Lock once and extract what we need. + let mut state_guard = state.lock().await; + + let (_, maybe_focused_block, received_blocks_clone, current_file_clone) = + match &mut *state_guard { + TransferState::Receiving { + expected_blocks, + focused_block, + received_blocks, + current_file, + } => { + // take the focused block out (leaves None in the guard) + let taken_block = focused_block.take(); + ( + *expected_blocks, + taken_block, + received_blocks.clone(), + current_file.clone(), + ) + } + _ => { + return Err(CommandError::ExecutionFailed( + "Error transfer state is not equal Receiving".to_string(), + )); + } + }; + + // If there was no focused block, nothing to do. + let focused_block = match maybe_focused_block { + Some(b) => b, + None => { + println!("No focused block to store data for."); + return Ok(ProtocolMessage::Ok); + } + }; + + // If block already received, restore focused_block into the state and return. + if received_blocks_clone.contains(&focused_block.index) { + // restore focused_block back into the guard before returning + if let TransferState::Receiving { + focused_block: guard_focused_block, + .. + } = &mut *state_guard + { + *guard_focused_block = Some(focused_block.clone()); + } + println!("Block {} already received, ignoring.", focused_block.index); + return Ok(ProtocolMessage::Ok); + } + + println!("Stored binary data block: {:?}", focused_block); + + // Clone what we need for the async storage write, then drop the guard before awaiting. + let file_for_write = current_file_clone.clone(); + let block_for_write = focused_block.clone(); + drop(state_guard); + + // Perform the async write while not holding the mutex. + self.storage + .write_block(&file_for_write, &block_for_write, data) + .await + .map_err(|e| CommandError::ExecutionFailed(format!("Storage error: {:?}", e)))?; + + // Re-lock and update received_blocks + clear focused_block. + let mut state_guard = state.lock().await; + match &mut *state_guard { + TransferState::Receiving { + received_blocks, + focused_block, + .. + } => { + received_blocks.push(block_for_write.index); + *focused_block = None; + } + _ => { + return Err(CommandError::ExecutionFailed( + "Transfer state changed while writing block".to_string(), + )); + } + }; + + Ok(ProtocolMessage::OkHousten(block_for_write.index)) + } +} diff --git a/topics/p2p-transfer-protocol/src/core/domain/mod.rs b/topics/p2p-transfer-protocol/src/core/domain/mod.rs new file mode 100644 index 0000000..5635d3d --- /dev/null +++ b/topics/p2p-transfer-protocol/src/core/domain/mod.rs @@ -0,0 +1,3 @@ +pub mod command; +pub mod network; +pub mod storage; diff --git a/topics/p2p-transfer-protocol/src/core/domain/network/entities.rs b/topics/p2p-transfer-protocol/src/core/domain/network/entities.rs new file mode 100644 index 0000000..690b341 --- /dev/null +++ b/topics/p2p-transfer-protocol/src/core/domain/network/entities.rs @@ -0,0 +1,183 @@ +use std::convert::TryFrom; + +use crate::core::domain::storage::entities::YeetBlock; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ProtocolMessage { + Hello { + // "HELLO " + filename: String, + filesize: u64, + }, + Ok, // "OK" + Nope(String), // "NOPE " + Yeet(YeetBlock), // "YEET " + OkHousten(u64), // "OK-HOUSTEN " + MissionAccomplished, // "MISSION-ACCOMPLISHED" + Success, // "SUCCESS" + Error(String), // "ERROR " + ByeRis, // "BYE-RIS" +} + +#[derive(Debug)] +pub enum ProtocolError { + InvalidUtf8, + InvalidCommand, + MissingArgs, + InvalidNumber, + Incomplete, + CommandExecutionFailed(String), +} + +impl TryFrom<&str> for ProtocolMessage { + type Error = ProtocolError; + + fn try_from(value: &str) -> Result { + let line = value.trim(); + let tokens: Vec<&str> = line.split_whitespace().collect(); + + match tokens.first().copied() { + Some("HELLO") => { + let filename = tokens.get(1).ok_or(ProtocolError::MissingArgs)?.to_string(); + let filesize = tokens + .get(2) + .ok_or(ProtocolError::MissingArgs)? + .parse::() + .map_err(|_| ProtocolError::InvalidNumber)?; + Ok(ProtocolMessage::Hello { filename, filesize }) + } + Some("OK") => Ok(ProtocolMessage::Ok), + Some("NOPE") => { + if tokens.len() < 2 { + return Err(ProtocolError::MissingArgs); + } + let reason = tokens[1..].join(" "); + Ok(ProtocolMessage::Nope(reason)) + } + Some("YEET") => { + let block_index = tokens + .get(1) + .ok_or(ProtocolError::MissingArgs)? + .parse::() + .map_err(|_| ProtocolError::InvalidNumber)?; + let block_size = tokens + .get(2) + .ok_or(ProtocolError::MissingArgs)? + .parse::() + .map_err(|_| ProtocolError::InvalidNumber)?; + let check_sum = tokens + .get(3) + .ok_or(ProtocolError::MissingArgs)? + .parse::() + .map_err(|_| ProtocolError::InvalidNumber)?; + + Ok(ProtocolMessage::Yeet(YeetBlock::new( + block_index, + block_size, + check_sum, + ))) + } + Some("OK-HOUSTEN") => { + let block_index = tokens + .get(1) + .ok_or(ProtocolError::MissingArgs)? + .parse::() + .map_err(|_| ProtocolError::InvalidNumber)?; + Ok(ProtocolMessage::OkHousten(block_index)) + } + Some("MISSION-ACCOMPLISHED") => Ok(ProtocolMessage::MissionAccomplished), + Some("SUCCESS") => Ok(ProtocolMessage::Success), + Some("ERROR") => { + if tokens.len() < 2 { + return Err(ProtocolError::MissingArgs); + } + let reason = tokens[1..].join(" "); + Ok(ProtocolMessage::Error(reason)) + } + Some("BYE-RIS") => Ok(ProtocolMessage::ByeRis), + _ => Err(ProtocolError::InvalidCommand), + } + } +} + +impl From for String { + fn from(msg: ProtocolMessage) -> Self { + match msg { + ProtocolMessage::Hello { filename, filesize } => { + format!("HELLO {} {}", filename, filesize) + } + ProtocolMessage::Ok => "OK".to_string(), + ProtocolMessage::Nope(reason) => format!("NOPE {}", reason), + ProtocolMessage::Yeet(yeet_block) => format!( + "YEET {} {} {}", + yeet_block.index, yeet_block.size, yeet_block.checksum + ), + ProtocolMessage::OkHousten(block_index) => format!("OK-HOUSTEN {}", block_index), + ProtocolMessage::MissionAccomplished => "MISSION-ACCOMPLISHED".to_string(), + ProtocolMessage::Success => "SUCCESS".to_string(), + ProtocolMessage::Error(reason) => format!("ERROR: {}", reason), + ProtocolMessage::ByeRis => "BYE-RIS".to_string(), + } + } +} + +impl From for String { + fn from(err: ProtocolError) -> Self { + match err { + ProtocolError::InvalidUtf8 => "Invalid UTF-8 sequence".to_string(), + ProtocolError::InvalidCommand => "Invalid command".to_string(), + ProtocolError::MissingArgs => "Missing arguments".to_string(), + ProtocolError::InvalidNumber => "Invalid number format".to_string(), + ProtocolError::Incomplete => "Incomplete command".to_string(), + ProtocolError::CommandExecutionFailed(msg) => { + format!("Command execution failed: {}", msg) + } + } + } +} + +#[derive(Debug)] +pub enum NetworkError { + ListenerBindFailed(std::io::Error), + TransferInterrupted, + TooManyConnections, + ConnectionLost, + Timeout, + InvalidData, + ProtocolError(ProtocolError), +} + +impl From for NetworkError { + fn from(err: ProtocolError) -> Self { + NetworkError::ProtocolError(err) + } +} + +impl From for String { + fn from(err: NetworkError) -> Self { + match err { + NetworkError::ListenerBindFailed(e) => { + format!("ERROR Listener bind failed: {}", e) + } + NetworkError::TransferInterrupted => "ERROR Transfer interrupted".to_string(), + NetworkError::TooManyConnections => "ERROR Too many connections".to_string(), + NetworkError::ConnectionLost => "ERROR Connection lost".to_string(), + NetworkError::Timeout => "ERROR Timeout occurred".to_string(), + NetworkError::InvalidData => "ERROR Invalid data received".to_string(), + NetworkError::ProtocolError(proto_err) => String::from(proto_err), + } + } +} + +#[derive(Debug, Clone)] +pub enum TransferState { + Idle, + Receiving { + current_file: String, + expected_blocks: u64, + focused_block: Option, + received_blocks: Vec, + }, + Finished, + Closed, +} diff --git a/topics/p2p-transfer-protocol/src/core/domain/network/mod.rs b/topics/p2p-transfer-protocol/src/core/domain/network/mod.rs new file mode 100644 index 0000000..2c88553 --- /dev/null +++ b/topics/p2p-transfer-protocol/src/core/domain/network/mod.rs @@ -0,0 +1,3 @@ +pub mod entities; +pub mod ports; +pub mod services; diff --git a/topics/p2p-transfer-protocol/src/core/domain/network/ports.rs b/topics/p2p-transfer-protocol/src/core/domain/network/ports.rs new file mode 100644 index 0000000..f0240d3 --- /dev/null +++ b/topics/p2p-transfer-protocol/src/core/domain/network/ports.rs @@ -0,0 +1,31 @@ +use std::io::Error; + +use crate::core::domain::network::entities::{NetworkError, ProtocolError, ProtocolMessage}; +use tokio::{ + net::TcpStream, + sync::mpsc::{Receiver, Sender}, +}; + +pub trait NetworkService { + fn listener( + &self, + addr: &str, + tx: Sender, + ) -> impl Future> + Send; + fn handler(&self, rx: Receiver) -> impl Future>; + fn trust_protocol( + &self, + message: ProtocolMessage, + ) -> impl Future> + Send; + fn send_message( + &self, + stream: &mut TcpStream, + message: ProtocolMessage, + ) -> impl Future> + Send; +} + +pub trait NetworkClient { + fn connect(&self, addr: &str) -> Result<(), ProtocolError>; + fn disconnect(&self) -> Result<(), ProtocolError>; + fn receive_message(&self) -> Result; +} diff --git a/topics/p2p-transfer-protocol/src/core/domain/network/services.rs b/topics/p2p-transfer-protocol/src/core/domain/network/services.rs new file mode 100644 index 0000000..2df62ac --- /dev/null +++ b/topics/p2p-transfer-protocol/src/core/domain/network/services.rs @@ -0,0 +1,279 @@ +use std::io::Error; +use std::sync::Arc; +use std::sync::atomic::AtomicBool; + +use tokio::io::AsyncBufReadExt; +use tokio::io::AsyncReadExt; +use tokio::io::AsyncWriteExt; +use tokio::io::BufReader; +use tokio::net::TcpListener; +use tokio::net::TcpStream; +use tokio::sync::Mutex; +use tokio::sync::mpsc::Receiver; + +use crate::core::domain::command::ports::CommandService; +use crate::core::domain::network::entities::NetworkError; +use crate::core::domain::network::entities::ProtocolError; +use crate::core::domain::network::entities::ProtocolMessage; +use crate::core::domain::network::entities::TransferState; +use crate::core::domain::network::ports::NetworkService; + +#[derive(Debug, Clone)] +pub struct NetworkServiceImpl +where + C: CommandService, +{ + pub command_service: C, + active: Arc, + transfer_state: Arc>, +} + +impl NetworkServiceImpl +where + C: CommandService + Clone + Send + Sync + 'static, +{ + pub fn new(command_service: C) -> Self { + NetworkServiceImpl { + command_service, + active: Arc::new(AtomicBool::new(false)), + transfer_state: Arc::new(Mutex::new(TransferState::Idle)), + } + } + + pub async fn reset_transfer_state(&self) { + let mut state_guard = self.transfer_state.lock().await; + *state_guard = TransferState::Idle; + drop(state_guard); + } +} + +impl NetworkService for NetworkServiceImpl +where + C: CommandService + Clone + Send + Sync + 'static, +{ + async fn listener( + &self, + addr: &str, + tx: tokio::sync::mpsc::Sender, + ) -> Result<(), NetworkError> { + let listener = TcpListener::bind(addr) + .await + .map_err(NetworkError::ListenerBindFailed)?; + println!("Listening on {}", addr); + + loop { + let (mut stream, addr) = listener + .accept() + .await + .map_err(|_| NetworkError::ConnectionLost)?; + println!("New connection from {}", addr); + + // Vérifie si une connexion est déjà active + if !self.active.load(std::sync::atomic::Ordering::SeqCst) { + self.active.store(true, std::sync::atomic::Ordering::SeqCst); + } else { + eprintln!( + "A connection is already active. Rejecting new connection from {}", + addr + ); + let _ = stream.shutdown().await; + continue; + } + + // Essaye d’envoyer la connexion au handler + match tx.try_send(stream) { + Ok(_) => println!("Connection sent to handler."), + Err(e) => { + eprintln!("Failed to send connection to handler: {}", e); + // Optionally close the connection if it can't be handled + } + } + } + } + + async fn handler(&self, mut rx: Receiver) -> Result<(), Error> { + while let Some(stream) = rx.recv().await { + let (read_half, mut write_half) = stream.into_split(); + let mut reader = BufReader::new(read_half); + let mut buf = Vec::new(); + + loop { + buf.clear(); + + // Read one line (terminated by '\n'); returns 0 on EOF + let n = reader.read_until(b'\n', &mut buf).await?; + if n == 0 { + println!("Client disconnected."); + // Mark connection as inactive so listener can accept new ones + self.active + .store(false, std::sync::atomic::Ordering::SeqCst); + self.reset_transfer_state().await; + break; + } + + // Trim trailing LF/CRLF + if buf.ends_with(b"\n") { + buf.pop(); + } + if buf.ends_with(b"\r") { + buf.pop(); + } + + // Convert to &str and parse into your ProtocolMessage + let line = std::str::from_utf8(&buf) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + + match ProtocolMessage::try_from(line) { + Ok(msg) => { + println!("Received message: {:?}", msg); + + match self.trust_protocol(msg).await { + Ok(message) => match message { + ProtocolMessage::Yeet(yeet_block) => { + let mut bin_buf = vec![0u8; yeet_block.size as usize]; + if let Err(e) = reader.read_exact(&mut bin_buf).await { + eprintln!("Error reading binary block: {:?}", e); + let err_msg = ProtocolMessage::Error(String::from( + "Read binary failed", + )); + let s = String::from(err_msg) + "\n"; + let _ = write_half.write_all(s.as_bytes()).await; + continue; + } + + // Consume the trailing newline after the binary block if any. + let mut _end = Vec::new(); + let _ = reader.read_until(b'\n', &mut _end).await; + + // Forward the block to the command service for storage. + match self + .command_service + .process_binary_data( + Arc::clone(&self.transfer_state), + &bin_buf, + ) + .await + { + Ok(response_msg) => { + let s = String::from(response_msg) + "\n"; + if let Err(e) = write_half.write_all(s.as_bytes()).await + { + eprintln!("Error sending response: {:?}", e); + } + } + Err(e) => { + eprintln!("Error processing binary data: {:?}", e); + let err_msg = ProtocolMessage::Error(String::from(e)); + let s = String::from(err_msg) + "\n"; + let _ = write_half.write_all(s.as_bytes()).await; + } + } + } + other => { + // Non-YEET responses (OK, SUCCESS, etc.) are sent back to writer. + let s = String::from(other) + "\n"; + if let Err(e) = write_half.write_all(s.as_bytes()).await { + eprintln!("Error sending message: {:?}", e); + } + } + }, + Err(e) => eprintln!("Error handling protocol message: {:?}", e), + } + + let guard = self.transfer_state.lock().await; + match *guard { + TransferState::Closed => { + println!("Closing connection."); + self.active + .store(false, std::sync::atomic::Ordering::SeqCst); + drop(guard); + + self.reset_transfer_state().await; + // shutdown the write half + let _ = write_half.shutdown().await; + + break; + } + _ => { + continue; + } + } + } + Err(_) => { + if let Err(e) = self + .command_service + .process_binary_data(Arc::clone(&self.transfer_state), &buf) + .await + { + eprintln!("Error processing binary data: {:?}", e); + let err_msg = ProtocolMessage::Error(format!("{:?}", e)); + let s = String::from(err_msg) + "\n"; + let _ = write_half.write_all(s.as_bytes()).await; + } + } + } + } + } + + self.active + .store(false, std::sync::atomic::Ordering::SeqCst); + self.reset_transfer_state().await; + Ok(()) + } + + async fn trust_protocol( + &self, + message: ProtocolMessage, + ) -> Result { + let guard = self.transfer_state.lock().await; + match *guard { + TransferState::Idle => { + if !matches!(message, ProtocolMessage::Hello { .. }) { + return Err(ProtocolError::InvalidCommand); + } else { + println!("Transitioning from Idle to Receiving state."); + } + } + TransferState::Receiving { .. } => { + // Accept either a Yeet message or a MissionAccomplished message while receiving. + if !(matches!(message, ProtocolMessage::Yeet { .. }) + || matches!(message, ProtocolMessage::MissionAccomplished)) + { + return Err(ProtocolError::InvalidCommand); + } else { + println!("In Receiving state, processing Yeet or MissionAccomplished."); + } + } + TransferState::Finished => { + if !matches!(message, ProtocolMessage::ByeRis) { + return Err(ProtocolError::InvalidCommand); + } else { + println!("Transitioning from Finished to Closed state."); + } + } + _ => { + return Err(ProtocolError::InvalidCommand); + } + } + + drop(guard); // Release the lock before awaiting + + println!("Executing command: {:?}", message); + self.command_service + .execute_protocol_command(Arc::clone(&self.transfer_state), &message) + .await + .map_err(|e| ProtocolError::CommandExecutionFailed(format!("{:?}", e))) + } + + async fn send_message( + &self, + stream: &mut TcpStream, + message: ProtocolMessage, + ) -> Result<(), ProtocolError> { + let msg_str = String::from(message) + "\n"; + if let Err(e) = stream.write_all(msg_str.as_bytes()).await { + eprintln!("Failed to send message: {}", e); + } + Ok(()) + } +} diff --git a/topics/p2p-transfer-protocol/src/core/domain/storage/entities.rs b/topics/p2p-transfer-protocol/src/core/domain/storage/entities.rs new file mode 100644 index 0000000..9a51347 --- /dev/null +++ b/topics/p2p-transfer-protocol/src/core/domain/storage/entities.rs @@ -0,0 +1,51 @@ +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct YeetBlock { + pub index: u64, + pub size: u32, + pub checksum: u32, +} + +impl YeetBlock { + pub fn new(index: u64, size: u32, checksum: u32) -> Self { + YeetBlock { + index, + size, + checksum, + } + } +} + +pub struct File { + pub id: u64, + pub name: String, + pub size: u64, +} + +#[derive(Debug)] +pub enum StorageError { + FileNotFound, + PermissionDenied, + AlreadyExists, + AbsolutePathNotAllowed, + ParentDirSegmentNotAllowed, + InvalidFilename, + Unknown(String), +} + +impl From for String { + fn from(error: StorageError) -> Self { + match error { + StorageError::FileNotFound => "File not found".into(), + StorageError::PermissionDenied => "Permission denied".into(), + StorageError::AlreadyExists => "File already exists".into(), + StorageError::AbsolutePathNotAllowed => { + "Absolute paths are not allowed in filenames".into() + } + StorageError::ParentDirSegmentNotAllowed => { + "Parent directory segments are not allowed in filenames".into() + } + StorageError::InvalidFilename => "Invalid filename".into(), + StorageError::Unknown(msg) => format!("Unknown storage error: {}", msg), + } + } +} diff --git a/topics/p2p-transfer-protocol/src/core/domain/storage/mod.rs b/topics/p2p-transfer-protocol/src/core/domain/storage/mod.rs new file mode 100644 index 0000000..3ec695b --- /dev/null +++ b/topics/p2p-transfer-protocol/src/core/domain/storage/mod.rs @@ -0,0 +1,2 @@ +pub mod entities; +pub mod ports; diff --git a/topics/p2p-transfer-protocol/src/core/domain/storage/ports.rs b/topics/p2p-transfer-protocol/src/core/domain/storage/ports.rs new file mode 100644 index 0000000..30cea5a --- /dev/null +++ b/topics/p2p-transfer-protocol/src/core/domain/storage/ports.rs @@ -0,0 +1,15 @@ +use crate::core::domain::storage::entities::{StorageError, YeetBlock}; + +pub trait StorageRepository { + fn open_file(&self, filename: &str) -> impl Future> + Send; + fn write_block( + &self, + filename: &str, + block: &YeetBlock, + data: &[u8], + ) -> impl Future> + Send; + fn finalize( + &self, + filename: &str, + ) -> impl Future> + Send + Sync; +} diff --git a/topics/p2p-transfer-protocol/src/core/mod.rs b/topics/p2p-transfer-protocol/src/core/mod.rs new file mode 100644 index 0000000..d7abca1 --- /dev/null +++ b/topics/p2p-transfer-protocol/src/core/mod.rs @@ -0,0 +1 @@ +pub mod domain; diff --git a/topics/p2p-transfer-protocol/src/infra/mod.rs b/topics/p2p-transfer-protocol/src/infra/mod.rs new file mode 100644 index 0000000..21b552a --- /dev/null +++ b/topics/p2p-transfer-protocol/src/infra/mod.rs @@ -0,0 +1 @@ +pub mod repositories; diff --git a/topics/p2p-transfer-protocol/src/infra/repositories/fs/fs_storage_repository.rs b/topics/p2p-transfer-protocol/src/infra/repositories/fs/fs_storage_repository.rs new file mode 100644 index 0000000..91c61e1 --- /dev/null +++ b/topics/p2p-transfer-protocol/src/infra/repositories/fs/fs_storage_repository.rs @@ -0,0 +1,138 @@ +use std::future::Future; +use std::path::{Path, PathBuf}; +use tokio::io::{AsyncSeekExt, AsyncWriteExt}; + +use crate::core::domain::storage::{ + entities::{StorageError, YeetBlock}, + ports::StorageRepository, +}; + +#[derive(Clone)] +pub struct FSStorageRepository { + base_path: String, +} + +impl FSStorageRepository { + pub fn new(base_path: String) -> Self { + FSStorageRepository { base_path } + } + + // helper pour sécuriser le filename (simple) + fn sanitize_filename(filename: &str) -> Result<(), StorageError> { + let p = Path::new(filename); + // refuse les chemins absolus ou qui remontent (..). + if p.is_absolute() { + return Err(StorageError::AbsolutePathNotAllowed); + } + if p.components() + .any(|c| matches!(c, std::path::Component::ParentDir)) + { + return Err(StorageError::ParentDirSegmentNotAllowed); + } + // Optionnel: s'assurer qu'on a bien un file name (pas "dir/") + if p.file_name().is_none() { + return Err(StorageError::InvalidFilename); + } + Ok(()) + } + + fn file_path_for(&self, filename: &str) -> PathBuf { + PathBuf::from(&self.base_path).join(filename) + } +} + +impl StorageRepository for FSStorageRepository { + fn open_file(&self, filename: &str) -> impl Future> + Send { + let filename = filename.to_string(); + + async move { + // sanitize + FSStorageRepository::sanitize_filename(&filename)?; + + let path = self.file_path_for(&filename); + // Use a temporary extension during transfer + let part_path = path.with_extension("ferrisshare"); + + // create parent dirs if needed + if let Some(parent) = path.parent() { + tokio::fs::create_dir_all(parent) + .await + .map_err(|e| StorageError::Unknown(format!("Failed to create dir: {}", e)))?; + } + + match tokio::fs::File::create(&part_path).await { + Ok(_) => Ok(()), + Err(e) => Err(StorageError::Unknown(e.to_string())), + } + } + } + + fn write_block( + &self, + filename: &str, + block: &YeetBlock, + data: &[u8], + ) -> impl Future> + Send { + let filename = filename.to_string(); + let data = data.to_vec(); + let block_index = block.index; + + async move { + // sanitize + FSStorageRepository::sanitize_filename(&filename)?; + + let path = self.file_path_for(&filename); + // write into a .ferrisshare temporary file while transferring + let part_path = path.with_extension("ferrisshare"); + + // ensure parent dir exists before open + if let Some(parent) = path.parent() { + tokio::fs::create_dir_all(parent) + .await + .map_err(|e| StorageError::Unknown(format!("Failed to create dir: {}", e)))?; + } + + match tokio::fs::OpenOptions::new() + .create(true) + .truncate(false) + .write(true) + .open(&part_path) + .await + { + Ok(mut file) => { + let offset = block_index * block.size as u64; + if let Err(e) = file.seek(std::io::SeekFrom::Start(offset)).await { + return Err(StorageError::Unknown(e.to_string())); + } + if let Err(e) = file.write_all(&data).await { + return Err(StorageError::Unknown(e.to_string())); + } + // flush si tu veux (optionnel) + if let Err(e) = file.flush().await { + return Err(StorageError::Unknown(e.to_string())); + } + Ok(()) + } + Err(e) => Err(StorageError::Unknown(e.to_string())), + } + } + } + + fn finalize(&self, filename: &str) -> impl Future> + Send { + let base = self.base_path.clone(); + let filename = filename.to_string(); + + async move { + // sanitize + FSStorageRepository::sanitize_filename(&filename)?; + + let path = PathBuf::from(&base).join(&filename); + // Rename the .ferrisshare temp file to the final filename + let part = path.with_extension("ferrisshare"); + match tokio::fs::rename(&part, &path).await { + Ok(_) => Ok(()), + Err(e) => Err(StorageError::Unknown(e.to_string())), + } + } + } +} diff --git a/topics/p2p-transfer-protocol/src/infra/repositories/fs/mod.rs b/topics/p2p-transfer-protocol/src/infra/repositories/fs/mod.rs new file mode 100644 index 0000000..fa32d2c --- /dev/null +++ b/topics/p2p-transfer-protocol/src/infra/repositories/fs/mod.rs @@ -0,0 +1 @@ +pub mod fs_storage_repository; diff --git a/topics/p2p-transfer-protocol/src/infra/repositories/mod.rs b/topics/p2p-transfer-protocol/src/infra/repositories/mod.rs new file mode 100644 index 0000000..d521fbd --- /dev/null +++ b/topics/p2p-transfer-protocol/src/infra/repositories/mod.rs @@ -0,0 +1 @@ +pub mod fs; diff --git a/topics/p2p-transfer-protocol/src/lib.rs b/topics/p2p-transfer-protocol/src/lib.rs new file mode 100644 index 0000000..6ab7089 --- /dev/null +++ b/topics/p2p-transfer-protocol/src/lib.rs @@ -0,0 +1,3 @@ +pub mod application; +pub mod core; +pub mod infra; diff --git a/topics/p2p-transfer-protocol/src/main.rs b/topics/p2p-transfer-protocol/src/main.rs new file mode 100644 index 0000000..b29765c --- /dev/null +++ b/topics/p2p-transfer-protocol/src/main.rs @@ -0,0 +1,53 @@ +use dotenv::dotenv; +use std::sync::Arc; + +use tokio::{net::TcpStream, sync::mpsc}; + +use ferrisshare::{ + application::config::Config, + core::domain::{ + command::services::CommandServiceImpl, + network::{ports::NetworkService as _, services::NetworkServiceImpl}, + }, + infra::repositories::fs::fs_storage_repository::FSStorageRepository, +}; + +#[tokio::main] +async fn main() -> tokio::io::Result<()> { + dotenv().ok(); + let cfg: Config = Config::from_env(); + + let (tx, rx) = mpsc::channel::(1); + + let storage_repo = FSStorageRepository::new(cfg.ferris_base_path); + + let command_service = CommandServiceImpl::new(storage_repo); + let network_service = NetworkServiceImpl::new(command_service); + + let ferrisshare_state = Arc::new( + ferrisshare::application::ferrisshare_state::FerrisShareState::new( + network_service.command_service.clone(), + network_service.clone(), + ), + ); + + let ferrisshare_state_clone = ferrisshare_state.clone(); + + tokio::spawn(async move { + if let Err(e) = ferrisshare_state_clone.network_service.handler(rx).await { + eprintln!("Handler error: {}", e); + } + }); + + if let Err(e) = ferrisshare_state + .network_service + .listener( + format!("{}:{}", cfg.ferris_host, cfg.ferris_port).as_str(), + tx, + ) + .await + { + eprintln!("Listener error: {:?}", e); + } + Ok(()) +}