Compare commits
No commits in common. "main" and "1" have entirely different histories.
16 changed files with 713 additions and 1181 deletions
266
Cargo.lock
generated
266
Cargo.lock
generated
|
@ -105,13 +105,13 @@ checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.89"
|
version = "0.1.88"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -137,9 +137,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-lc-rs"
|
name = "aws-lc-rs"
|
||||||
version = "1.13.3"
|
version = "1.13.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba"
|
checksum = "08b5d4e069cbc868041a64bd68dc8cb39a0d79585cd6c5a24caa8c2d622121be"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-sys",
|
"aws-lc-sys",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
|
@ -221,7 +221,7 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -286,15 +286,15 @@ dependencies = [
|
||||||
"regex",
|
"regex",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"shlex",
|
"shlex",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
"which",
|
"which",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.9.4"
|
version = "2.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
|
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
|
@ -313,9 +313,9 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.23.2"
|
version = "1.23.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677"
|
checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
|
@ -346,11 +346,10 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.35"
|
version = "1.2.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "590f9024a68a8c40351881787f1934dc11afd69090f5edb6831464694d836ea3"
|
checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
|
||||||
"jobserver",
|
"jobserver",
|
||||||
"libc",
|
"libc",
|
||||||
"shlex",
|
"shlex",
|
||||||
|
@ -367,9 +366,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.3"
|
version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
|
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
|
@ -398,9 +397,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.47"
|
version = "4.5.43"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931"
|
checksum = "50fd97c9dc2399518aa331917ac6f274280ec5eb34e555dd291899745c48ec6f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
|
@ -408,9 +407,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.47"
|
version = "4.5.43"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6"
|
checksum = "c35b5830294e1fa0462034af85cc95225a4cb07092c088c55bda3147cfcd8f65"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
|
@ -420,14 +419,14 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_derive"
|
name = "clap_derive"
|
||||||
version = "4.5.47"
|
version = "4.5.41"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c"
|
checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -547,7 +546,7 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"strsim",
|
"strsim",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -558,14 +557,14 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.5.3"
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc"
|
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
]
|
]
|
||||||
|
@ -609,7 +608,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -627,7 +626,7 @@ dependencies = [
|
||||||
"enum-ordinalize",
|
"enum-ordinalize",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -662,7 +661,7 @@ checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -713,12 +712,6 @@ dependencies = [
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "find-msvc-tools"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e178e4fba8a2726903f6ba98a6d221e76f9c12c650d5dc0e6afdc50677b49650"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
|
@ -748,9 +741,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.2.2"
|
version = "1.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
@ -827,7 +820,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -899,7 +892,7 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi",
|
"r-efi",
|
||||||
"wasi 0.14.3+wasi-0.2.4",
|
"wasi 0.14.2+wasi-0.2.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -910,9 +903,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glob"
|
name = "glob"
|
||||||
version = "0.3.3"
|
version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
|
@ -935,9 +928,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.5"
|
version = "0.15.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"allocator-api2",
|
"allocator-api2",
|
||||||
"equivalent",
|
"equivalent",
|
||||||
|
@ -1013,14 +1006,13 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.7.0"
|
version = "1.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e"
|
checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic-waker",
|
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-util",
|
||||||
"h2",
|
"h2",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
|
@ -1028,7 +1020,6 @@ dependencies = [
|
||||||
"httpdate",
|
"httpdate",
|
||||||
"itoa",
|
"itoa",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"pin-utils",
|
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"tokio",
|
"tokio",
|
||||||
"want",
|
"want",
|
||||||
|
@ -1210,9 +1201,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "1.1.0"
|
version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
|
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"idna_adapter",
|
"idna_adapter",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
@ -1231,9 +1222,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.11.0"
|
version = "2.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9"
|
checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
|
@ -1261,14 +1252,14 @@ dependencies = [
|
||||||
"indoc",
|
"indoc",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "io-uring"
|
name = "io-uring"
|
||||||
version = "0.7.10"
|
version = "0.7.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
|
checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
@ -1323,9 +1314,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jobserver"
|
name = "jobserver"
|
||||||
version = "0.1.34"
|
version = "0.1.33"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.3.3",
|
"getrandom 0.3.3",
|
||||||
"libc",
|
"libc",
|
||||||
|
@ -1333,7 +1324,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jocalsend"
|
name = "jocalsend"
|
||||||
version = "1.6.1803398"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"axum-server",
|
"axum-server",
|
||||||
|
@ -1357,7 +1348,6 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha256",
|
"sha256",
|
||||||
"simsearch",
|
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
|
@ -1402,9 +1392,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.175"
|
version = "0.2.174"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libloading"
|
name = "libloading"
|
||||||
|
@ -1468,9 +1458,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.28"
|
version = "0.4.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lru"
|
name = "lru"
|
||||||
|
@ -1580,9 +1570,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "network-interface"
|
name = "network-interface"
|
||||||
version = "2.0.3"
|
version = "2.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "07709a6d4eba90ab10ec170a0530b3aafc81cb8a2d380e4423ae41fc55fe5745"
|
checksum = "862f41f1276e7148fb597fc55ed8666423bebe045199a1298c3515a73ec5cdd9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
|
@ -1660,7 +1650,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1736,7 +1726,7 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"proc-macro2-diagnostics",
|
"proc-macro2-diagnostics",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1751,9 +1741,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.2"
|
version = "2.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
|
@ -1775,9 +1765,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "potential_utf"
|
||||||
version = "0.1.3"
|
version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
|
checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
@ -1799,19 +1789,19 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prettyplease"
|
name = "prettyplease"
|
||||||
version = "0.2.37"
|
version = "0.2.35"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.101"
|
version = "1.0.95"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
|
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
@ -1824,7 +1814,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
"version_check",
|
"version_check",
|
||||||
"yansi",
|
"yansi",
|
||||||
]
|
]
|
||||||
|
@ -1940,9 +1930,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.11.2"
|
version = "1.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
|
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
@ -1952,9 +1942,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.4.10"
|
version = "0.4.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
|
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
@ -1963,15 +1953,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.8.6"
|
version = "0.8.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
|
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.12.23"
|
version = "0.12.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb"
|
checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -2106,9 +2096,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.22"
|
version = "1.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
|
@ -2171,14 +2161,14 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.143"
|
version = "1.0.142"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
|
checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
@ -2286,21 +2276,11 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "simsearch"
|
|
||||||
version = "0.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "629d21c4ebf25655995cda9eb93e85539fa68b0438acb85e9e5d10f6fe2404bc"
|
|
||||||
dependencies = [
|
|
||||||
"strsim",
|
|
||||||
"triple_accel",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.11"
|
version = "0.4.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
|
checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
|
@ -2355,7 +2335,7 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2377,9 +2357,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.106"
|
version = "2.0.104"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
|
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -2403,7 +2383,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2429,42 +2409,42 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.21.0"
|
version = "3.20.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e"
|
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"getrandom 0.3.3",
|
"getrandom 0.3.3",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix 1.0.8",
|
"rustix 1.0.8",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.16"
|
version = "2.0.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
|
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "2.0.16"
|
version = "2.0.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
|
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.43"
|
version = "0.3.41"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031"
|
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
"num-conv",
|
"num-conv",
|
||||||
|
@ -2475,9 +2455,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-core"
|
name = "time-core"
|
||||||
version = "0.1.6"
|
version = "0.1.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
|
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
|
@ -2515,7 +2495,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2698,12 +2678,6 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "triple_accel"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "22048bc95dfb2ffd05b1ff9a756290a009224b60b2f0e7525faeee7603851e63"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "try-lock"
|
name = "try-lock"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
|
@ -2800,14 +2774,13 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.7"
|
version = "2.5.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
|
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"idna",
|
"idna",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"serde",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2851,11 +2824,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.14.3+wasi-0.2.4"
|
version = "0.14.2+wasi-0.2.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95"
|
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"wit-bindgen",
|
"wit-bindgen-rt",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2880,7 +2853,7 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -2915,7 +2888,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
"wasm-bindgen-backend",
|
"wasm-bindgen-backend",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
@ -2994,7 +2967,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -3005,7 +2978,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -3201,18 +3174,21 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "0.7.13"
|
version = "0.7.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
|
checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wit-bindgen"
|
name = "wit-bindgen-rt"
|
||||||
version = "0.45.0"
|
version = "0.39.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814"
|
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "writeable"
|
name = "writeable"
|
||||||
|
@ -3255,7 +3231,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -3276,7 +3252,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -3296,7 +3272,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -3336,5 +3312,5 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
[package]
|
[package]
|
||||||
name = "jocalsend"
|
name = "jocalsend"
|
||||||
# 1.61803398874989484
|
version = "1.0.0"
|
||||||
#----------^
|
|
||||||
version = "1.6.1803398"
|
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.89"
|
|
||||||
authors = ["Joe Ardent <code@ardent.nebcorp.com>"]
|
authors = ["Joe Ardent <code@ardent.nebcorp.com>"]
|
||||||
keywords = ["p2p", "localsend", "tui", "linux"]
|
keywords = ["p2p", "localsend", "tui", "linux"]
|
||||||
description = "A TUI for LocalSend"
|
description = "A terminal implementation of the LocalSend protocol"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license-file = "LICENSE.md"
|
license-file = "LICENSE.md"
|
||||||
repository = "https://git.kittencollective.com/nebkor/joecalsend"
|
repository = "https://git.kittencollective.com/nebkor/joecalsend"
|
||||||
|
@ -35,7 +32,6 @@ rustix = { version = "1", default-features = false, features = ["system"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
sha256 = "1.6"
|
sha256 = "1.6"
|
||||||
simsearch = "0.3"
|
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
tokio = { version = "1", default-features = false, features = ["time", "macros", "rt-multi-thread"] }
|
tokio = { version = "1", default-features = false, features = ["time", "macros", "rt-multi-thread"] }
|
||||||
tokio-rustls = { version = "0.26", default-features = false, features = ["tls12", "logging"] }
|
tokio-rustls = { version = "0.26", default-features = false, features = ["tls12", "logging"] }
|
||||||
|
|
29
README.md
29
README.md
|
@ -10,12 +10,7 @@ that uses [Ratatui](https://github.com/ratatui/ratatui) to provide an interactiv
|
||||||
application, and is compatible with the official app.
|
application, and is compatible with the official app.
|
||||||
|
|
||||||
Install with `cargo install jocalsend` (requires [Rust](https://rustup.rs/)); tested on Linux, it
|
Install with `cargo install jocalsend` (requires [Rust](https://rustup.rs/)); tested on Linux, it
|
||||||
will probably work on Macs but if you're on a Mac, you probably have AirDrop. It's also available in
|
will probably work on Macs but if you're on a Mac, you probably have AirDrop.
|
||||||
nixpkgs, and so if you're a NixOS user, `nix-shell -p jocalsend` will do what you expect.
|
|
||||||
|
|
||||||
## BLOG POSTS!
|
|
||||||
- [Announcement post](https://proclamations.nebcorp-hias.com/sundries/jocalsend/)
|
|
||||||
- [Design and development](https://proclamations.nebcorp-hias.com/rnd/jocalsend-development/)
|
|
||||||
|
|
||||||
## Capabilities and screenshots
|
## Capabilities and screenshots
|
||||||
|
|
||||||
|
@ -27,21 +22,18 @@ available:
|
||||||
- `S` -> go to the sending screen, defaulting to sending files
|
- `S` -> go to the sending screen, defaulting to sending files
|
||||||
- `R` -> go to the receiving screen to approve or deny incoming transfers
|
- `R` -> go to the receiving screen to approve or deny incoming transfers
|
||||||
- `L` -> go to the logging screen where you can adjust the log level
|
- `L` -> go to the logging screen where you can adjust the log level
|
||||||
- `C` -> clear the list of local peers and re-discover them
|
|
||||||
- `H` or `?` -> go to help screen
|
|
||||||
- `ESC` -> go back to the previous screen
|
- `ESC` -> go back to the previous screen
|
||||||
- `Q` -> exit the application
|
- `Q` -> exit the application
|
||||||
|
|
||||||
When in the sending screen, the following are available
|
Additionally, when in the sending screen, the following are available
|
||||||
|
|
||||||
- `TAB` -> switch between content selection and peer selection
|
- `TAB` -> switch between content selection and peer selection
|
||||||
- `T` -> enter text directly to send, `ESC` to cancel
|
- `P` -> switch to peer selection
|
||||||
- `/` -> fuzzy filename search, use `ESC` to stop inputting text
|
- `T` -> switch to entering text to send
|
||||||
|
- `F` -> switch to selecting files to send (not available when entering text, use `ESC` to exit text entry)
|
||||||
|
|
||||||
When in the receiving screen, use `A` to approve the incoming transfer request, or `D` to deny it.
|
In addition to the interactive commands, it will also accept commandline arguments to pre-select a
|
||||||
|
file or pre-populate text to send:
|
||||||
Finally, it will also accept commandline arguments to pre-select a file or pre-populate text to
|
|
||||||
send:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
$ jocalsend -h
|
$ jocalsend -h
|
||||||
|
@ -94,10 +86,3 @@ screen and in the receiving screen:
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Thanks
|
|
||||||
|
|
||||||
- to the LocalSend developers for the app and protocol
|
|
||||||
- to [wyli](https://github.com/wylited) for the initial [protocol backend](https://github.com/wylited/localsend) implementation
|
|
||||||
|
|
||||||
[](https://ratatui.rs/)
|
|
||||||
|
|
1
VERSION
1
VERSION
|
@ -1 +0,0 @@
|
||||||
1.61803398
|
|
|
@ -1,31 +0,0 @@
|
||||||
# Golden Versioning
|
|
||||||
|
|
||||||
This software is versioned under a scheme I call "goldver", as an homage to the
|
|
||||||
vastly inferior [semver](https://semver.org).
|
|
||||||
|
|
||||||
## What does "goldver" mean?
|
|
||||||
|
|
||||||
When projects are versioned with goldver, the first version is "1". Note that it
|
|
||||||
is not "1.0", or, "1.0-prealpha-release-preview", or anything nonsensical like
|
|
||||||
that. As new versions are released, decimals from *phi*, the [Golden
|
|
||||||
Ratio](https://en.wikipedia.org/wiki/Golden_ratio), are appended after an
|
|
||||||
initial decimal point. So the second released version will be "1.6", the third
|
|
||||||
would be "1.61", etc., and on until perfection is asymptotically approached as
|
|
||||||
the number of released versions goes to infinity.
|
|
||||||
|
|
||||||
## Wait, didn't Donald Knuth do this?
|
|
||||||
|
|
||||||
No! He uses [pi for TeX and e for MetaFont](https://texfaq.org/FAQ-TeXfuture),
|
|
||||||
obviously COMPLETELY different.
|
|
||||||
|
|
||||||
## Ok.
|
|
||||||
|
|
||||||
Cool.
|
|
||||||
|
|
||||||
## What version is JocalSend now?
|
|
||||||
|
|
||||||
Canonically, see the `VERSION` file. Heretically, once there have been
|
|
||||||
at enough releases, the version string in the `Cargo.toml` file will
|
|
||||||
always be of the form "1.6.x", where *x* is at least one digit long, starting
|
|
||||||
with "1". Each subsequent release will append the next digit of *phi* to
|
|
||||||
*x*.
|
|
|
@ -1,72 +0,0 @@
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use crossterm::event::Event;
|
|
||||||
use jocalsend::error::Result;
|
|
||||||
use ratatui::widgets::WidgetRef;
|
|
||||||
use ratatui_explorer::FileExplorer;
|
|
||||||
use simsearch::{SearchOptions, SimSearch};
|
|
||||||
use tui_input::Input;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub(crate) struct FileFinder {
|
|
||||||
pub explorer: FileExplorer,
|
|
||||||
pub fuzzy: SimSearch<usize>,
|
|
||||||
pub working_dir: Option<PathBuf>,
|
|
||||||
pub input: Input,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FileFinder {
|
|
||||||
pub fn new() -> Result<Self> {
|
|
||||||
let fuzzy = SimSearch::new_with(
|
|
||||||
SearchOptions::new()
|
|
||||||
.stop_words(vec![std::path::MAIN_SEPARATOR_STR.to_string()])
|
|
||||||
.stop_whitespace(false)
|
|
||||||
.threshold(0.0),
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
explorer: FileExplorer::new()?,
|
|
||||||
fuzzy,
|
|
||||||
working_dir: None,
|
|
||||||
input: Default::default(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle(&mut self, event: &Event) -> Result<()> {
|
|
||||||
self.index();
|
|
||||||
Ok(self.explorer.handle(event)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cwd(&self) -> &Path {
|
|
||||||
self.explorer.cwd()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_cwd(&mut self, cwd: &Path) -> Result<()> {
|
|
||||||
self.explorer.set_cwd(cwd)?;
|
|
||||||
self.index();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn widget(&self) -> impl WidgetRef {
|
|
||||||
self.explorer.widget()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset_fuzzy(&mut self) {
|
|
||||||
self.fuzzy.clear();
|
|
||||||
self.input.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn index(&mut self) {
|
|
||||||
if let Some(owd) = self.working_dir.as_ref()
|
|
||||||
&& owd == self.cwd()
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.working_dir = Some(self.cwd().to_path_buf());
|
|
||||||
self.reset_fuzzy();
|
|
||||||
|
|
||||||
for (i, f) in self.explorer.files().iter().enumerate() {
|
|
||||||
self.fuzzy.insert(i, f.name());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,341 +0,0 @@
|
||||||
use crossterm::event::{Event, KeyCode, KeyEvent};
|
|
||||||
use jocalsend::ReceiveDialog;
|
|
||||||
use log::{debug, error, warn};
|
|
||||||
use tui_input::backend::crossterm::EventHandler;
|
|
||||||
|
|
||||||
use crate::app::{App, CurrentScreen, FileMode, SendingScreen};
|
|
||||||
|
|
||||||
impl App {
|
|
||||||
pub(super) async fn handle_key_event(
|
|
||||||
&mut self,
|
|
||||||
key_event: KeyEvent,
|
|
||||||
event: crossterm::event::Event,
|
|
||||||
) {
|
|
||||||
let code = key_event.code;
|
|
||||||
let mode = self.screen();
|
|
||||||
match mode {
|
|
||||||
CurrentScreen::Main
|
|
||||||
| CurrentScreen::Help
|
|
||||||
| CurrentScreen::Logging
|
|
||||||
| CurrentScreen::Receiving => match code {
|
|
||||||
KeyCode::Char('q') => self.exit().await,
|
|
||||||
KeyCode::Char('s') => self.send(),
|
|
||||||
KeyCode::Char('r') => self.recv(),
|
|
||||||
KeyCode::Char('l') => self.logs(),
|
|
||||||
KeyCode::Char('m') => self.main(),
|
|
||||||
KeyCode::Char('h') | KeyCode::Char('?') => self.help(),
|
|
||||||
KeyCode::Char('c') => self.service.clear_peers().await,
|
|
||||||
KeyCode::Esc => self.pop(),
|
|
||||||
_ => match mode {
|
|
||||||
CurrentScreen::Main | CurrentScreen::Help => {}
|
|
||||||
CurrentScreen::Logging => match code {
|
|
||||||
KeyCode::Left => change_log_level(LogDelta::Down),
|
|
||||||
KeyCode::Right => change_log_level(LogDelta::Up),
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
CurrentScreen::Receiving => match code {
|
|
||||||
KeyCode::Up => self.receiving_state.select_previous(),
|
|
||||||
KeyCode::Down => self.receiving_state.select_next(),
|
|
||||||
KeyCode::Char('a') => self.accept(),
|
|
||||||
KeyCode::Char('d') => self.deny(),
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
CurrentScreen::Stopping | CurrentScreen::Sending(_) => unreachable!(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
CurrentScreen::Sending(_) => self.sending_screen(key_event, event).await,
|
|
||||||
CurrentScreen::Stopping => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn sending_screen(&mut self, key_event: KeyEvent, event: Event) {
|
|
||||||
let mode = self.screen();
|
|
||||||
let CurrentScreen::Sending(mode) = mode else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let code = key_event.code;
|
|
||||||
match mode {
|
|
||||||
SendingScreen::Peers => match code {
|
|
||||||
KeyCode::Char('q') => self.exit().await,
|
|
||||||
KeyCode::Char('s') => self.send(),
|
|
||||||
KeyCode::Char('r') => self.recv(),
|
|
||||||
KeyCode::Char('l') => self.logs(),
|
|
||||||
KeyCode::Char('m') => self.main(),
|
|
||||||
KeyCode::Char('h') | KeyCode::Char('?') => self.help(),
|
|
||||||
KeyCode::Char('c') => self.service.clear_peers().await,
|
|
||||||
KeyCode::Char('t') => self.sending_text(),
|
|
||||||
KeyCode::Tab => self.sending_files(),
|
|
||||||
KeyCode::Enter => self.send_content().await,
|
|
||||||
KeyCode::Esc => self.pop(),
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
SendingScreen::Files(FileMode::Picking) => match code {
|
|
||||||
KeyCode::Char('q') => self.exit().await,
|
|
||||||
KeyCode::Char('s') => self.send(),
|
|
||||||
KeyCode::Char('r') => self.recv(),
|
|
||||||
KeyCode::Char('l') => self.logs(),
|
|
||||||
KeyCode::Char('m') => self.main(),
|
|
||||||
KeyCode::Char('h') | KeyCode::Char('?') => self.help(),
|
|
||||||
KeyCode::Char('c') => self.service.clear_peers().await,
|
|
||||||
KeyCode::Char('t') => self.sending_text(),
|
|
||||||
KeyCode::Tab => self.sending_peers(),
|
|
||||||
KeyCode::Enter => self.chdir_or_send_file().await,
|
|
||||||
KeyCode::Esc => self.pop(),
|
|
||||||
KeyCode::Char('/') => self.sending_fuzzy(),
|
|
||||||
_ => self.file_finder.handle(&event).unwrap_or_default(),
|
|
||||||
},
|
|
||||||
SendingScreen::Files(FileMode::Fuzzy) => match code {
|
|
||||||
KeyCode::Tab => self.sending_peers(),
|
|
||||||
KeyCode::Enter => self.chdir_or_send_file().await,
|
|
||||||
KeyCode::Esc => {
|
|
||||||
self.file_finder.reset_fuzzy();
|
|
||||||
self.sending_files();
|
|
||||||
}
|
|
||||||
KeyCode::Up | KeyCode::Down => {
|
|
||||||
if let Err(e) = self.file_finder.handle(&event) {
|
|
||||||
log::error!("error selecting file: {e:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
self.file_finder.index();
|
|
||||||
if let Some(changed) = self.file_finder.input.handle_event(&event)
|
|
||||||
&& changed.value
|
|
||||||
{
|
|
||||||
let id = self
|
|
||||||
.file_finder
|
|
||||||
.fuzzy
|
|
||||||
.search(self.file_finder.input.value())
|
|
||||||
.first()
|
|
||||||
.copied()
|
|
||||||
.unwrap_or(0);
|
|
||||||
self.file_finder.explorer.set_selected_idx(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
SendingScreen::Text => match code {
|
|
||||||
KeyCode::Tab => self.sending_peers(),
|
|
||||||
KeyCode::Enter => self.send_text().await,
|
|
||||||
KeyCode::Esc => {
|
|
||||||
self.text = None;
|
|
||||||
self.input.reset();
|
|
||||||
self.sending_files();
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
if let Some(changed) = self.input.handle_event(&event)
|
|
||||||
&& changed.value
|
|
||||||
{
|
|
||||||
if self.input.value().is_empty() {
|
|
||||||
self.text = None;
|
|
||||||
} else {
|
|
||||||
self.text = Some(self.input.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn exit(&mut self) {
|
|
||||||
self.screen.push(CurrentScreen::Stopping);
|
|
||||||
self.service.stop().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send(&mut self) {
|
|
||||||
let last = self.screen.last();
|
|
||||||
match last {
|
|
||||||
Some(CurrentScreen::Sending(_)) => {}
|
|
||||||
_ => self
|
|
||||||
.screen
|
|
||||||
.push(CurrentScreen::Sending(SendingScreen::Files(
|
|
||||||
FileMode::Picking,
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn recv(&mut self) {
|
|
||||||
let last = self.screen.last();
|
|
||||||
match last {
|
|
||||||
Some(CurrentScreen::Receiving) => {}
|
|
||||||
_ => self.screen.push(CurrentScreen::Receiving),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn logs(&mut self) {
|
|
||||||
let last = self.screen.last();
|
|
||||||
match last {
|
|
||||||
Some(CurrentScreen::Logging) => {}
|
|
||||||
_ => self.screen.push(CurrentScreen::Logging),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pop(&mut self) {
|
|
||||||
self.screen.pop();
|
|
||||||
if self.screen.last().is_none() {
|
|
||||||
self.screen.push(CurrentScreen::Main);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn main(&mut self) {
|
|
||||||
let last = self.screen.last();
|
|
||||||
match last {
|
|
||||||
Some(CurrentScreen::Main) => {}
|
|
||||||
_ => self.screen.push(CurrentScreen::Main),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn help(&mut self) {
|
|
||||||
let last = self.screen.last();
|
|
||||||
match last {
|
|
||||||
Some(CurrentScreen::Help) => {}
|
|
||||||
_ => self.screen.push(CurrentScreen::Help),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sending_peers(&mut self) {
|
|
||||||
if let CurrentScreen::Sending(mode) = self.screen_mut() {
|
|
||||||
*mode = SendingScreen::Peers;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sending_text(&mut self) {
|
|
||||||
if let CurrentScreen::Sending(mode) = self.screen_mut() {
|
|
||||||
*mode = SendingScreen::Text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sending_fuzzy(&mut self) {
|
|
||||||
if let CurrentScreen::Sending(mode) = self.screen_mut() {
|
|
||||||
*mode = SendingScreen::Files(FileMode::Fuzzy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sending_files(&mut self) {
|
|
||||||
let doing_files = self.text.is_none();
|
|
||||||
let doing_picking = self.file_finder.input.value().is_empty();
|
|
||||||
let screen = self.screen_mut();
|
|
||||||
if let CurrentScreen::Sending(mode) = screen {
|
|
||||||
if doing_files {
|
|
||||||
if doing_picking {
|
|
||||||
*mode = SendingScreen::Files(FileMode::Picking);
|
|
||||||
} else {
|
|
||||||
*mode = SendingScreen::Files(FileMode::Fuzzy);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
*mode = SendingScreen::Text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// accept a content receive request
|
|
||||||
fn accept(&mut self) {
|
|
||||||
let Some(idx) = self.receiving_state.selected() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
// keys are sorted, so we can use the table selection index
|
|
||||||
let keys: Vec<_> = self.receive_requests.keys().collect();
|
|
||||||
let Some(key) = keys.get(idx) else {
|
|
||||||
warn!("could not get id from selection index {idx}");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(req) = self.receive_requests.get(key) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
if let Err(e) = req.tx.send(ReceiveDialog::Approve) {
|
|
||||||
error!("got error sending upload confirmation: {e:?}");
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// reject an content receive request
|
|
||||||
fn deny(&mut self) {
|
|
||||||
let Some(idx) = self.receiving_state.selected() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
// keys are sorted, so we can use the table selection index
|
|
||||||
let keys: Vec<_> = self.receive_requests.keys().cloned().collect();
|
|
||||||
let Some(key) = keys.get(idx) else {
|
|
||||||
warn!("could not get id from selection index {idx}");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(req) = self.receive_requests.get(key).cloned() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
if let Err(e) = req.tx.send(ReceiveDialog::Deny) {
|
|
||||||
error!("got error sending upload confirmation: {e:?}");
|
|
||||||
};
|
|
||||||
self.receive_requests.remove(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn chdir_or_send_file(&mut self) {
|
|
||||||
let file = self.file_finder.explorer.current().path().clone();
|
|
||||||
if file.is_dir()
|
|
||||||
&& let Err(e) = self.file_finder.set_cwd(&file)
|
|
||||||
{
|
|
||||||
error!("could not list directory {file:?}: {e}");
|
|
||||||
return;
|
|
||||||
} else if file.is_dir() {
|
|
||||||
self.file_finder.input.reset();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(peer_idx) = self.peer_state.selected() else {
|
|
||||||
warn!("no peer selected to send to");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(peer) = self.peers.get(peer_idx) else {
|
|
||||||
warn!("invalid peer index {peer_idx}");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if file.is_file() {
|
|
||||||
debug!("sending {file:?}");
|
|
||||||
if let Err(e) = self.service.send_file(&peer.fingerprint, file).await {
|
|
||||||
error!("got error sending content: {e:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// send content to selected peer, or change directories in the file explorer
|
|
||||||
async fn send_text(&mut self) {
|
|
||||||
debug!("sending text");
|
|
||||||
|
|
||||||
let Some(peer_idx) = self.peer_state.selected() else {
|
|
||||||
debug!("no peer selected to send to");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(peer) = self.peers.get(peer_idx) else {
|
|
||||||
warn!("invalid peer index {peer_idx}");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(text) = &self.text else {
|
|
||||||
debug!("no text to send");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = self.service.send_text(&peer.fingerprint, text).await {
|
|
||||||
error!("got error sending \"{text}\" to {}: {e:?}", peer.alias);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_content(&mut self) {
|
|
||||||
if self.text.is_some() {
|
|
||||||
self.send_text().await;
|
|
||||||
} else {
|
|
||||||
self.chdir_or_send_file().await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
|
||||||
enum LogDelta {
|
|
||||||
Up,
|
|
||||||
Down,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn change_log_level(delta: LogDelta) {
|
|
||||||
let level = match delta {
|
|
||||||
LogDelta::Up => log::max_level().increment_severity(),
|
|
||||||
LogDelta::Down => log::max_level().decrement_severity(),
|
|
||||||
};
|
|
||||||
log::set_max_level(level);
|
|
||||||
}
|
|
308
src/app/mod.rs
308
src/app/mod.rs
|
@ -1,24 +1,20 @@
|
||||||
use std::{collections::BTreeMap, net::SocketAddr};
|
use std::{collections::BTreeMap, net::SocketAddr, time::Duration};
|
||||||
|
|
||||||
use crossterm::event::{Event, EventStream, KeyEventKind};
|
use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, KeyEventKind};
|
||||||
use futures::{FutureExt, StreamExt};
|
use futures::{FutureExt, StreamExt};
|
||||||
use jocalsend::{JocalEvent, JocalService, ReceiveRequest, error::Result};
|
use jocalsend::{JocalService, ReceiveDialog, ReceiveRequest, TransferEvent, error::Result};
|
||||||
use julid::Julid;
|
use julid::Julid;
|
||||||
|
use log::{LevelFilter, debug, error, warn};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
Frame,
|
Frame,
|
||||||
widgets::{ListState, TableState},
|
widgets::{ListState, TableState},
|
||||||
};
|
};
|
||||||
use ratatui_explorer::FileExplorer;
|
use ratatui_explorer::FileExplorer;
|
||||||
use tokio::sync::mpsc::UnboundedReceiver;
|
use tokio::sync::mpsc::UnboundedReceiver;
|
||||||
use tui_input::Input;
|
use tui_input::{Input, backend::crossterm::EventHandler};
|
||||||
|
|
||||||
pub mod widgets;
|
pub mod widgets;
|
||||||
|
|
||||||
mod file_finder;
|
|
||||||
use file_finder::FileFinder;
|
|
||||||
|
|
||||||
mod handle;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Peer {
|
pub struct Peer {
|
||||||
pub alias: String,
|
pub alias: String,
|
||||||
|
@ -28,7 +24,7 @@ pub struct Peer {
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
pub service: JocalService,
|
pub service: JocalService,
|
||||||
pub terminal_events: EventStream,
|
pub events: EventStream,
|
||||||
pub peers: Vec<Peer>,
|
pub peers: Vec<Peer>,
|
||||||
pub peer_state: ListState,
|
pub peer_state: ListState,
|
||||||
pub receive_requests: BTreeMap<Julid, ReceiveRequest>,
|
pub receive_requests: BTreeMap<Julid, ReceiveRequest>,
|
||||||
|
@ -36,8 +32,8 @@ pub struct App {
|
||||||
receiving_state: TableState,
|
receiving_state: TableState,
|
||||||
// for getting messages back from the web server or web client about things we've done; the
|
// for getting messages back from the web server or web client about things we've done; the
|
||||||
// other end is held by the service
|
// other end is held by the service
|
||||||
jocal_event_rx: UnboundedReceiver<JocalEvent>,
|
event_listener: UnboundedReceiver<TransferEvent>,
|
||||||
file_finder: FileFinder,
|
file_picker: FileExplorer,
|
||||||
text: Option<String>,
|
text: Option<String>,
|
||||||
input: Input,
|
input: Input,
|
||||||
}
|
}
|
||||||
|
@ -49,31 +45,24 @@ pub enum CurrentScreen {
|
||||||
Receiving,
|
Receiving,
|
||||||
Stopping,
|
Stopping,
|
||||||
Logging,
|
Logging,
|
||||||
Help,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum SendingScreen {
|
pub enum SendingScreen {
|
||||||
Files(FileMode),
|
Files,
|
||||||
Peers,
|
Peers,
|
||||||
Text,
|
Text,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum FileMode {
|
|
||||||
Picking,
|
|
||||||
Fuzzy,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn new(service: JocalService, event_listener: UnboundedReceiver<JocalEvent>) -> Self {
|
pub fn new(service: JocalService, event_listener: UnboundedReceiver<TransferEvent>) -> Self {
|
||||||
App {
|
App {
|
||||||
service,
|
service,
|
||||||
jocal_event_rx: event_listener,
|
event_listener,
|
||||||
screen: vec![CurrentScreen::Main],
|
screen: vec![CurrentScreen::Main],
|
||||||
file_finder: FileFinder::new().expect("could not create file explorer"),
|
file_picker: FileExplorer::new().expect("could not create file explorer"),
|
||||||
text: None,
|
text: None,
|
||||||
terminal_events: Default::default(),
|
events: Default::default(),
|
||||||
peers: Default::default(),
|
peers: Default::default(),
|
||||||
peer_state: Default::default(),
|
peer_state: Default::default(),
|
||||||
receive_requests: Default::default(),
|
receive_requests: Default::default(),
|
||||||
|
@ -84,37 +73,32 @@ impl App {
|
||||||
|
|
||||||
pub async fn handle_events(&mut self) -> Result<()> {
|
pub async fn handle_events(&mut self) -> Result<()> {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
jocal_event = self.jocal_event_rx.recv().fuse() => {
|
event = self.events.next().fuse() => {
|
||||||
if let Some(event) = jocal_event {
|
if let Some(Ok(evt)) = event {
|
||||||
log::trace!("got JocalEvent {event:?}");
|
|
||||||
match event {
|
|
||||||
JocalEvent::ReceiveRequest { id, request } => {
|
|
||||||
self.receive_requests.insert(id, request);
|
|
||||||
}
|
|
||||||
JocalEvent::Cancelled { session_id: id } | JocalEvent::ReceivedInbound(id) => {
|
|
||||||
self.receive_requests.remove(&id);
|
|
||||||
}
|
|
||||||
JocalEvent::SendApproved(id) => log::info!("remote recipient approved outbound transfer {id}"),
|
|
||||||
JocalEvent::SendDenied => log::warn!("outbound transfer request has been denied"),
|
|
||||||
JocalEvent::SendSuccess { content, session: _session } => log::info!("successfully sent {content}"),
|
|
||||||
JocalEvent::SendFailed { error } => log::error!("could not send content: {error}"),
|
|
||||||
JocalEvent::Tick => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
terminal_event = self.terminal_events.next().fuse() => {
|
|
||||||
if let Some(Ok(evt)) = terminal_event {
|
|
||||||
match evt {
|
match evt {
|
||||||
Event::Key(key)
|
Event::Key(key)
|
||||||
if key.kind == KeyEventKind::Press
|
if key.kind == KeyEventKind::Press
|
||||||
=> self.handle_key_event(key, evt).await,
|
=> self.handle_key_event(key, evt).await,
|
||||||
Event::Mouse(_) => {}
|
Event::Mouse(_) => {}
|
||||||
Event::Resize(_, _) => {}
|
Event::Resize(_, _) => {}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
transfer_event = self.event_listener.recv().fuse() => {
|
||||||
|
if let Some(event) = transfer_event {
|
||||||
|
debug!("got transferr event {event:?}");
|
||||||
|
match event {
|
||||||
|
TransferEvent::ReceiveRequest { id, request } => {
|
||||||
|
self.receive_requests.insert(id, request);
|
||||||
|
}
|
||||||
|
TransferEvent::Cancelled(id) | TransferEvent::Received(id) => {
|
||||||
|
self.receive_requests.remove(&id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = tokio::time::sleep(Duration::from_millis(200)) => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -125,7 +109,7 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn files(&mut self) -> &mut FileExplorer {
|
pub fn files(&mut self) -> &mut FileExplorer {
|
||||||
&mut self.file_finder.explorer
|
&mut self.file_picker
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text(&mut self) -> &mut Option<String> {
|
pub fn text(&mut self) -> &mut Option<String> {
|
||||||
|
@ -139,7 +123,237 @@ impl App {
|
||||||
self.screen.last_mut().unwrap()
|
self.screen.last_mut().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_key_event(&mut self, key_event: KeyEvent, event: crossterm::event::Event) {
|
||||||
|
let code = key_event.code;
|
||||||
|
let mode = self.screen.last_mut().unwrap();
|
||||||
|
match mode {
|
||||||
|
CurrentScreen::Main
|
||||||
|
| CurrentScreen::Logging
|
||||||
|
| CurrentScreen::Receiving
|
||||||
|
| CurrentScreen::Sending(SendingScreen::Files)
|
||||||
|
| CurrentScreen::Sending(SendingScreen::Peers) => match code {
|
||||||
|
KeyCode::Esc => self.pop(),
|
||||||
|
KeyCode::Char('q') => self.exit().await,
|
||||||
|
KeyCode::Char('s') => self.send(),
|
||||||
|
KeyCode::Char('r') => self.recv(),
|
||||||
|
KeyCode::Char('l') => self.logs(),
|
||||||
|
KeyCode::Char('m') => self.main(),
|
||||||
|
_ => match mode {
|
||||||
|
CurrentScreen::Logging => match code {
|
||||||
|
KeyCode::Left => change_log_level(-1),
|
||||||
|
KeyCode::Right => change_log_level(1),
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
CurrentScreen::Receiving => match code {
|
||||||
|
KeyCode::Up => self.receiving_state.select_previous(),
|
||||||
|
KeyCode::Down => self.receiving_state.select_next(),
|
||||||
|
KeyCode::Char('a') => self.accept(),
|
||||||
|
KeyCode::Char('d') => self.deny(),
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
CurrentScreen::Sending(sending_screen) => match sending_screen {
|
||||||
|
SendingScreen::Files => match code {
|
||||||
|
KeyCode::Char('t') => *sending_screen = SendingScreen::Text,
|
||||||
|
KeyCode::Tab => *sending_screen = SendingScreen::Peers,
|
||||||
|
KeyCode::Enter => self.chdir_or_send_file().await,
|
||||||
|
_ => self.file_picker.handle(&event).unwrap_or_default(),
|
||||||
|
},
|
||||||
|
SendingScreen::Peers => match code {
|
||||||
|
KeyCode::Tab => *sending_screen = SendingScreen::Files,
|
||||||
|
KeyCode::Char('t') => *sending_screen = SendingScreen::Text,
|
||||||
|
KeyCode::Enter => self.send_content().await,
|
||||||
|
KeyCode::Up => self.peer_state.select_previous(),
|
||||||
|
KeyCode::Down => self.peer_state.select_next(),
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
SendingScreen::Text => unreachable!(),
|
||||||
|
},
|
||||||
|
CurrentScreen::Main => {}
|
||||||
|
CurrentScreen::Stopping => unreachable!(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// we only need to deal with sending text now
|
||||||
|
CurrentScreen::Sending(sending_screen) => match sending_screen {
|
||||||
|
SendingScreen::Text => match code {
|
||||||
|
KeyCode::Tab => *sending_screen = SendingScreen::Peers,
|
||||||
|
KeyCode::Enter => self.send_text().await,
|
||||||
|
KeyCode::Esc => {
|
||||||
|
self.text = None;
|
||||||
|
self.input.reset();
|
||||||
|
*sending_screen = SendingScreen::Files;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if let Some(changed) = self.input.handle_event(&event)
|
||||||
|
&& changed.value
|
||||||
|
{
|
||||||
|
if self.input.value().is_empty() {
|
||||||
|
self.text = None;
|
||||||
|
} else {
|
||||||
|
self.text = Some(self.input.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// we've already handled the other sending modes
|
||||||
|
SendingScreen::Files | SendingScreen::Peers => unreachable!(),
|
||||||
|
},
|
||||||
|
CurrentScreen::Stopping => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn draw(&mut self, frame: &mut Frame) {
|
pub fn draw(&mut self, frame: &mut Frame) {
|
||||||
frame.render_widget(self, frame.area());
|
frame.render_widget(self, frame.area());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn exit(&mut self) {
|
||||||
|
self.screen.push(CurrentScreen::Stopping);
|
||||||
|
self.service.stop().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send(&mut self) {
|
||||||
|
let last = self.screen.last();
|
||||||
|
match last {
|
||||||
|
Some(CurrentScreen::Sending(_)) => {}
|
||||||
|
_ => self
|
||||||
|
.screen
|
||||||
|
.push(CurrentScreen::Sending(SendingScreen::Files)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recv(&mut self) {
|
||||||
|
let last = self.screen.last();
|
||||||
|
match last {
|
||||||
|
Some(CurrentScreen::Receiving) => {}
|
||||||
|
_ => self.screen.push(CurrentScreen::Receiving),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn logs(&mut self) {
|
||||||
|
let last = self.screen.last();
|
||||||
|
match last {
|
||||||
|
Some(CurrentScreen::Logging) => {}
|
||||||
|
_ => self.screen.push(CurrentScreen::Logging),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pop(&mut self) {
|
||||||
|
self.screen.pop();
|
||||||
|
if self.screen.last().is_none() {
|
||||||
|
self.screen.push(CurrentScreen::Main);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main(&mut self) {
|
||||||
|
let last = self.screen.last();
|
||||||
|
match last {
|
||||||
|
Some(CurrentScreen::Main) => {}
|
||||||
|
_ => self.screen.push(CurrentScreen::Main),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// accept a content receive request
|
||||||
|
fn accept(&mut self) {
|
||||||
|
let Some(idx) = self.receiving_state.selected() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// keys are sorted, so we can use the table selection index
|
||||||
|
let keys: Vec<_> = self.receive_requests.keys().collect();
|
||||||
|
let Some(key) = keys.get(idx) else {
|
||||||
|
warn!("could not get id from selection index {idx}");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(req) = self.receive_requests.get(key) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Err(e) = req.tx.send(ReceiveDialog::Approve) {
|
||||||
|
error!("got error sending upload confirmation: {e:?}");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// reject an content receive request
|
||||||
|
fn deny(&mut self) {
|
||||||
|
let Some(idx) = self.receiving_state.selected() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// keys are sorted, so we can use the table selection index
|
||||||
|
let keys: Vec<_> = self.receive_requests.keys().cloned().collect();
|
||||||
|
let Some(key) = keys.get(idx) else {
|
||||||
|
warn!("could not get id from selection index {idx}");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(req) = self.receive_requests.get(key).cloned() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Err(e) = req.tx.send(ReceiveDialog::Deny) {
|
||||||
|
error!("got error sending upload confirmation: {e:?}");
|
||||||
|
};
|
||||||
|
self.receive_requests.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn chdir_or_send_file(&mut self) {
|
||||||
|
let file = self.file_picker.current().path().clone();
|
||||||
|
if file.is_dir()
|
||||||
|
&& let Err(e) = self.file_picker.set_cwd(&file)
|
||||||
|
{
|
||||||
|
error!("could not list directory {file:?}: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(peer_idx) = self.peer_state.selected() else {
|
||||||
|
warn!("no peer selected to send to");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(peer) = self.peers.get(peer_idx) else {
|
||||||
|
warn!("invalid peer index {peer_idx}");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if file.is_file() {
|
||||||
|
debug!("sending {file:?}");
|
||||||
|
if let Err(e) = self.service.send_file(&peer.fingerprint, file).await {
|
||||||
|
error!("got error sending content: {e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// send content to selected peer, or change directories in the file explorer
|
||||||
|
async fn send_text(&mut self) {
|
||||||
|
debug!("sending text");
|
||||||
|
|
||||||
|
let Some(peer_idx) = self.peer_state.selected() else {
|
||||||
|
debug!("no peer selected to send to");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(peer) = self.peers.get(peer_idx) else {
|
||||||
|
warn!("invalid peer index {peer_idx}");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(text) = &self.text else {
|
||||||
|
debug!("no text to send");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = self.service.send_text(&peer.fingerprint, text).await {
|
||||||
|
error!("got error sending \"{text}\" to {}: {e:?}", peer.alias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_content(&mut self) {
|
||||||
|
if self.text.is_some() {
|
||||||
|
self.send_text().await;
|
||||||
|
} else {
|
||||||
|
self.chdir_or_send_file().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn change_log_level(delta: isize) {
|
||||||
|
let level = log::max_level() as isize;
|
||||||
|
let max = log::LevelFilter::max() as isize;
|
||||||
|
let level = (level + delta).clamp(0, max) as usize;
|
||||||
|
// levelfilter is repr(usize) so this is safe
|
||||||
|
let level = unsafe { std::mem::transmute::<usize, LevelFilter>(level) };
|
||||||
|
|
||||||
|
log::set_max_level(level);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,12 +10,12 @@ use ratatui::{
|
||||||
text::{Line, Text, ToLine},
|
text::{Line, Text, ToLine},
|
||||||
widgets::{
|
widgets::{
|
||||||
Block, Borders, Clear, List, ListItem, ListState, Padding, Paragraph, Row, Table,
|
Block, Borders, Clear, List, ListItem, ListState, Padding, Paragraph, Row, Table,
|
||||||
TableState, Widget, Wrap,
|
TableState, Widget,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use tui_logger::{TuiLoggerLevelOutput, TuiLoggerWidget, TuiWidgetState};
|
use tui_logger::{TuiLoggerLevelOutput, TuiLoggerWidget, TuiWidgetState};
|
||||||
|
|
||||||
use super::{App, CurrentScreen, FileMode, Peer, SendingScreen};
|
use super::{App, CurrentScreen, Peer, SendingScreen};
|
||||||
|
|
||||||
static MAIN_MENU: LazyLock<Line> = LazyLock::new(|| {
|
static MAIN_MENU: LazyLock<Line> = LazyLock::new(|| {
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
|
@ -27,8 +27,6 @@ static MAIN_MENU: LazyLock<Line> = LazyLock::new(|| {
|
||||||
"<L>".blue().bold(),
|
"<L>".blue().bold(),
|
||||||
" Previous Screen ".into(),
|
" Previous Screen ".into(),
|
||||||
"<ESC>".blue().bold(),
|
"<ESC>".blue().bold(),
|
||||||
" Help ".into(),
|
|
||||||
"<H|?>".blue().bold(),
|
|
||||||
" Quit ".into(),
|
" Quit ".into(),
|
||||||
"<Q>".blue().bold(),
|
"<Q>".blue().bold(),
|
||||||
])
|
])
|
||||||
|
@ -66,14 +64,16 @@ static CONTENT_RECEIVE_MENU: LazyLock<Line> = LazyLock::new(|| {
|
||||||
|
|
||||||
static CONTENT_SEND_FILE_MENU: LazyLock<Line> = LazyLock::new(|| {
|
static CONTENT_SEND_FILE_MENU: LazyLock<Line> = LazyLock::new(|| {
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
" Fuzzy Search ".into(),
|
|
||||||
"</>".blue().bold(),
|
|
||||||
" Select Previous ".into(),
|
" Select Previous ".into(),
|
||||||
"<UP>".blue().bold(),
|
"<UP>".blue().bold(),
|
||||||
" Select Next ".into(),
|
" Select Next ".into(),
|
||||||
"<DOWN>".blue().bold(),
|
"<DOWN>".blue().bold(),
|
||||||
" Send File ".into(),
|
" Send File ".into(),
|
||||||
"<ENTER>".blue().bold(),
|
"<ENTER>".blue().bold(),
|
||||||
|
" Parent Dir ".into(),
|
||||||
|
"<LEFT>".blue().bold(),
|
||||||
|
" Child Dir ".into(),
|
||||||
|
"<RIGHT>".blue().bold(),
|
||||||
" Enter Text ".into(),
|
" Enter Text ".into(),
|
||||||
"<T>".blue().bold(),
|
"<T>".blue().bold(),
|
||||||
" Peers ".into(),
|
" Peers ".into(),
|
||||||
|
@ -130,14 +130,13 @@ impl Widget for &mut App {
|
||||||
let [header_left, header_right] = header_layout.areas(top);
|
let [header_left, header_right] = header_layout.areas(top);
|
||||||
let header_margin = Margin::new(1, 2);
|
let header_margin = Margin::new(1, 2);
|
||||||
|
|
||||||
let top_heavy = Layout::vertical([Constraint::Percentage(66), Constraint::Percentage(34)]);
|
|
||||||
let [heavy_top, _skinny_bottom] = top_heavy.areas(area);
|
|
||||||
|
|
||||||
let subscreen_margin = Margin::new(1, 2);
|
let subscreen_margin = Margin::new(1, 2);
|
||||||
|
|
||||||
|
// it's safe to call `unwrap()` here because we ensure there's always at least
|
||||||
|
// one element in `self.screen`; see the `self.pop()` method
|
||||||
let current_screen = self.screen();
|
let current_screen = self.screen();
|
||||||
match current_screen {
|
match current_screen {
|
||||||
CurrentScreen::Main => {
|
CurrentScreen::Main | CurrentScreen::Stopping => {
|
||||||
let rx_reqs: Vec<_> = self.receive_requests.values().collect();
|
let rx_reqs: Vec<_> = self.receive_requests.values().collect();
|
||||||
outer_frame(¤t_screen, &MAIN_MENU, area, buf);
|
outer_frame(¤t_screen, &MAIN_MENU, area, buf);
|
||||||
logger(header_right.inner(header_margin), buf);
|
logger(header_right.inner(header_margin), buf);
|
||||||
|
@ -159,11 +158,7 @@ impl Widget for &mut App {
|
||||||
buf,
|
buf,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
CurrentScreen::Help => {
|
CurrentScreen::Logging => {
|
||||||
outer_frame(¤t_screen, &MAIN_MENU, area, buf);
|
|
||||||
help_screen(area.inner(subscreen_margin), buf);
|
|
||||||
}
|
|
||||||
CurrentScreen::Logging | CurrentScreen::Stopping => {
|
|
||||||
outer_frame(¤t_screen, &LOGGING_MENU, area, buf);
|
outer_frame(¤t_screen, &LOGGING_MENU, area, buf);
|
||||||
logger(area.inner(subscreen_margin), buf);
|
logger(area.inner(subscreen_margin), buf);
|
||||||
}
|
}
|
||||||
|
@ -178,9 +173,9 @@ impl Widget for &mut App {
|
||||||
);
|
);
|
||||||
logger(bottom.inner(subscreen_margin), buf);
|
logger(bottom.inner(subscreen_margin), buf);
|
||||||
}
|
}
|
||||||
CurrentScreen::Sending(sending_screen) => {
|
CurrentScreen::Sending(s) => {
|
||||||
match sending_screen {
|
match s {
|
||||||
SendingScreen::Files(_) => {
|
SendingScreen::Files => {
|
||||||
outer_frame(¤t_screen, &CONTENT_SEND_FILE_MENU, area, buf)
|
outer_frame(¤t_screen, &CONTENT_SEND_FILE_MENU, area, buf)
|
||||||
}
|
}
|
||||||
SendingScreen::Peers => {
|
SendingScreen::Peers => {
|
||||||
|
@ -191,34 +186,9 @@ impl Widget for &mut App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let file_area = header_left.inner(header_margin);
|
self.file_picker
|
||||||
let cwd = self
|
.widget()
|
||||||
.file_finder
|
.render(header_left.inner(header_margin), buf);
|
||||||
.cwd()
|
|
||||||
.as_os_str()
|
|
||||||
.to_string_lossy()
|
|
||||||
.into_owned();
|
|
||||||
match sending_screen {
|
|
||||||
SendingScreen::Files(FileMode::Picking)
|
|
||||||
| SendingScreen::Peers
|
|
||||||
| SendingScreen::Text => {
|
|
||||||
let layout = Layout::vertical([Constraint::Length(3), Constraint::Min(5)]);
|
|
||||||
let [cwd_area, file_area] = layout.areas(file_area);
|
|
||||||
let cwd: Line = cwd.into();
|
|
||||||
Paragraph::new(cwd)
|
|
||||||
.centered()
|
|
||||||
.block(Block::bordered())
|
|
||||||
.render(cwd_area, buf);
|
|
||||||
self.file_finder.widget().render(file_area, buf);
|
|
||||||
}
|
|
||||||
SendingScreen::Files(FileMode::Fuzzy) => {
|
|
||||||
let layout = Layout::vertical([Constraint::Max(3), Constraint::Min(5)]);
|
|
||||||
let [input_area, files_area] = layout.areas(file_area);
|
|
||||||
text_popup(self.file_finder.input.value(), &cwd, input_area, buf);
|
|
||||||
self.file_finder.widget().render(files_area, buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger(header_right.inner(header_margin), buf);
|
logger(header_right.inner(header_margin), buf);
|
||||||
|
|
||||||
peers(
|
peers(
|
||||||
|
@ -228,9 +198,8 @@ impl Widget for &mut App {
|
||||||
buf,
|
buf,
|
||||||
);
|
);
|
||||||
|
|
||||||
if sending_screen == SendingScreen::Text {
|
if s == SendingScreen::Text {
|
||||||
let rect =
|
let rect = centered_rect(area, Constraint::Percentage(80), Constraint::Max(10));
|
||||||
centered_rect(heavy_top, Constraint::Percentage(80), Constraint::Max(6));
|
|
||||||
let text = if let Some(text) = self.text.as_ref() {
|
let text = if let Some(text) = self.text.as_ref() {
|
||||||
text
|
text
|
||||||
} else {
|
} else {
|
||||||
|
@ -264,96 +233,18 @@ fn outer_frame(screen: &CurrentScreen, menu: &Line, area: Rect, buf: &mut Buffer
|
||||||
.render(area, buf);
|
.render(area, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn help_screen(area: Rect, buf: &mut Buffer) {
|
fn text_popup(text: &str, title: &str, area: Rect, buf: &mut Buffer) {
|
||||||
let spacer = "".to_line().centered();
|
let title = Line::from(title.bold());
|
||||||
let main_bindings = vec![
|
let block = Block::bordered().title(title.centered());
|
||||||
Row::new(vec!["".to_line(), spacer.clone(), "".to_line()]),
|
|
||||||
Row::new(vec![
|
|
||||||
// Sending
|
|
||||||
"Send data".bold().into_right_aligned_line(),
|
|
||||||
spacer.clone(),
|
|
||||||
"S".bold().into_left_aligned_line(),
|
|
||||||
]),
|
|
||||||
// Receiving
|
|
||||||
Row::new(vec![
|
|
||||||
"Manage incoming transfer requests"
|
|
||||||
.bold()
|
|
||||||
.into_right_aligned_line(),
|
|
||||||
spacer.clone(),
|
|
||||||
"R".bold().into_left_aligned_line(),
|
|
||||||
]),
|
|
||||||
// logging
|
|
||||||
Row::new(vec![
|
|
||||||
"View logs and change log level"
|
|
||||||
.bold()
|
|
||||||
.into_right_aligned_line(),
|
|
||||||
spacer.clone(),
|
|
||||||
"L".bold().into_left_aligned_line(),
|
|
||||||
]),
|
|
||||||
// misc: main menu
|
|
||||||
Row::new(vec![
|
|
||||||
"Go to the main screen".bold().into_right_aligned_line(),
|
|
||||||
spacer.clone(),
|
|
||||||
"M".bold().into_left_aligned_line(),
|
|
||||||
]),
|
|
||||||
// misc: clear peers
|
|
||||||
Row::new(vec![
|
|
||||||
"Clear peers and rediscover"
|
|
||||||
.bold()
|
|
||||||
.into_right_aligned_line(),
|
|
||||||
spacer.clone(),
|
|
||||||
"C".bold().into_left_aligned_line(),
|
|
||||||
]),
|
|
||||||
// misc: help
|
|
||||||
Row::new(vec![
|
|
||||||
"This help screen".bold().into_right_aligned_line(),
|
|
||||||
spacer.clone(),
|
|
||||||
"H or ?".bold().into_left_aligned_line(),
|
|
||||||
]),
|
|
||||||
// misc: pop
|
|
||||||
Row::new(vec![
|
|
||||||
"Go to previous screen".bold().into_right_aligned_line(),
|
|
||||||
spacer.clone(),
|
|
||||||
"ESC".bold().into_left_aligned_line(),
|
|
||||||
]),
|
|
||||||
// misc: quit
|
|
||||||
Row::new(vec![
|
|
||||||
"Quit the application".bold().into_right_aligned_line(),
|
|
||||||
spacer.clone(),
|
|
||||||
"Q".bold().into_left_aligned_line(),
|
|
||||||
]),
|
|
||||||
];
|
|
||||||
|
|
||||||
let layout = Layout::vertical(vec![
|
|
||||||
Constraint::Max(3),
|
|
||||||
Constraint::Max(12),
|
|
||||||
Constraint::Max(3),
|
|
||||||
])
|
|
||||||
.flex(Flex::SpaceAround);
|
|
||||||
let [intro_area, bindings_area, outro_area] = layout.areas(area);
|
|
||||||
|
|
||||||
let widths = vec![
|
|
||||||
Constraint::Percentage(50),
|
|
||||||
Constraint::Max(6),
|
|
||||||
Constraint::Percentage(50),
|
|
||||||
];
|
|
||||||
let main_bindings = Table::new(main_bindings, widths).header(Row::new(vec![
|
|
||||||
"Action".bold().into_right_aligned_line(),
|
|
||||||
spacer,
|
|
||||||
"Key input".bold().into_left_aligned_line(),
|
|
||||||
]));
|
|
||||||
|
|
||||||
Clear.render(area, buf);
|
Clear.render(area, buf);
|
||||||
|
|
||||||
let intro = "JocalSend is a mode-based application that responds to key-presses. Most modes support the following key bindings:".to_line().centered();
|
block.render(area, buf);
|
||||||
let intro = Paragraph::new(intro).wrap(Wrap { trim: true });
|
|
||||||
|
|
||||||
let outro = "Additional key bindings are available when in the sending or receiving screens, and are displayed at the bottom of the screen there.".to_line().centered();
|
let (_, len) = unicode_segmentation::UnicodeSegmentation::graphemes(text, true).size_hint();
|
||||||
let outro = Paragraph::new(outro).wrap(Wrap { trim: true });
|
let len = len.unwrap_or(text.len()) as u16 + 2;
|
||||||
|
let area = centered_rect(area, Constraint::Length(len), Constraint::Length(1));
|
||||||
|
|
||||||
intro.render(intro_area, buf);
|
Paragraph::new(text).centered().yellow().render(area, buf);
|
||||||
main_bindings.render(bindings_area, buf);
|
|
||||||
outro.render(outro_area, buf);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn logger(area: Rect, buf: &mut Buffer) {
|
fn logger(area: Rect, buf: &mut Buffer) {
|
||||||
|
@ -509,17 +400,3 @@ fn centered_rect(area: Rect, horizontal: Constraint, vertical: Constraint) -> Re
|
||||||
let [area] = Layout::vertical([vertical]).flex(Flex::Center).areas(area);
|
let [area] = Layout::vertical([vertical]).flex(Flex::Center).areas(area);
|
||||||
area
|
area
|
||||||
}
|
}
|
||||||
|
|
||||||
fn text_popup(text: &str, title: &str, area: Rect, buf: &mut Buffer) {
|
|
||||||
let title = Line::from(title.bold());
|
|
||||||
let block = Block::bordered().title(title.centered());
|
|
||||||
Clear.render(area, buf);
|
|
||||||
|
|
||||||
block.render(area, buf);
|
|
||||||
|
|
||||||
let (_, len) = unicode_segmentation::UnicodeSegmentation::graphemes(text, true).size_hint();
|
|
||||||
let len = len.unwrap_or(text.len()) as u16 + 2;
|
|
||||||
let area = centered_rect(area, Constraint::Length(len), Constraint::Length(1));
|
|
||||||
|
|
||||||
Paragraph::new(text).centered().yellow().render(area, buf);
|
|
||||||
}
|
|
||||||
|
|
|
@ -88,7 +88,7 @@ impl Config {
|
||||||
.map_err(Box::new)? // boxed because the error size from figment is large
|
.map_err(Box::new)? // boxed because the error size from figment is large
|
||||||
};
|
};
|
||||||
|
|
||||||
log::debug!("using config: {config:?}");
|
log::info!("using config: {config:?}");
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
use std::{
|
use std::{
|
||||||
net::{SocketAddr, SocketAddrV4},
|
net::{SocketAddr, SocketAddrV4},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Json,
|
Json,
|
||||||
extract::{ConnectInfo, State},
|
extract::{ConnectInfo, State},
|
||||||
response::IntoResponse,
|
|
||||||
};
|
};
|
||||||
use log::{debug, error, trace, warn};
|
use log::{debug, error, trace, warn};
|
||||||
use reqwest::StatusCode;
|
|
||||||
use tokio::net::UdpSocket;
|
use tokio::net::UdpSocket;
|
||||||
|
|
||||||
use crate::{Config, DEFAULT_INTERVAL, JocalService, RunningState, models::Device};
|
use crate::{Config, JocalService, RunningState, models::Device};
|
||||||
|
|
||||||
impl JocalService {
|
impl JocalService {
|
||||||
pub async fn announce(&self, socket: Option<SocketAddr>) -> crate::error::Result<()> {
|
pub async fn announce(&self, socket: Option<SocketAddr>) -> crate::error::Result<()> {
|
||||||
|
@ -30,7 +29,7 @@ impl JocalService {
|
||||||
pub async fn listen_multicast(&self) -> crate::error::Result<()> {
|
pub async fn listen_multicast(&self) -> crate::error::Result<()> {
|
||||||
let mut buf = [0; 65536];
|
let mut buf = [0; 65536];
|
||||||
|
|
||||||
let mut timeout = tokio::time::interval(DEFAULT_INTERVAL);
|
let mut timeout = tokio::time::interval(Duration::from_secs(5));
|
||||||
timeout.tick().await;
|
timeout.tick().await;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
@ -46,6 +45,7 @@ impl JocalService {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
r = self.socket.recv_from(&mut buf) => {
|
r = self.socket.recv_from(&mut buf) => {
|
||||||
|
trace!("received multicast datagram");
|
||||||
match r {
|
match r {
|
||||||
Ok((size, src)) => {
|
Ok((size, src)) => {
|
||||||
let received_msg = String::from_utf8_lossy(&buf[..size]);
|
let received_msg = String::from_utf8_lossy(&buf[..size]);
|
||||||
|
@ -63,7 +63,7 @@ impl JocalService {
|
||||||
|
|
||||||
async fn process_device(&self, message: &str, src: SocketAddr, config: &Config) {
|
async fn process_device(&self, message: &str, src: SocketAddr, config: &Config) {
|
||||||
if let Ok(device) = serde_json::from_str::<Device>(message) {
|
if let Ok(device) = serde_json::from_str::<Device>(message) {
|
||||||
if device == self.config.device {
|
if device.fingerprint == self.config.device.fingerprint {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,10 +100,7 @@ pub async fn register_device(
|
||||||
State(service): State<JocalService>,
|
State(service): State<JocalService>,
|
||||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||||
Json(device): Json<Device>,
|
Json(device): Json<Device>,
|
||||||
) -> impl IntoResponse {
|
) -> Json<Device> {
|
||||||
if device == service.config.device {
|
|
||||||
return StatusCode::ALREADY_REPORTED.into_response();
|
|
||||||
}
|
|
||||||
let mut addr = addr;
|
let mut addr = addr;
|
||||||
addr.set_port(service.config.device.port);
|
addr.set_port(service.config.device.port);
|
||||||
service
|
service
|
||||||
|
@ -111,7 +108,7 @@ pub async fn register_device(
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.insert(device.fingerprint.clone(), (addr, device.clone()));
|
.insert(device.fingerprint.clone(), (addr, device.clone()));
|
||||||
Json(device).into_response()
|
Json(device)
|
||||||
}
|
}
|
||||||
|
|
||||||
//-************************************************************************
|
//-************************************************************************
|
||||||
|
@ -138,3 +135,33 @@ async fn announce_multicast(
|
||||||
socket.send_to(msg.as_bytes(), addr).await?;
|
socket.send_to(msg.as_bytes(), addr).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
async fn announce_unicast(
|
||||||
|
device: &Device,
|
||||||
|
ip: Option<SocketAddr>,
|
||||||
|
client: reqwest::Client,
|
||||||
|
) -> crate::error::Result<()> {
|
||||||
|
// for enumerating subnet peers when multicast fails (https://github.com/localsend/protocol?tab=readme-ov-file#32-http-legacy-mode)
|
||||||
|
let std::net::IpAddr::V4(ip) = local_ip_address::local_ip()? else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut _network_ip = ip;
|
||||||
|
let nifs = NetworkInterface::show()?;
|
||||||
|
for addr in nifs.into_iter().flat_map(|i| i.addr) {
|
||||||
|
if let Addr::V4(V4IfAddr {
|
||||||
|
ip: ifip,
|
||||||
|
netmask: Some(netmask),
|
||||||
|
..
|
||||||
|
}) = addr
|
||||||
|
&& ip == ifip
|
||||||
|
{
|
||||||
|
_network_ip = ip & netmask;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
|
@ -29,11 +29,8 @@ impl JocalService {
|
||||||
|
|
||||||
let handle = Handle::new();
|
let handle = Handle::new();
|
||||||
self.http_handle.get_or_init(|| handle.clone());
|
self.http_handle.get_or_init(|| handle.clone());
|
||||||
|
|
||||||
log::info!("starting http server");
|
log::info!("starting http server");
|
||||||
|
|
||||||
// need to make a custom tls acceptor, see
|
|
||||||
// https://github.com/programatik29/axum-server/blob/master/examples/rustls_session.rs
|
|
||||||
axum_server::bind_rustls(addr, ssl_config)
|
axum_server::bind_rustls(addr, ssl_config)
|
||||||
.handle(handle)
|
.handle(handle)
|
||||||
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
|
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
|
||||||
|
|
159
src/lib.rs
159
src/lib.rs
|
@ -9,7 +9,6 @@ use std::{
|
||||||
collections::BTreeMap,
|
collections::BTreeMap,
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
|
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
|
||||||
path::PathBuf,
|
|
||||||
sync::{Arc, OnceLock},
|
sync::{Arc, OnceLock},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
@ -22,7 +21,7 @@ use tokio::{
|
||||||
net::UdpSocket,
|
net::UdpSocket,
|
||||||
sync::{
|
sync::{
|
||||||
Mutex,
|
Mutex,
|
||||||
mpsc::{self, UnboundedReceiver, UnboundedSender},
|
mpsc::{self, UnboundedSender},
|
||||||
},
|
},
|
||||||
task::JoinSet,
|
task::JoinSet,
|
||||||
};
|
};
|
||||||
|
@ -30,17 +29,16 @@ use transfer::Session;
|
||||||
|
|
||||||
pub const DEFAULT_PORT: u16 = 53317;
|
pub const DEFAULT_PORT: u16 = 53317;
|
||||||
pub const MULTICAST_IP: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 167);
|
pub const MULTICAST_IP: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 167);
|
||||||
pub const DEFAULT_INTERVAL: Duration = Duration::from_millis(100);
|
pub const LISTENING_SOCKET_ADDR: SocketAddrV4 =
|
||||||
|
SocketAddrV4::new(Ipv4Addr::from_bits(0), DEFAULT_PORT);
|
||||||
|
|
||||||
pub type Peers = Arc<Mutex<BTreeMap<String, (SocketAddr, Device)>>>;
|
pub type ShutdownSender = mpsc::Sender<()>;
|
||||||
pub type Sessions = Arc<Mutex<BTreeMap<String, Session>>>; // Session ID to Session
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum JocalTasks {
|
pub enum Listeners {
|
||||||
Udp,
|
Udp,
|
||||||
Http,
|
Http,
|
||||||
Multicast,
|
Multicast,
|
||||||
Tick,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
@ -49,22 +47,11 @@ pub enum ReceiveDialog {
|
||||||
Deny,
|
Deny,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum SendingType {
|
pub enum TransferEvent {
|
||||||
File(PathBuf),
|
Received(Julid),
|
||||||
Text(String),
|
Cancelled(Julid),
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum JocalEvent {
|
|
||||||
ReceivedInbound(Julid),
|
|
||||||
SendApproved(String),
|
|
||||||
SendDenied,
|
|
||||||
SendSuccess { content: String, session: String },
|
|
||||||
SendFailed { error: String },
|
|
||||||
Cancelled { session_id: Julid },
|
|
||||||
ReceiveRequest { id: Julid, request: ReceiveRequest },
|
ReceiveRequest { id: Julid, request: ReceiveRequest },
|
||||||
Tick,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -74,14 +61,6 @@ pub struct ReceiveRequest {
|
||||||
pub tx: UnboundedSender<ReceiveDialog>,
|
pub tx: UnboundedSender<ReceiveDialog>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for ReceiveRequest {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.alias == other.alias && self.files == other.files
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Eq for ReceiveRequest {}
|
|
||||||
|
|
||||||
impl Debug for ReceiveRequest {
|
impl Debug for ReceiveRequest {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("ReceiveRequest")
|
f.debug_struct("ReceiveRequest")
|
||||||
|
@ -94,110 +73,88 @@ impl Debug for ReceiveRequest {
|
||||||
/// Contains the main network and backend state for an application session.
|
/// Contains the main network and backend state for an application session.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct JocalService {
|
pub struct JocalService {
|
||||||
pub peers: Peers,
|
pub peers: Arc<Mutex<BTreeMap<String, (SocketAddr, Device)>>>,
|
||||||
pub sessions: Sessions,
|
pub sessions: Arc<Mutex<BTreeMap<String, Session>>>, // Session ID to Session
|
||||||
pub running_state: Arc<Mutex<RunningState>>,
|
pub running_state: Arc<Mutex<RunningState>>,
|
||||||
pub socket: Arc<UdpSocket>,
|
pub socket: Arc<UdpSocket>,
|
||||||
pub client: reqwest::Client,
|
pub client: reqwest::Client,
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
pub http_handle: Arc<OnceLock<axum_server::Handle>>,
|
http_handle: Arc<OnceLock<axum_server::Handle>>,
|
||||||
// the receiving end will be held by the application so it can update the UI based on backend
|
// the receiving end will be held by the application so it can update the UI based on backend
|
||||||
// events
|
// events
|
||||||
transfer_event_tx: UnboundedSender<JocalEvent>,
|
transfer_event_tx: UnboundedSender<TransferEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JocalService {
|
impl JocalService {
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
config: Config,
|
config: Config,
|
||||||
) -> crate::error::Result<(Self, UnboundedReceiver<JocalEvent>)> {
|
transfer_event_tx: UnboundedSender<TransferEvent>,
|
||||||
let (tx, rx) = mpsc::unbounded_channel();
|
) -> crate::error::Result<Self> {
|
||||||
let addr = SocketAddrV4::new(config.local_ip_addr, DEFAULT_PORT);
|
let socket = UdpSocket::bind(LISTENING_SOCKET_ADDR).await?;
|
||||||
let socket = UdpSocket::bind(addr).await?;
|
socket.set_multicast_loop_v4(true)?;
|
||||||
socket.set_multicast_loop_v4(false)?;
|
socket.set_multicast_ttl_v4(8)?; // 8 hops out from localnet
|
||||||
socket.set_multicast_ttl_v4(1)?; // local subnet only
|
socket.join_multicast_v4(MULTICAST_IP, Ipv4Addr::from_bits(0))?;
|
||||||
socket.join_multicast_v4(MULTICAST_IP, config.local_ip_addr)?;
|
|
||||||
|
|
||||||
let client = reqwest::ClientBuilder::new()
|
let client = reqwest::ClientBuilder::new()
|
||||||
// localsend certs are self-signed
|
// localsend certs are self-signed
|
||||||
.danger_accept_invalid_certs(true)
|
.danger_accept_invalid_certs(true)
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
Ok((
|
Ok(Self {
|
||||||
Self {
|
config,
|
||||||
config,
|
client,
|
||||||
client,
|
socket: socket.into(),
|
||||||
socket: socket.into(),
|
transfer_event_tx,
|
||||||
transfer_event_tx: tx,
|
peers: Default::default(),
|
||||||
peers: Default::default(),
|
sessions: Default::default(),
|
||||||
sessions: Default::default(),
|
running_state: Default::default(),
|
||||||
running_state: Default::default(),
|
http_handle: Default::default(),
|
||||||
http_handle: Default::default(),
|
})
|
||||||
},
|
|
||||||
rx,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start(&self, handles: &mut JoinSet<JocalTasks>) {
|
pub async fn start(&self, handles: &mut JoinSet<Listeners>) {
|
||||||
let service = self.clone();
|
let service = self.clone();
|
||||||
|
|
||||||
handles.spawn(async move {
|
handles.spawn({
|
||||||
if let Err(e) = service.start_http_server().await {
|
async move {
|
||||||
error!("HTTP server error: {e}");
|
if let Err(e) = service.start_http_server().await {
|
||||||
|
error!("HTTP server error: {e}");
|
||||||
|
}
|
||||||
|
Listeners::Http
|
||||||
}
|
}
|
||||||
JocalTasks::Http
|
|
||||||
});
|
});
|
||||||
let service = self.clone();
|
let service = self.clone();
|
||||||
|
|
||||||
handles.spawn(async move {
|
handles.spawn({
|
||||||
if let Err(e) = service.listen_multicast().await {
|
async move {
|
||||||
error!("UDP listener error: {e}");
|
if let Err(e) = service.listen_multicast().await {
|
||||||
|
error!("UDP listener error: {e}");
|
||||||
|
}
|
||||||
|
Listeners::Multicast
|
||||||
}
|
}
|
||||||
JocalTasks::Multicast
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let service = self.clone();
|
let service = self.clone();
|
||||||
handles.spawn(async move {
|
handles.spawn({
|
||||||
let service = &service;
|
async move {
|
||||||
let mut tick = tokio::time::interval(DEFAULT_INTERVAL);
|
loop {
|
||||||
|
let rstate = service.running_state.lock().await;
|
||||||
loop {
|
if *rstate == RunningState::Stopping {
|
||||||
tick.tick().await;
|
break;
|
||||||
service
|
}
|
||||||
.transfer_event_tx
|
if let Err(e) = service.announce(None).await {
|
||||||
.send(JocalEvent::Tick)
|
error!("Announcement error: {e}");
|
||||||
.unwrap_or_else(|e| log::warn!("could not send tick event: {e:?}"));
|
}
|
||||||
|
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||||
let rstate = service.running_state.lock().await;
|
|
||||||
if *rstate == RunningState::Stopping {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
Listeners::Udp
|
||||||
}
|
}
|
||||||
JocalTasks::Tick
|
|
||||||
});
|
|
||||||
|
|
||||||
let service = self.clone();
|
|
||||||
handles.spawn(async move {
|
|
||||||
loop {
|
|
||||||
if let Err(e) = service.announce(None).await {
|
|
||||||
error!("Announcement error: {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
|
||||||
|
|
||||||
let rstate = service.running_state.lock().await;
|
|
||||||
if *rstate == RunningState::Stopping {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
JocalTasks::Udp
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn stop(&self) {
|
pub async fn stop(&self) {
|
||||||
{
|
let mut rstate = self.running_state.lock().await;
|
||||||
let mut rstate = self.running_state.lock().await;
|
*rstate = RunningState::Stopping;
|
||||||
*rstate = RunningState::Stopping;
|
|
||||||
}
|
|
||||||
log::info!("shutting down http server");
|
log::info!("shutting down http server");
|
||||||
self.http_handle
|
self.http_handle
|
||||||
.get()
|
.get()
|
||||||
|
@ -205,12 +162,12 @@ impl JocalService {
|
||||||
.graceful_shutdown(Some(Duration::from_secs(5)));
|
.graceful_shutdown(Some(Duration::from_secs(5)));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn clear_peers(&self) {
|
pub async fn refresh_peers(&self) {
|
||||||
let mut peers = self.peers.lock().await;
|
let mut peers = self.peers.lock().await;
|
||||||
peers.clear();
|
peers.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_event(&self, event: JocalEvent) {
|
pub fn send_event(&self, event: TransferEvent) {
|
||||||
if let Err(e) = self.transfer_event_tx.send(event.clone()) {
|
if let Err(e) = self.transfer_event_tx.send(event.clone()) {
|
||||||
error!("got error sending transfer event '{event:?}': {e:?}");
|
error!("got error sending transfer event '{event:?}': {e:?}");
|
||||||
}
|
}
|
||||||
|
|
91
src/main.rs
91
src/main.rs
|
@ -1,12 +1,12 @@
|
||||||
use std::{path::Path, str::FromStr, time::Duration};
|
use std::{path::Path, str::FromStr, time::Duration};
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use jocalsend::{Config, DEFAULT_INTERVAL, JocalService, JocalTasks, error::Result};
|
use jocalsend::{Config, JocalService, Listeners, error::Result};
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
use ratatui::DefaultTerminal;
|
use ratatui::DefaultTerminal;
|
||||||
use ratatui_explorer::FileExplorer;
|
use ratatui_explorer::FileExplorer;
|
||||||
use tokio::task::JoinSet;
|
use tokio::{sync::mpsc::unbounded_channel, task::JoinSet};
|
||||||
use tui_logger::{LevelFilter, init_logger};
|
use tui_logger::{LevelFilter, init_logger, set_env_filter_from_env};
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
use app::{App, CurrentScreen, Peer};
|
use app::{App, CurrentScreen, Peer};
|
||||||
|
@ -18,11 +18,13 @@ fn main() -> Result<()> {
|
||||||
// just in case we need to display the help
|
// just in case we need to display the help
|
||||||
let _ = Cli::parse();
|
let _ = Cli::parse();
|
||||||
|
|
||||||
|
if std::env::var("RUST_LOG").is_err() {
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var("RUST_LOG", "jocalsend");
|
||||||
|
}
|
||||||
|
}
|
||||||
init_logger(LevelFilter::Info).map_err(|e| std::io::Error::other(format!("{e}")))?;
|
init_logger(LevelFilter::Info).map_err(|e| std::io::Error::other(format!("{e}")))?;
|
||||||
|
set_env_filter_from_env(None);
|
||||||
tui_logger::set_env_filter_from_string(
|
|
||||||
&std::env::var("RUST_LOG").unwrap_or("jocalsend".to_string()),
|
|
||||||
);
|
|
||||||
|
|
||||||
let config = Config::new()?;
|
let config = Config::new()?;
|
||||||
|
|
||||||
|
@ -35,7 +37,11 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
#[tokio::main(flavor = "multi_thread")]
|
#[tokio::main(flavor = "multi_thread")]
|
||||||
async fn start_and_run(terminal: &mut DefaultTerminal, config: Config) -> Result<()> {
|
async fn start_and_run(terminal: &mut DefaultTerminal, config: Config) -> Result<()> {
|
||||||
let (service, event_listener) = JocalService::new(config.clone()).await?;
|
let (event_tx, event_listener) = unbounded_channel();
|
||||||
|
|
||||||
|
let service = JocalService::new(config.clone(), event_tx)
|
||||||
|
.await
|
||||||
|
.expect("Could not create JocalService");
|
||||||
|
|
||||||
let mut app = App::new(service, event_listener);
|
let mut app = App::new(service, event_listener);
|
||||||
|
|
||||||
|
@ -53,52 +59,53 @@ async fn start_and_run(terminal: &mut DefaultTerminal, config: Config) -> Result
|
||||||
|
|
||||||
let mut handles = JoinSet::new();
|
let mut handles = JoinSet::new();
|
||||||
app.service.start(&mut handles).await;
|
app.service.start(&mut handles).await;
|
||||||
let shutdown = shutdown(&mut handles);
|
let mut alarm = tokio::time::interval(Duration::from_millis(200));
|
||||||
let mut shutdown = std::pin::pin!(shutdown);
|
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(|frame| app.draw(frame))?;
|
terminal.draw(|frame| app.draw(frame))?;
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
res = app.handle_events() => {
|
||||||
|
res?;
|
||||||
|
}
|
||||||
|
_ = alarm.tick() => {}
|
||||||
|
}
|
||||||
|
|
||||||
if app.screen() == CurrentScreen::Stopping {
|
if app.screen() == CurrentScreen::Stopping {
|
||||||
tokio::select! {
|
break;
|
||||||
_ = shutdown.as_mut() => {
|
}
|
||||||
break;
|
|
||||||
}
|
|
||||||
_ = tokio::time::sleep(DEFAULT_INTERVAL) => {}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
app.handle_events().await?;
|
|
||||||
|
|
||||||
let peers = app.service.peers.lock().await;
|
let peers = app.service.peers.lock().await;
|
||||||
app.peers.clear();
|
app.peers.clear();
|
||||||
peers.iter().for_each(|(fingerprint, (addr, device))| {
|
peers.iter().for_each(|(fingerprint, (addr, device))| {
|
||||||
let alias = device.alias.clone();
|
let alias = device.alias.clone();
|
||||||
let peer = Peer {
|
let peer = Peer {
|
||||||
alias,
|
alias,
|
||||||
fingerprint: fingerprint.to_owned(),
|
fingerprint: fingerprint.to_owned(),
|
||||||
addr: addr.to_owned(),
|
addr: addr.to_owned(),
|
||||||
};
|
};
|
||||||
app.peers.push(peer);
|
app.peers.push(peer);
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut stale_rx_requests = Vec::with_capacity(app.receive_requests.len());
|
let mut stale_rx_requests = Vec::with_capacity(app.receive_requests.len());
|
||||||
let now = chrono::Utc::now().timestamp_millis() as u64;
|
let now = chrono::Utc::now().timestamp_millis() as u64;
|
||||||
for (id, request) in app.receive_requests.iter() {
|
for (id, request) in app.receive_requests.iter() {
|
||||||
if request.tx.is_closed() || (now - id.timestamp()) > 60_000 {
|
if request.tx.is_closed() || (now - id.timestamp()) > 60_000 {
|
||||||
stale_rx_requests.push(*id);
|
stale_rx_requests.push(*id);
|
||||||
}
|
|
||||||
}
|
|
||||||
for id in stale_rx_requests {
|
|
||||||
app.receive_requests.remove(&id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for id in stale_rx_requests {
|
||||||
|
app.receive_requests.remove(&id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shutdown(&mut handles).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn shutdown(handles: &mut JoinSet<JocalTasks>) {
|
async fn shutdown(handles: &mut JoinSet<Listeners>) {
|
||||||
let mut timeout = tokio::time::interval(Duration::from_secs(5));
|
let mut alarm = tokio::time::interval(Duration::from_secs(5));
|
||||||
timeout.tick().await;
|
alarm.tick().await;
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
join_result = handles.join_next() => {
|
join_result = handles.join_next() => {
|
||||||
|
@ -110,7 +117,7 @@ async fn shutdown(handles: &mut JoinSet<JocalTasks>) {
|
||||||
None => break,
|
None => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = timeout.tick() => {
|
_ = alarm.tick() => {
|
||||||
info!("Exit timeout reached, aborting all unjoined tasks");
|
info!("Exit timeout reached, aborting all unjoined tasks");
|
||||||
handles.abort_all();
|
handles.abort_all();
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::error::LocalSendError;
|
use crate::error::LocalSendError;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct FileMetadata {
|
pub struct FileMetadata {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
@ -21,7 +21,7 @@ pub struct FileMetadata {
|
||||||
pub metadata: Option<FileMetadataExt>,
|
pub metadata: Option<FileMetadataExt>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct FileMetadataExt {
|
pub struct FileMetadataExt {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub modified: Option<String>,
|
pub modified: Option<String>,
|
||||||
|
@ -81,7 +81,7 @@ impl FileMetadata {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum DeviceType {
|
pub enum DeviceType {
|
||||||
Mobile,
|
Mobile,
|
||||||
|
@ -110,14 +110,6 @@ pub struct Device {
|
||||||
pub announce: Option<bool>,
|
pub announce: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for Device {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.fingerprint == other.fingerprint
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Eq for Device {}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum Protocol {
|
pub enum Protocol {
|
||||||
|
|
347
src/transfer.rs
347
src/transfer.rs
|
@ -9,12 +9,11 @@ use axum::{
|
||||||
};
|
};
|
||||||
use julid::Julid;
|
use julid::Julid;
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
use reqwest::Client;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::sync::mpsc::{UnboundedSender, unbounded_channel};
|
use tokio::sync::mpsc::unbounded_channel;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
JocalEvent, JocalService, Peers, ReceiveDialog, ReceiveRequest, SendingType, Sessions,
|
JocalService, ReceiveDialog, ReceiveRequest, TransferEvent,
|
||||||
error::{LocalSendError, Result},
|
error::{LocalSendError, Result},
|
||||||
models::{Device, FileMetadata},
|
models::{Device, FileMetadata},
|
||||||
};
|
};
|
||||||
|
@ -54,14 +53,112 @@ pub struct PrepareUploadRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JocalService {
|
impl JocalService {
|
||||||
|
pub async fn prepare_upload(
|
||||||
|
&self,
|
||||||
|
peer: &str,
|
||||||
|
files: BTreeMap<String, FileMetadata>,
|
||||||
|
) -> Result<PrepareUploadResponse> {
|
||||||
|
let Some((addr, device)) = self.peers.lock().await.get(peer).cloned() else {
|
||||||
|
return Err(LocalSendError::PeerNotFound);
|
||||||
|
};
|
||||||
|
|
||||||
|
log::debug!("preparing upload request");
|
||||||
|
|
||||||
|
let request = self
|
||||||
|
.client
|
||||||
|
.post(format!(
|
||||||
|
"{}://{}/api/localsend/v2/prepare-upload",
|
||||||
|
device.protocol, addr
|
||||||
|
))
|
||||||
|
.json(&PrepareUploadRequest {
|
||||||
|
info: self.config.device.clone(),
|
||||||
|
files: files.clone(),
|
||||||
|
})
|
||||||
|
.timeout(Duration::from_secs(30));
|
||||||
|
|
||||||
|
debug!("sending '{request:?}' to peer at {addr:?}");
|
||||||
|
|
||||||
|
let response = request.send().await?;
|
||||||
|
|
||||||
|
debug!("Response: {response:?}");
|
||||||
|
|
||||||
|
let response: PrepareUploadResponse = match response.json().await {
|
||||||
|
Err(e) => {
|
||||||
|
error!("got error deserializing response: {e:?}");
|
||||||
|
return Err(LocalSendError::RequestError(e));
|
||||||
|
}
|
||||||
|
Ok(r) => r,
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!("decoded response: {response:?}");
|
||||||
|
|
||||||
|
let session = Session {
|
||||||
|
session_id: response.session_id.clone(),
|
||||||
|
files,
|
||||||
|
file_tokens: response.files.clone(),
|
||||||
|
receiver: device,
|
||||||
|
sender: self.config.device.clone(),
|
||||||
|
status: SessionStatus::Active,
|
||||||
|
addr,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.sessions
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.insert(response.session_id.clone(), session);
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn send_file(&self, peer: &str, file_path: PathBuf) -> Result<()> {
|
pub async fn send_file(&self, peer: &str, file_path: PathBuf) -> Result<()> {
|
||||||
let content = SendingType::File(file_path);
|
let file_metadata = FileMetadata::from_path(&file_path)?;
|
||||||
self.send_content(peer, content).await
|
let mut files = BTreeMap::new();
|
||||||
|
files.insert(file_metadata.id.clone(), file_metadata.clone());
|
||||||
|
|
||||||
|
let prepare_response = self.prepare_upload(peer, files).await?;
|
||||||
|
|
||||||
|
let token = prepare_response
|
||||||
|
.files
|
||||||
|
.get(&file_metadata.id)
|
||||||
|
.ok_or(LocalSendError::InvalidToken)?;
|
||||||
|
|
||||||
|
let file_contents = tokio::fs::read(&file_path).await?;
|
||||||
|
let bytes = Bytes::from(file_contents);
|
||||||
|
|
||||||
|
self.send_bytes(
|
||||||
|
&prepare_response.session_id,
|
||||||
|
&file_metadata.id,
|
||||||
|
token,
|
||||||
|
bytes,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_text(&self, peer: &str, text: &str) -> Result<()> {
|
pub async fn send_text(&self, peer: &str, text: &str) -> Result<()> {
|
||||||
let content = SendingType::Text(text.to_owned());
|
let file_metadata = FileMetadata::from_text(text)?;
|
||||||
self.send_content(peer, content).await
|
let mut files = BTreeMap::new();
|
||||||
|
files.insert(file_metadata.id.clone(), file_metadata.clone());
|
||||||
|
|
||||||
|
let prepare_response = self.prepare_upload(peer, files).await?;
|
||||||
|
|
||||||
|
let token = prepare_response
|
||||||
|
.files
|
||||||
|
.get(&file_metadata.id)
|
||||||
|
.ok_or(LocalSendError::InvalidToken)?;
|
||||||
|
|
||||||
|
let bytes = Bytes::from(text.to_owned());
|
||||||
|
|
||||||
|
self.send_bytes(
|
||||||
|
&prepare_response.session_id,
|
||||||
|
&file_metadata.id,
|
||||||
|
token,
|
||||||
|
bytes,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn cancel_upload(&self, session_id: &str) -> Result<()> {
|
pub async fn cancel_upload(&self, session_id: &str) -> Result<()> {
|
||||||
|
@ -86,94 +183,38 @@ impl JocalService {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// spawns a tokio task to wait for responses
|
async fn send_bytes(
|
||||||
async fn send_content(&self, peer: &str, content: SendingType) -> Result<()> {
|
&self,
|
||||||
let (metadata, bytes) = match content {
|
session_id: &str,
|
||||||
SendingType::File(path) => {
|
content_id: &str,
|
||||||
let contents = tokio::fs::read(&path).await?;
|
token: &String,
|
||||||
let bytes = Bytes::from(contents);
|
body: Bytes,
|
||||||
(FileMetadata::from_path(&path)?, bytes)
|
) -> Result<()> {
|
||||||
}
|
let sessions = self.sessions.lock().await;
|
||||||
SendingType::Text(text) => (FileMetadata::from_text(&text)?, Bytes::from(text)),
|
let session = sessions.get(session_id).unwrap();
|
||||||
};
|
|
||||||
|
|
||||||
let mut files = BTreeMap::new();
|
if session.status != SessionStatus::Active {
|
||||||
files.insert(metadata.id.clone(), metadata.clone());
|
return Err(LocalSendError::SessionInactive);
|
||||||
|
}
|
||||||
|
|
||||||
let ourself = self.config.device.clone();
|
if session.file_tokens.get(content_id) != Some(token) {
|
||||||
let client = self.client.clone();
|
return Err(LocalSendError::InvalidToken);
|
||||||
let tx = self.transfer_event_tx.clone();
|
}
|
||||||
let peer = peer.to_string();
|
|
||||||
let sessions = self.sessions.clone();
|
|
||||||
let peers = self.peers.clone();
|
|
||||||
|
|
||||||
tokio::task::spawn(async move {
|
let request = self.client
|
||||||
fn send_tx(msg: JocalEvent, tx: &UnboundedSender<JocalEvent>) {
|
.post(format!(
|
||||||
if let Err(e) = tx.send(msg.clone()) {
|
"{}://{}/api/localsend/v2/upload?sessionId={session_id}&fileId={content_id}&token={token}",
|
||||||
log::error!("got error sending {msg:?} to frontend: {e:?}");
|
session.receiver.protocol, session.addr))
|
||||||
}
|
.body(body).build()?;
|
||||||
}
|
|
||||||
|
|
||||||
let prepare_response =
|
debug!("Uploading bytes: {request:?}");
|
||||||
do_prepare_upload(ourself, &client, &peer, &peers, &sessions, files).await;
|
let response = self.client.execute(request).await?;
|
||||||
|
|
||||||
let prepare_response = match prepare_response {
|
if response.status() != 200 {
|
||||||
Ok(r) => r,
|
warn!("Upload failed: {response:?}");
|
||||||
Err(e) => {
|
return Err(LocalSendError::UploadFailed);
|
||||||
log::debug!("got error from remote receiver: {e:?}");
|
}
|
||||||
send_tx(JocalEvent::SendDenied, &tx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
send_tx(JocalEvent::SendApproved(metadata.id.clone()), &tx);
|
|
||||||
|
|
||||||
let token = match prepare_response.files.get(&metadata.id) {
|
|
||||||
Some(t) => t,
|
|
||||||
None => {
|
|
||||||
send_tx(
|
|
||||||
JocalEvent::SendFailed {
|
|
||||||
error: "missing token in prepare response from remote".into(),
|
|
||||||
},
|
|
||||||
&tx,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let content_id = &metadata.id;
|
|
||||||
let session_id = prepare_response.session_id;
|
|
||||||
log::info!(
|
|
||||||
"sending {content_id} to {}",
|
|
||||||
peers
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.get(&peer)
|
|
||||||
.map(|(_, peer)| peer.alias.as_str())
|
|
||||||
.unwrap_or("unknown peer")
|
|
||||||
);
|
|
||||||
let resp = do_send_bytes(sessions, client, &session_id, content_id, token, bytes).await;
|
|
||||||
|
|
||||||
match resp {
|
|
||||||
Ok(_) => {
|
|
||||||
send_tx(
|
|
||||||
JocalEvent::SendSuccess {
|
|
||||||
content: content_id.to_owned(),
|
|
||||||
session: session_id,
|
|
||||||
},
|
|
||||||
&tx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
send_tx(
|
|
||||||
JocalEvent::SendFailed {
|
|
||||||
error: format!("{e:?}"),
|
|
||||||
},
|
|
||||||
&tx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -198,7 +239,7 @@ pub async fn handle_prepare_upload(
|
||||||
|
|
||||||
match service
|
match service
|
||||||
.transfer_event_tx
|
.transfer_event_tx
|
||||||
.send(JocalEvent::ReceiveRequest { id, request })
|
.send(TransferEvent::ReceiveRequest { id, request })
|
||||||
{
|
{
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
@ -281,7 +322,11 @@ pub async fn handle_receive_upload(
|
||||||
let file_metadata = match session.files.get(file_id) {
|
let file_metadata = match session.files.get(file_id) {
|
||||||
Some(metadata) => metadata,
|
Some(metadata) => metadata,
|
||||||
None => {
|
None => {
|
||||||
return (StatusCode::BAD_REQUEST, "File not found".to_string()).into_response();
|
return (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"File not found".to_string(),
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -289,8 +334,11 @@ pub async fn handle_receive_upload(
|
||||||
|
|
||||||
// Create directory if it doesn't exist
|
// Create directory if it doesn't exist
|
||||||
if let Err(e) = tokio::fs::create_dir_all(download_dir).await {
|
if let Err(e) = tokio::fs::create_dir_all(download_dir).await {
|
||||||
log::error!("could not create download directory '{download_dir:?}', got {e}");
|
return (
|
||||||
return (StatusCode::INTERNAL_SERVER_ERROR, "could not save content").into_response();
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("Failed to create directory: {e}"),
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create file path
|
// Create file path
|
||||||
|
@ -298,16 +346,15 @@ pub async fn handle_receive_upload(
|
||||||
|
|
||||||
// Write file
|
// Write file
|
||||||
if let Err(e) = tokio::fs::write(&file_path, body).await {
|
if let Err(e) = tokio::fs::write(&file_path, body).await {
|
||||||
log::warn!("could not save content to {file_path:?}, got {e}");
|
return (
|
||||||
return (StatusCode::INTERNAL_SERVER_ERROR, "could not save content").into_response();
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("Failed to write file: {e}"),
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"saved content from {} to {file_path:?}",
|
|
||||||
&session.sender.alias
|
|
||||||
);
|
|
||||||
if let Ok(id) = Julid::from_str(session_id) {
|
if let Ok(id) = Julid::from_str(session_id) {
|
||||||
service.send_event(JocalEvent::ReceivedInbound(id));
|
service.send_event(TransferEvent::Received(id));
|
||||||
};
|
};
|
||||||
|
|
||||||
StatusCode::OK.into_response()
|
StatusCode::OK.into_response()
|
||||||
|
@ -337,7 +384,7 @@ pub async fn handle_cancel(
|
||||||
session.status = SessionStatus::Cancelled;
|
session.status = SessionStatus::Cancelled;
|
||||||
|
|
||||||
if let Ok(id) = Julid::from_str(¶ms.session_id) {
|
if let Ok(id) = Julid::from_str(¶ms.session_id) {
|
||||||
service.send_event(JocalEvent::Cancelled { session_id: id });
|
service.send_event(TransferEvent::Cancelled(id));
|
||||||
};
|
};
|
||||||
|
|
||||||
StatusCode::OK.into_response()
|
StatusCode::OK.into_response()
|
||||||
|
@ -349,101 +396,3 @@ pub async fn handle_cancel(
|
||||||
pub struct CancelParams {
|
pub struct CancelParams {
|
||||||
session_id: String,
|
session_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// free function that can be called inside a future in tokio::task::spawn()
|
|
||||||
async fn do_send_bytes(
|
|
||||||
sessions: Sessions,
|
|
||||||
client: Client,
|
|
||||||
session_id: &str,
|
|
||||||
content_id: &str,
|
|
||||||
token: &String,
|
|
||||||
body: Bytes,
|
|
||||||
) -> Result<()> {
|
|
||||||
let sessions = sessions.lock().await;
|
|
||||||
let session = sessions.get(session_id).unwrap();
|
|
||||||
|
|
||||||
if session.status != SessionStatus::Active {
|
|
||||||
return Err(LocalSendError::SessionInactive);
|
|
||||||
}
|
|
||||||
|
|
||||||
if session.file_tokens.get(content_id) != Some(token) {
|
|
||||||
return Err(LocalSendError::InvalidToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
let request = client
|
|
||||||
.post(format!(
|
|
||||||
"{}://{}/api/localsend/v2/upload?sessionId={session_id}&fileId={content_id}&token={token}",
|
|
||||||
session.receiver.protocol, session.addr))
|
|
||||||
.body(body);
|
|
||||||
|
|
||||||
debug!("Uploading bytes: {request:?}");
|
|
||||||
let response = request.send().await?;
|
|
||||||
|
|
||||||
if response.status() != 200 {
|
|
||||||
log::trace!("non-200 remote response: {response:?}");
|
|
||||||
return Err(LocalSendError::UploadFailed);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// free function that can be called inside a future in tokio::task::spawn()
|
|
||||||
async fn do_prepare_upload(
|
|
||||||
ourself: Device,
|
|
||||||
client: &reqwest::Client,
|
|
||||||
peer: &str,
|
|
||||||
peers: &Peers,
|
|
||||||
sessions: &Sessions,
|
|
||||||
files: BTreeMap<String, FileMetadata>,
|
|
||||||
) -> Result<PrepareUploadResponse> {
|
|
||||||
let Some((addr, device)) = peers.lock().await.get(peer).cloned() else {
|
|
||||||
return Err(LocalSendError::PeerNotFound);
|
|
||||||
};
|
|
||||||
|
|
||||||
log::debug!("preparing upload request");
|
|
||||||
|
|
||||||
let request = client
|
|
||||||
.post(format!(
|
|
||||||
"{}://{}/api/localsend/v2/prepare-upload",
|
|
||||||
device.protocol, addr
|
|
||||||
))
|
|
||||||
.json(&PrepareUploadRequest {
|
|
||||||
info: ourself.clone(),
|
|
||||||
files: files.clone(),
|
|
||||||
})
|
|
||||||
.timeout(Duration::from_secs(30));
|
|
||||||
|
|
||||||
debug!("sending '{request:?}' to peer at {addr:?}");
|
|
||||||
|
|
||||||
// tokio::spawn(future);
|
|
||||||
let response = request.send().await?;
|
|
||||||
|
|
||||||
debug!("Response: {response:?}");
|
|
||||||
|
|
||||||
let response: PrepareUploadResponse = match response.json().await {
|
|
||||||
Err(e) => {
|
|
||||||
error!("got error deserializing response: {e:?}");
|
|
||||||
return Err(LocalSendError::RequestError(e));
|
|
||||||
}
|
|
||||||
Ok(r) => r,
|
|
||||||
};
|
|
||||||
|
|
||||||
debug!("decoded response: {response:?}");
|
|
||||||
|
|
||||||
let session = Session {
|
|
||||||
session_id: response.session_id.clone(),
|
|
||||||
files,
|
|
||||||
file_tokens: response.files.clone(),
|
|
||||||
receiver: device,
|
|
||||||
sender: ourself.clone(),
|
|
||||||
status: SessionStatus::Active,
|
|
||||||
addr,
|
|
||||||
};
|
|
||||||
|
|
||||||
sessions
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.insert(response.session_id.clone(), session);
|
|
||||||
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue