Compare commits
32 Commits
2435e25aee
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 676f0ca01c | |||
| 92b6d6f1dd | |||
| 754dd8b053 | |||
| 6fa26b3ddb | |||
| a1c9832515 | |||
| 5c3057e998 | |||
| a0c90f57d5 | |||
| 5c09120c23 | |||
| 1c4aaa7040 | |||
| 2b29716fee | |||
| e1e97ef79d | |||
| 3329860b02 | |||
| 8c0d80d7fb | |||
| cef77e86b7 | |||
| a4c2c039d2 | |||
| 73d3b2a27f | |||
| 8c4f56a7ab | |||
| 49c59cbaea | |||
| 13e91fdfbf | |||
| afcbadee8b | |||
| 91c65e0861 | |||
| 735e825c7d | |||
| cdd4151462 | |||
| 76aedbaf96 | |||
| 8b2ed0e700 | |||
| 49b4162448 | |||
| e2085c1baa | |||
| be46f92ddf | |||
| 702f16f199 | |||
| 9b3f4a5fe6 | |||
| 3498a2926c | |||
| 76713db985 |
@@ -12,3 +12,4 @@ frontend/node_modules
|
|||||||
|
|
||||||
backend/target
|
backend/target
|
||||||
backend/.gitignore
|
backend/.gitignore
|
||||||
|
backend/data
|
||||||
|
|||||||
7
README.md
Normal file
7
README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Warren
|
||||||
|
A lightweight self-hosted web-based file manager
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
> **NOTE:** Warren will create an initial admin user with the email `admin@example.com` and the password `admin1234567`. Make sure to change this user's password or delete it after you have created another user with admin privileges.
|
||||||
|
### Docker
|
||||||
|
Copy the repository's `compose.yaml` as a starting point. Use environment variables to configure warren to your liking.
|
||||||
1
backend/.gitignore
vendored
1
backend/.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
target
|
target
|
||||||
serve
|
serve
|
||||||
.env
|
.env
|
||||||
|
data
|
||||||
|
|||||||
251
backend/Cargo.lock
generated
251
backend/Cargo.lock
generated
@@ -17,6 +17,17 @@ version = "2.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aes"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cipher",
|
||||||
|
"cpufeatures",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
@@ -53,6 +64,15 @@ version = "1.0.98"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arbitrary"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||||
|
dependencies = [
|
||||||
|
"derive_arbitrary",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "argon2"
|
name = "argon2"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
@@ -248,12 +268,23 @@ version = "1.10.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bzip2"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bea8dcd42434048e4f7a304411d9273a411f647446c1234a65ce0554923f4cff"
|
||||||
|
dependencies = [
|
||||||
|
"libbz2-rs-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.29"
|
version = "1.2.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362"
|
checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"jobserver",
|
||||||
|
"libc",
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -278,6 +309,16 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cipher"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"inout",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@@ -293,6 +334,12 @@ version = "0.9.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "constant_time_eq"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cookie"
|
name = "cookie"
|
||||||
version = "0.18.1"
|
version = "0.18.1"
|
||||||
@@ -344,6 +391,15 @@ version = "2.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc32fast"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-queue"
|
name = "crossbeam-queue"
|
||||||
version = "0.3.12"
|
version = "0.3.12"
|
||||||
@@ -410,6 +466,12 @@ version = "2.9.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
|
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deflate64"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "der"
|
name = "der"
|
||||||
version = "0.7.10"
|
version = "0.7.10"
|
||||||
@@ -430,6 +492,17 @@ dependencies = [
|
|||||||
"powerfmt",
|
"powerfmt",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_arbitrary"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_more"
|
name = "derive_more"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
@@ -548,6 +621,17 @@ version = "2.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flate2"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
|
||||||
|
dependencies = [
|
||||||
|
"crc32fast",
|
||||||
|
"libz-rs-sys",
|
||||||
|
"miniz_oxide",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flume"
|
name = "flume"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@@ -1036,6 +1120,15 @@ dependencies = [
|
|||||||
"hashbrown",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inout"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "io-uring"
|
name = "io-uring"
|
||||||
version = "0.7.8"
|
version = "0.7.8"
|
||||||
@@ -1069,6 +1162,16 @@ version = "1.0.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jobserver"
|
||||||
|
version = "0.1.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.3",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.77"
|
version = "0.3.77"
|
||||||
@@ -1088,12 +1191,38 @@ dependencies = [
|
|||||||
"spin",
|
"spin",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libbz2-rs-sys"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.174"
|
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 = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "liblzma"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "10bf66f4598dc77ff96677c8e763655494f00ff9c1cf79e2eb5bb07bc31f807d"
|
||||||
|
dependencies = [
|
||||||
|
"liblzma-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "liblzma-sys"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "01b9596486f6d60c3bbe644c0e1be1aa6ccc472ad630fe8927b456973d7cb736"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libm"
|
name = "libm"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
@@ -1106,10 +1235,20 @@ version = "0.30.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
|
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"cc",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libz-rs-sys"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221"
|
||||||
|
dependencies = [
|
||||||
|
"zlib-rs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
@@ -1427,6 +1566,16 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pbkdf2"
|
||||||
|
version = "0.12.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
"hmac",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pem-rfc7468"
|
name = "pem-rfc7468"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@@ -1496,6 +1645,12 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppmd-rust"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c834641d8ad1b348c9ee86dec3b9840d805acd5f24daa5f90c788951a52ff59b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
@@ -1878,6 +2033,12 @@ dependencies = [
|
|||||||
"rand_core",
|
"rand_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simd-adler32"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.10"
|
version = "0.4.10"
|
||||||
@@ -2560,6 +2721,7 @@ version = "1.17.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
|
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"getrandom 0.3.3",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"serde",
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -2646,6 +2808,7 @@ dependencies = [
|
|||||||
"sqlx",
|
"sqlx",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
@@ -2653,6 +2816,7 @@ dependencies = [
|
|||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3081,6 +3245,20 @@ name = "zeroize"
|
|||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||||
|
dependencies = [
|
||||||
|
"zeroize_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize_derive"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerotrie"
|
name = "zerotrie"
|
||||||
@@ -3114,3 +3292,76 @@ dependencies = [
|
|||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zip"
|
||||||
|
version = "4.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8835eb39822904d39cb19465de1159e05d371973f0c6df3a365ad50565ddc8b9"
|
||||||
|
dependencies = [
|
||||||
|
"aes",
|
||||||
|
"arbitrary",
|
||||||
|
"bzip2",
|
||||||
|
"constant_time_eq",
|
||||||
|
"crc32fast",
|
||||||
|
"deflate64",
|
||||||
|
"flate2",
|
||||||
|
"getrandom 0.3.3",
|
||||||
|
"hmac",
|
||||||
|
"indexmap",
|
||||||
|
"liblzma",
|
||||||
|
"memchr",
|
||||||
|
"pbkdf2",
|
||||||
|
"ppmd-rust",
|
||||||
|
"sha1",
|
||||||
|
"time",
|
||||||
|
"zeroize",
|
||||||
|
"zopfli",
|
||||||
|
"zstd",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zlib-rs"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zopfli"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"crc32fast",
|
||||||
|
"log",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd"
|
||||||
|
version = "0.13.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||||
|
dependencies = [
|
||||||
|
"zstd-safe",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd-safe"
|
||||||
|
version = "7.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||||
|
dependencies = [
|
||||||
|
"zstd-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd-sys"
|
||||||
|
version = "2.0.15+zstd.1.5.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ path = "src/bin/backend/main.rs"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.98"
|
anyhow = "1.0.98"
|
||||||
argon2 = "0.5.3"
|
argon2 = { version = "0.5.3", features = ["std"] }
|
||||||
axum = { version = "0.8.4", features = ["multipart", "query"] }
|
axum = { version = "0.8.4", features = ["multipart", "query"] }
|
||||||
axum-extra = { version = "0.10.1", features = ["cookie", "multipart"] }
|
axum-extra = { version = "0.10.1", features = ["cookie", "multipart"] }
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
@@ -29,19 +29,15 @@ regex = "1.11.1"
|
|||||||
rustix = { version = "1.0.8", features = ["fs"] }
|
rustix = { version = "1.0.8", features = ["fs"] }
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
sqlx = { version = "0.8.6", features = [
|
sqlx = { version = "0.8.6", features = ["chrono", "runtime-tokio", "sqlite", "time", "uuid"] }
|
||||||
"chrono",
|
|
||||||
"postgres",
|
|
||||||
"runtime-tokio",
|
|
||||||
"time",
|
|
||||||
"uuid",
|
|
||||||
] }
|
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
tokio = { version = "1.46.1", features = ["full"] }
|
tokio = { version = "1.46.1", features = ["full"] }
|
||||||
tokio-util = "0.7.15"
|
tokio-stream = "0.1.17"
|
||||||
|
tokio-util = { version = "0.7.15", features = ["io-util"] }
|
||||||
tower = "0.5.2"
|
tower = "0.5.2"
|
||||||
tower-http = { version = "0.6.6", features = ["cors", "fs", "trace"] }
|
tower-http = { version = "0.6.6", features = ["cors", "fs", "trace"] }
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-subscriber = "0.3.19"
|
tracing-subscriber = "0.3.19"
|
||||||
url = "2.5.4"
|
url = "2.5.4"
|
||||||
uuid = { version = "1.17.0", features = ["serde"] }
|
uuid = { version = "1.17.0", features = ["serde", "v4"] }
|
||||||
|
zip = "4.5.0"
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
CREATE TABLE warrens (
|
|
||||||
id UUID PRIMARY KEY DEFAULT GEN_RANDOM_UUID(),
|
|
||||||
path VARCHAR NOT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_warrens_path ON warrens(path);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE warrens ADD COLUMN name VARCHAR NOT NULL;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
CREATE TABLE users (
|
|
||||||
id UUID PRIMARY KEY DEFAULT GEN_RANDOM_UUID(),
|
|
||||||
name VARCHAR NOT NULL,
|
|
||||||
email VARCHAR NOT NULL,
|
|
||||||
hash VARCHAR NOT NULL,
|
|
||||||
admin BOOLEAN NOT NULL DEFAULT FALSE,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE users ADD CONSTRAINT users_email_key UNIQUE (email);
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
CREATE TABLE auth_sessions (
|
|
||||||
session_id VARCHAR NOT NULL PRIMARY KEY,
|
|
||||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
expires_at TIMESTAMP NOT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
CREATE TABLE user_warrens (
|
|
||||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
warren_id UUID NOT NULL REFERENCES warrens(id) ON DELETE CASCADE,
|
|
||||||
can_create_children BOOLEAN NOT NULL,
|
|
||||||
can_list_files BOOLEAN NOT NULL,
|
|
||||||
can_read_files BOOLEAN NOT NULL,
|
|
||||||
can_modify_files BOOLEAN NOT NULL,
|
|
||||||
can_delete_files BOOLEAN NOT NULL,
|
|
||||||
can_delete_warren BOOLEAN NOT NULL,
|
|
||||||
PRIMARY KEY(user_id, warren_id)
|
|
||||||
);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE user_warrens DROP COLUMN can_create_children, DROP COLUMN can_delete_warren;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE users ALTER COLUMN hash DROP NOT NULL;
|
|
||||||
ALTER TABLE users ADD COLUMN oidc_sub VARCHAR UNIQUE;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
CREATE TABLE shares (
|
|
||||||
id UUID PRIMARY KEY DEFAULT GEN_RANDOM_UUID(),
|
|
||||||
creator_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
warren_id UUID NOT NULL REFERENCES warrens(id) ON DELETE CASCADE,
|
|
||||||
path VARCHAR NOT NULL,
|
|
||||||
password_hash VARCHAR NOT NULL,
|
|
||||||
expires_at TIMESTAMP,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
ALTER TABLE
|
|
||||||
user_warrens
|
|
||||||
ADD COLUMN
|
|
||||||
can_list_shares BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
ADD COLUMN
|
|
||||||
can_create_shares BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
ADD COLUMN
|
|
||||||
can_modify_shares BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
ADD COLUMN
|
|
||||||
can_delete_shares BOOLEAN NOT NULL DEFAULT false;
|
|
||||||
|
|
||||||
ALTER TABLE
|
|
||||||
user_warrens
|
|
||||||
ALTER COLUMN
|
|
||||||
can_list_shares DROP DEFAULT,
|
|
||||||
ALTER COLUMN
|
|
||||||
can_create_shares DROP DEFAULT,
|
|
||||||
ALTER COLUMN
|
|
||||||
can_modify_shares DROP DEFAULT,
|
|
||||||
ALTER COLUMN
|
|
||||||
can_delete_shares DROP DEFAULT;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE shares ALTER COLUMN password_hash DROP NOT NULL;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
CREATE INDEX idx_shares_path ON shares(path);
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
INSERT INTO users (
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
hash,
|
|
||||||
admin
|
|
||||||
) VALUES (
|
|
||||||
'admin',
|
|
||||||
'admin@example.com',
|
|
||||||
'$argon2id$v=19$m=19456,t=2,p=1$H1WsElL4921/WD5oPkY7JQ$aHudNG8z0ns3pRULfuDpuEkxPUbGxq9AHC4QGyt5odc',
|
|
||||||
true
|
|
||||||
);
|
|
||||||
55
backend/migrations/20250906174941_init.sql
Normal file
55
backend/migrations/20250906174941_init.sql
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
CREATE TABLE users (
|
||||||
|
id BLOB NOT NULL PRIMARY KEY,
|
||||||
|
oidc_sub TEXT UNIQUE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
email TEXT NOT NULL UNIQUE,
|
||||||
|
admin BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
hash TEXT,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE warrens (
|
||||||
|
id BLOB NOT NULL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
path TEXT NOT NULL UNIQUE,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE user_warrens (
|
||||||
|
user_id BLOB NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
warren_id BLOB NOT NULL REFERENCES warrens(id) ON DELETE CASCADE,
|
||||||
|
can_list_files BOOLEAN NOT NULL,
|
||||||
|
can_read_files BOOLEAN NOT NULL,
|
||||||
|
can_modify_files BOOLEAN NOT NULL,
|
||||||
|
can_delete_files BOOLEAN NOT NULL,
|
||||||
|
can_list_shares BOOLEAN NOT NULL,
|
||||||
|
can_create_shares BOOLEAN NOT NULL,
|
||||||
|
can_modify_shares BOOLEAN NOT NULL,
|
||||||
|
can_delete_shares BOOLEAN NOT NULL,
|
||||||
|
PRIMARY KEY(user_id, warren_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE shares (
|
||||||
|
id BLOB NOT NULL PRIMARY KEY,
|
||||||
|
creator_id BLOB NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
warren_id BLOB NOT NULL REFERENCES warrens(id) ON DELETE CASCADE,
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
password_hash TEXT,
|
||||||
|
expires_at DATETIME,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_shares_path ON shares(path);
|
||||||
|
|
||||||
|
CREATE TABLE auth_sessions (
|
||||||
|
session_id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
user_id BLOB NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
expires_at DATETIME NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE application_options (
|
||||||
|
key TEXT NOT NULL PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
);
|
||||||
@@ -7,11 +7,11 @@ use warren::{
|
|||||||
metrics_debug_logger::MetricsDebugLogger,
|
metrics_debug_logger::MetricsDebugLogger,
|
||||||
notifier_debug_logger::NotifierDebugLogger,
|
notifier_debug_logger::NotifierDebugLogger,
|
||||||
oidc::{Oidc, OidcConfig},
|
oidc::{Oidc, OidcConfig},
|
||||||
postgres::{Postgres, PostgresConfig},
|
sqlite::{Sqlite, SqliteConfig},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main(flavor = "multi_thread")]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
dotenv::dotenv().ok();
|
dotenv::dotenv().ok();
|
||||||
|
|
||||||
@@ -25,16 +25,15 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
let metrics = MetricsDebugLogger::new();
|
let metrics = MetricsDebugLogger::new();
|
||||||
let notifier = NotifierDebugLogger::new();
|
let notifier = NotifierDebugLogger::new();
|
||||||
|
|
||||||
let postgres_config =
|
let sqlite_config = SqliteConfig::new(config.database_url.clone());
|
||||||
PostgresConfig::new(config.database_url.clone(), config.database_name.clone());
|
let sqlite = Sqlite::new(sqlite_config).await?;
|
||||||
let postgres = Postgres::new(postgres_config).await?;
|
|
||||||
|
|
||||||
let fs_config = FileSystemConfig::from_env(config.serve_dir.clone())?;
|
let fs_config = FileSystemConfig::from_env(config.serve_dir.clone())?;
|
||||||
let fs = FileSystem::new(fs_config)?;
|
let fs = FileSystem::new(fs_config)?;
|
||||||
let fs_service = domain::warren::service::file_system::Service::new(fs, metrics, notifier);
|
let fs_service = domain::warren::service::file_system::Service::new(fs, metrics, notifier);
|
||||||
|
|
||||||
let warren_service = domain::warren::service::warren::Service::new(
|
let warren_service = domain::warren::service::warren::Service::new(
|
||||||
postgres.clone(),
|
sqlite.clone(),
|
||||||
metrics,
|
metrics,
|
||||||
notifier,
|
notifier,
|
||||||
fs_service.clone(),
|
fs_service.clone(),
|
||||||
@@ -47,13 +46,18 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let option_service =
|
||||||
|
domain::warren::service::option::Service::new(sqlite.clone(), metrics, notifier);
|
||||||
|
|
||||||
let auth_service = domain::warren::service::auth::Service::new(
|
let auth_service = domain::warren::service::auth::Service::new(
|
||||||
postgres,
|
sqlite,
|
||||||
metrics,
|
metrics,
|
||||||
notifier,
|
notifier,
|
||||||
config.auth,
|
config.auth,
|
||||||
oidc_service,
|
oidc_service,
|
||||||
);
|
option_service,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let server_config = HttpServerConfig::new(
|
let server_config = HttpServerConfig::new(
|
||||||
&config.server_address,
|
&config.server_address,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use tracing::level_filters::LevelFilter;
|
|||||||
use crate::domain::warren::service::auth::AuthConfig;
|
use crate::domain::warren::service::auth::AuthConfig;
|
||||||
|
|
||||||
const DATABASE_URL_KEY: &str = "DATABASE_URL";
|
const DATABASE_URL_KEY: &str = "DATABASE_URL";
|
||||||
const DATABASE_NAME_KEY: &str = "DATABASE_NAME";
|
|
||||||
|
|
||||||
const SERVER_ADDRESS_KEY: &str = "SERVER_ADDRESS";
|
const SERVER_ADDRESS_KEY: &str = "SERVER_ADDRESS";
|
||||||
const SERVER_PORT_KEY: &str = "SERVER_PORT";
|
const SERVER_PORT_KEY: &str = "SERVER_PORT";
|
||||||
@@ -28,7 +27,6 @@ pub struct Config {
|
|||||||
pub static_frontend_dir: Option<String>,
|
pub static_frontend_dir: Option<String>,
|
||||||
|
|
||||||
pub database_url: String,
|
pub database_url: String,
|
||||||
pub database_name: String,
|
|
||||||
|
|
||||||
pub log_level: LevelFilter,
|
pub log_level: LevelFilter,
|
||||||
|
|
||||||
@@ -45,7 +43,6 @@ impl Config {
|
|||||||
let static_frontend_dir = Self::load_env(STATIC_FRONTEND_DIRECTORY).ok();
|
let static_frontend_dir = Self::load_env(STATIC_FRONTEND_DIRECTORY).ok();
|
||||||
|
|
||||||
let database_url = Self::load_env(DATABASE_URL_KEY)?;
|
let database_url = Self::load_env(DATABASE_URL_KEY)?;
|
||||||
let database_name = Self::load_env(DATABASE_NAME_KEY)?;
|
|
||||||
|
|
||||||
let log_level =
|
let log_level =
|
||||||
LevelFilter::from_str(&Self::load_env(LOG_LEVEL_KEY).unwrap_or("INFO".to_string()))
|
LevelFilter::from_str(&Self::load_env(LOG_LEVEL_KEY).unwrap_or("INFO".to_string()))
|
||||||
@@ -62,7 +59,6 @@ impl Config {
|
|||||||
static_frontend_dir,
|
static_frontend_dir,
|
||||||
|
|
||||||
database_url,
|
database_url,
|
||||||
database_name,
|
|
||||||
|
|
||||||
log_level,
|
log_level,
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,24 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct NoopService {}
|
||||||
|
impl OidcService for NoopService {
|
||||||
|
async fn get_redirect(
|
||||||
|
&self,
|
||||||
|
_: GetRedirectRequest,
|
||||||
|
) -> Result<GetRedirectResponse, GetRedirectError> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_user_info(
|
||||||
|
&self,
|
||||||
|
_: GetUserInfoRequest,
|
||||||
|
) -> Result<GetUserInfoResponse, GetUserInfoError> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<R, M, N> OidcService for Service<R, M, N>
|
impl<R, M, N> OidcService for Service<R, M, N>
|
||||||
where
|
where
|
||||||
R: OidcRepository,
|
R: OidcRepository,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
mod requests;
|
mod requests;
|
||||||
|
use bytes::Bytes;
|
||||||
|
use futures_util::Stream;
|
||||||
pub use requests::*;
|
pub use requests::*;
|
||||||
use tokio_util::io::ReaderStream;
|
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ pub struct File {
|
|||||||
name: FileName,
|
name: FileName,
|
||||||
file_type: FileType,
|
file_type: FileType,
|
||||||
mime_type: Option<FileMimeType>,
|
mime_type: Option<FileMimeType>,
|
||||||
|
size: FileSize,
|
||||||
created_at: Option<u64>,
|
created_at: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,12 +23,14 @@ impl File {
|
|||||||
name: FileName,
|
name: FileName,
|
||||||
file_type: FileType,
|
file_type: FileType,
|
||||||
mime_type: Option<FileMimeType>,
|
mime_type: Option<FileMimeType>,
|
||||||
|
size: FileSize,
|
||||||
created_at: Option<u64>,
|
created_at: Option<u64>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name,
|
name,
|
||||||
file_type,
|
file_type,
|
||||||
mime_type,
|
mime_type,
|
||||||
|
size,
|
||||||
created_at,
|
created_at,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,6 +47,10 @@ impl File {
|
|||||||
self.mime_type.as_ref()
|
self.mime_type.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn size(&self) -> FileSize {
|
||||||
|
self.size
|
||||||
|
}
|
||||||
|
|
||||||
pub fn created_at(&self) -> Option<u64> {
|
pub fn created_at(&self) -> Option<u64> {
|
||||||
self.created_at
|
self.created_at
|
||||||
}
|
}
|
||||||
@@ -80,8 +88,23 @@ impl FileName {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Display)]
|
||||||
|
pub struct FileSize(u64);
|
||||||
|
|
||||||
|
impl FileSize {
|
||||||
|
pub fn new(value: u64) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FileSize> for u64 {
|
||||||
|
fn from(value: FileSize) -> Self {
|
||||||
|
value.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A valid file type
|
/// A valid file type
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Display)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Display)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub enum FileType {
|
pub enum FileType {
|
||||||
File,
|
File,
|
||||||
@@ -93,11 +116,19 @@ pub enum FileType {
|
|||||||
pub struct FileMimeType(String);
|
pub struct FileMimeType(String);
|
||||||
|
|
||||||
impl FileMimeType {
|
impl FileMimeType {
|
||||||
|
pub fn new_raw(raw: &str) -> Self {
|
||||||
|
Self(raw.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn from_name(name: &str) -> Option<Self> {
|
pub fn from_name(name: &str) -> Option<Self> {
|
||||||
mime_guess::from_path(name)
|
mime_guess::from_path(name)
|
||||||
.first_raw()
|
.first_raw()
|
||||||
.map(|s| Self(s.to_owned()))
|
.map(|s| Self(s.to_owned()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
self.0.as_str()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A valid file path that might start with a slash
|
/// A valid file path that might start with a slash
|
||||||
@@ -263,21 +294,42 @@ impl From<AbsoluteFilePath> for FilePath {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
pub type FileStreamInner =
|
||||||
pub struct FileStream(ReaderStream<tokio::fs::File>);
|
Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send + Sync + Unpin + 'static>;
|
||||||
|
|
||||||
|
pub struct FileStream {
|
||||||
|
file_type: FileType,
|
||||||
|
mime_type: Option<FileMimeType>,
|
||||||
|
stream: FileStreamInner,
|
||||||
|
}
|
||||||
|
|
||||||
impl FileStream {
|
impl FileStream {
|
||||||
pub fn new(stream: ReaderStream<tokio::fs::File>) -> Self {
|
pub fn new<S>(file_type: FileType, mime_type: Option<FileMimeType>, stream: S) -> Self
|
||||||
Self(stream)
|
where
|
||||||
|
S: Stream<Item = Result<Bytes, std::io::Error>> + Send + Sync + Unpin + 'static,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
file_type,
|
||||||
|
mime_type,
|
||||||
|
stream: Box::new(stream),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stream(&self) -> &ReaderStream<tokio::fs::File> {
|
pub fn file_type(&self) -> FileType {
|
||||||
&self.0
|
self.file_type
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mime_type(&self) -> Option<&FileMimeType> {
|
||||||
|
self.mime_type.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stream(&self) -> &FileStreamInner {
|
||||||
|
&self.stream
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<FileStream> for ReaderStream<tokio::fs::File> {
|
impl From<FileStream> for FileStreamInner {
|
||||||
fn from(value: FileStream) -> Self {
|
fn from(value: FileStream) -> Self {
|
||||||
value.0
|
value.stream
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::domain::warren::models::file::AbsoluteFilePath;
|
use super::AbsoluteFilePathList;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct CatRequest {
|
pub struct CatRequest {
|
||||||
path: AbsoluteFilePath,
|
paths: AbsoluteFilePathList,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CatRequest {
|
impl CatRequest {
|
||||||
pub fn new(path: AbsoluteFilePath) -> Self {
|
pub fn new(paths: AbsoluteFilePathList) -> Self {
|
||||||
Self { path }
|
Self { paths }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path(&self) -> &AbsoluteFilePath {
|
pub fn paths(&self) -> &AbsoluteFilePathList {
|
||||||
&self.path
|
&self.paths
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_path(self) -> AbsoluteFilePath {
|
pub fn into_paths(self) -> AbsoluteFilePathList {
|
||||||
self.path
|
self.paths
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum CatError {
|
pub enum CatError {
|
||||||
#[error("The file does not exist")]
|
#[error("A file does not exist")]
|
||||||
NotFound,
|
NotFound,
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Unknown(#[from] anyhow::Error),
|
Unknown(#[from] anyhow::Error),
|
||||||
|
|||||||
@@ -16,4 +16,40 @@ pub use mv::*;
|
|||||||
pub use rm::*;
|
pub use rm::*;
|
||||||
pub use save::*;
|
pub use save::*;
|
||||||
pub use stat::*;
|
pub use stat::*;
|
||||||
|
use thiserror::Error;
|
||||||
pub use touch::*;
|
pub use touch::*;
|
||||||
|
|
||||||
|
use super::AbsoluteFilePath;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct AbsoluteFilePathList(Vec<AbsoluteFilePath>);
|
||||||
|
|
||||||
|
impl From<AbsoluteFilePathList> for Vec<AbsoluteFilePath> {
|
||||||
|
fn from(value: AbsoluteFilePathList) -> Self {
|
||||||
|
value.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AbsoluteFilePathList {
|
||||||
|
pub fn new(paths: Vec<AbsoluteFilePath>) -> Result<Self, AbsoluteFilePathListError> {
|
||||||
|
if paths.is_empty() {
|
||||||
|
return Err(AbsoluteFilePathListError::Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self(paths))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paths(&self) -> &Vec<AbsoluteFilePath> {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paths_mut(&mut self) -> &mut Vec<AbsoluteFilePath> {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Error)]
|
||||||
|
pub enum AbsoluteFilePathListError {
|
||||||
|
#[error("A list must not be empty")]
|
||||||
|
Empty,
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,36 +2,38 @@ use thiserror::Error;
|
|||||||
|
|
||||||
use crate::domain::warren::models::file::AbsoluteFilePath;
|
use crate::domain::warren::models::file::AbsoluteFilePath;
|
||||||
|
|
||||||
|
use super::AbsoluteFilePathList;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct MvRequest {
|
pub struct MvRequest {
|
||||||
path: AbsoluteFilePath,
|
paths: AbsoluteFilePathList,
|
||||||
target_path: AbsoluteFilePath,
|
target_path: AbsoluteFilePath,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MvRequest {
|
impl MvRequest {
|
||||||
pub fn new(path: AbsoluteFilePath, target_path: AbsoluteFilePath) -> Self {
|
pub fn new(paths: AbsoluteFilePathList, target_path: AbsoluteFilePath) -> Self {
|
||||||
Self { path, target_path }
|
Self { paths, target_path }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path(&self) -> &AbsoluteFilePath {
|
pub fn paths(&self) -> &AbsoluteFilePathList {
|
||||||
&self.path
|
&self.paths
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn target_path(&self) -> &AbsoluteFilePath {
|
pub fn target_path(&self) -> &AbsoluteFilePath {
|
||||||
&self.target_path
|
&self.target_path
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unpack(self) -> (AbsoluteFilePath, AbsoluteFilePath) {
|
pub fn unpack(self) -> (AbsoluteFilePathList, AbsoluteFilePath) {
|
||||||
(self.path, self.target_path)
|
(self.paths, self.target_path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum MvError {
|
pub enum MvError {
|
||||||
#[error("The path does not exist")]
|
#[error("The path does not exist")]
|
||||||
NotFound,
|
NotFound(AbsoluteFilePath),
|
||||||
#[error("The target path already exists")]
|
#[error("The target path already exists")]
|
||||||
AlreadyExists,
|
AlreadyExists(AbsoluteFilePath),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Unknown(#[from] anyhow::Error),
|
Unknown(#[from] anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::domain::warren::models::file::AbsoluteFilePath;
|
use crate::domain::warren::models::file::{AbsoluteFilePath, AbsoluteFilePathList};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct RmRequest {
|
pub struct RmRequest {
|
||||||
path: AbsoluteFilePath,
|
paths: AbsoluteFilePathList,
|
||||||
force: bool,
|
force: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RmRequest {
|
impl RmRequest {
|
||||||
pub fn new(path: AbsoluteFilePath, force: bool) -> Self {
|
pub fn new(paths: AbsoluteFilePathList, force: bool) -> Self {
|
||||||
Self { path, force }
|
Self { paths, force }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path(&self) -> &AbsoluteFilePath {
|
pub fn paths(&self) -> &AbsoluteFilePathList {
|
||||||
&self.path
|
&self.paths
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_path(self) -> AbsoluteFilePath {
|
pub fn into_paths(self) -> AbsoluteFilePathList {
|
||||||
self.path
|
self.paths
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn force(&self) -> bool {
|
pub fn force(&self) -> bool {
|
||||||
@@ -28,10 +28,10 @@ impl RmRequest {
|
|||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum RmError {
|
pub enum RmError {
|
||||||
#[error("The path does not exist")]
|
#[error("At least one file does not exist")]
|
||||||
NotFound,
|
NotFound(AbsoluteFilePath),
|
||||||
#[error("The directory is not empty")]
|
#[error("At least one directory is not empty")]
|
||||||
NotEmpty,
|
NotEmpty(AbsoluteFilePath),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Unknown(#[from] anyhow::Error),
|
Unknown(#[from] anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
pub mod auth_session;
|
pub mod auth_session;
|
||||||
pub mod file;
|
pub mod file;
|
||||||
|
pub mod option;
|
||||||
pub mod share;
|
pub mod share;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod user_warren;
|
pub mod user_warren;
|
||||||
|
|||||||
74
backend/src/lib/domain/warren/models/option/mod.rs
Normal file
74
backend/src/lib/domain/warren/models/option/mod.rs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
mod requests;
|
||||||
|
use derive_more::Display;
|
||||||
|
pub use requests::*;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Display)]
|
||||||
|
pub struct OptionKey(String);
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum OptionKeyError {
|
||||||
|
#[error("An OptionKey must not be empty")]
|
||||||
|
Empty,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OptionKey {
|
||||||
|
pub fn new(raw: &str) -> Result<Self, OptionKeyError> {
|
||||||
|
let raw = raw.trim();
|
||||||
|
|
||||||
|
if raw.is_empty() {
|
||||||
|
return Err(OptionKeyError::Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self(raw.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct OptionValue<T>(T)
|
||||||
|
where
|
||||||
|
T: OptionType;
|
||||||
|
|
||||||
|
impl<T> OptionValue<T>
|
||||||
|
where
|
||||||
|
T: OptionType,
|
||||||
|
{
|
||||||
|
pub fn new(value: T) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inner(&self) -> &T {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_inner(self) -> T {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait OptionType: std::fmt::Debug + Clone + Send + Sync {
|
||||||
|
type Error: std::fmt::Debug;
|
||||||
|
|
||||||
|
fn parse(raw: &str) -> Result<Self, Self::Error>;
|
||||||
|
fn to_string(&self) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OptionType for bool {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn parse(raw: &str) -> Result<Self, Self::Error> {
|
||||||
|
Ok(match raw.to_lowercase().as_str() {
|
||||||
|
"true" => true,
|
||||||
|
"false" => false,
|
||||||
|
_ => anyhow::bail!("Expected 'true' or 'false': {raw}"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
if *self { "true" } else { "false" }.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::domain::warren::models::option::OptionKey;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct DeleteOptionRequest {
|
||||||
|
key: OptionKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeleteOptionRequest {
|
||||||
|
pub fn new(key: OptionKey) -> Self {
|
||||||
|
Self { key }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key(&self) -> &OptionKey {
|
||||||
|
&self.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DeleteOptionRequest> for OptionKey {
|
||||||
|
fn from(value: DeleteOptionRequest) -> Self {
|
||||||
|
value.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct DeleteOptionResponse {
|
||||||
|
key: OptionKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeleteOptionResponse {
|
||||||
|
pub fn new(key: OptionKey) -> Self {
|
||||||
|
Self { key }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key(&self) -> &OptionKey {
|
||||||
|
&self.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum DeleteOptionError {
|
||||||
|
#[error("Could not find option with key: {0}")]
|
||||||
|
NotFound(OptionKey),
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
57
backend/src/lib/domain/warren/models/option/requests/get.rs
Normal file
57
backend/src/lib/domain/warren/models/option/requests/get.rs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::domain::warren::models::option::{OptionKey, OptionType};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct GetOptionRequest {
|
||||||
|
key: OptionKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetOptionRequest {
|
||||||
|
pub fn new(key: OptionKey) -> Self {
|
||||||
|
Self { key }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key(&self) -> &OptionKey {
|
||||||
|
&self.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GetOptionRequest> for OptionKey {
|
||||||
|
fn from(value: GetOptionRequest) -> Self {
|
||||||
|
value.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct GetOptionResponse<T: OptionType> {
|
||||||
|
key: OptionKey,
|
||||||
|
value: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> GetOptionResponse<T>
|
||||||
|
where
|
||||||
|
T: OptionType,
|
||||||
|
{
|
||||||
|
pub fn new(key: OptionKey, value: T) -> Self {
|
||||||
|
Self { key, value }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key(&self) -> &OptionKey {
|
||||||
|
&self.key
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value(&self) -> &T {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum GetOptionError {
|
||||||
|
#[error("Could not find option with key: {0}")]
|
||||||
|
NotFound(OptionKey),
|
||||||
|
#[error("Could not parse the option value with the specified type")]
|
||||||
|
Parse,
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
mod delete;
|
||||||
|
mod get;
|
||||||
|
mod set;
|
||||||
|
pub use delete::*;
|
||||||
|
pub use get::*;
|
||||||
|
pub use set::*;
|
||||||
83
backend/src/lib/domain/warren/models/option/requests/set.rs
Normal file
83
backend/src/lib/domain/warren/models/option/requests/set.rs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::domain::warren::models::option::{OptionKey, OptionType, OptionValue};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct SetOptionRequest<T>
|
||||||
|
where
|
||||||
|
T: OptionType,
|
||||||
|
{
|
||||||
|
key: OptionKey,
|
||||||
|
value: OptionValue<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> SetOptionRequest<T>
|
||||||
|
where
|
||||||
|
T: OptionType,
|
||||||
|
{
|
||||||
|
pub fn new(key: OptionKey, value: T) -> Self {
|
||||||
|
Self {
|
||||||
|
key,
|
||||||
|
value: OptionValue::new(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key(&self) -> &OptionKey {
|
||||||
|
&self.key
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value(&self) -> &OptionValue<T> {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unpack(self) -> (OptionKey, OptionValue<T>) {
|
||||||
|
(self.key, self.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<SetOptionRequest<T>> for OptionKey
|
||||||
|
where
|
||||||
|
T: OptionType,
|
||||||
|
{
|
||||||
|
fn from(value: SetOptionRequest<T>) -> Self {
|
||||||
|
value.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<SetOptionRequest<T>> for OptionValue<T>
|
||||||
|
where
|
||||||
|
T: OptionType,
|
||||||
|
{
|
||||||
|
fn from(value: SetOptionRequest<T>) -> Self {
|
||||||
|
value.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct SetOptionResponse<T: OptionType> {
|
||||||
|
key: OptionKey,
|
||||||
|
value: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> SetOptionResponse<T>
|
||||||
|
where
|
||||||
|
T: OptionType,
|
||||||
|
{
|
||||||
|
pub fn new(key: OptionKey, value: T) -> Self {
|
||||||
|
Self { key, value }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key(&self) -> &OptionKey {
|
||||||
|
&self.key
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value(&self) -> &T {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum SetOptionError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ use thiserror::Error;
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::domain::warren::models::{
|
use crate::domain::warren::models::{
|
||||||
file::{AbsoluteFilePath, CatRequest, FileStream},
|
file::{AbsoluteFilePathList, CatRequest, FileStream},
|
||||||
share::{Share, SharePassword},
|
share::{Share, SharePassword},
|
||||||
warren::{FetchWarrenError, Warren, WarrenCatError, WarrenCatRequest},
|
warren::{FetchWarrenError, Warren, WarrenCatError, WarrenCatRequest},
|
||||||
};
|
};
|
||||||
@@ -12,16 +12,16 @@ use super::{VerifySharePasswordError, VerifySharePasswordRequest};
|
|||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct ShareCatRequest {
|
pub struct ShareCatRequest {
|
||||||
share_id: Uuid,
|
share_id: Uuid,
|
||||||
path: AbsoluteFilePath,
|
base: CatRequest,
|
||||||
password: Option<SharePassword>,
|
password: Option<SharePassword>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShareCatRequest {
|
impl ShareCatRequest {
|
||||||
pub fn new(share_id: Uuid, path: AbsoluteFilePath, password: Option<SharePassword>) -> Self {
|
pub fn new(share_id: Uuid, password: Option<SharePassword>, base: CatRequest) -> Self {
|
||||||
Self {
|
Self {
|
||||||
share_id,
|
share_id,
|
||||||
path,
|
|
||||||
password,
|
password,
|
||||||
|
base,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,18 +29,23 @@ impl ShareCatRequest {
|
|||||||
&self.share_id
|
&self.share_id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path(&self) -> &AbsoluteFilePath {
|
|
||||||
&self.path
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn password(&self) -> Option<&SharePassword> {
|
pub fn password(&self) -> Option<&SharePassword> {
|
||||||
self.password.as_ref()
|
self.password.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_warren_cat_request(self, share: &Share, warren: &Warren) -> WarrenCatRequest {
|
pub fn base(&self) -> &CatRequest {
|
||||||
let path = share.path().clone().join(&self.path.to_relative());
|
&self.base
|
||||||
|
}
|
||||||
|
|
||||||
WarrenCatRequest::new(*warren.id(), CatRequest::new(path))
|
pub fn build_warren_cat_request(self, share: &Share, warren: &Warren) -> WarrenCatRequest {
|
||||||
|
let mut paths = self.base.into_paths();
|
||||||
|
|
||||||
|
paths
|
||||||
|
.paths_mut()
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|path| *path = share.path.clone().join(&path.clone().to_relative()));
|
||||||
|
|
||||||
|
WarrenCatRequest::new(*warren.id(), CatRequest::new(paths))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,20 +55,24 @@ impl From<&ShareCatRequest> for VerifySharePasswordRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ShareCatResponse {
|
pub struct ShareCatResponse {
|
||||||
share: Share,
|
share: Share,
|
||||||
warren: Warren,
|
warren: Warren,
|
||||||
path: AbsoluteFilePath,
|
paths: AbsoluteFilePathList,
|
||||||
stream: FileStream,
|
stream: FileStream,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShareCatResponse {
|
impl ShareCatResponse {
|
||||||
pub fn new(share: Share, warren: Warren, path: AbsoluteFilePath, stream: FileStream) -> Self {
|
pub fn new(
|
||||||
|
share: Share,
|
||||||
|
warren: Warren,
|
||||||
|
paths: AbsoluteFilePathList,
|
||||||
|
stream: FileStream,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
share,
|
share,
|
||||||
warren,
|
warren,
|
||||||
path,
|
paths,
|
||||||
stream,
|
stream,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,8 +85,8 @@ impl ShareCatResponse {
|
|||||||
&self.warren
|
&self.warren
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path(&self) -> &AbsoluteFilePath {
|
pub fn paths(&self) -> &AbsoluteFilePathList {
|
||||||
&self.path
|
&self.paths
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stream(&self) -> &FileStream {
|
pub fn stream(&self) -> &FileStream {
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ pub struct RegisterUserRequest {
|
|||||||
name: UserName,
|
name: UserName,
|
||||||
email: UserEmail,
|
email: UserEmail,
|
||||||
password: UserPassword,
|
password: UserPassword,
|
||||||
|
bypass_registration_flag: bool,
|
||||||
|
admin: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RegisterUserRequest {
|
impl RegisterUserRequest {
|
||||||
@@ -17,6 +19,23 @@ impl RegisterUserRequest {
|
|||||||
name,
|
name,
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
|
bypass_registration_flag: false,
|
||||||
|
admin: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_bypass_flag(
|
||||||
|
name: UserName,
|
||||||
|
email: UserEmail,
|
||||||
|
password: UserPassword,
|
||||||
|
admin: bool,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
bypass_registration_flag: true,
|
||||||
|
admin,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,11 +50,19 @@ impl RegisterUserRequest {
|
|||||||
pub fn password(&self) -> &UserPassword {
|
pub fn password(&self) -> &UserPassword {
|
||||||
&self.password
|
&self.password
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn admin(&self) -> bool {
|
||||||
|
self.admin
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bypass_registration_flag(&self) -> bool {
|
||||||
|
self.bypass_registration_flag
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<RegisterUserRequest> for CreateUserRequest {
|
impl From<RegisterUserRequest> for CreateUserRequest {
|
||||||
fn from(value: RegisterUserRequest) -> Self {
|
fn from(value: RegisterUserRequest) -> Self {
|
||||||
Self::new(value.name, value.email, value.password, false)
|
Self::new(value.name, value.email, value.password, value.admin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -221,12 +221,15 @@ impl WarrenRmRequest {
|
|||||||
|
|
||||||
pub fn build_fs_request(self, warren: &Warren) -> RmRequest {
|
pub fn build_fs_request(self, warren: &Warren) -> RmRequest {
|
||||||
let force = self.base.force();
|
let force = self.base.force();
|
||||||
let path = warren
|
|
||||||
.path()
|
|
||||||
.clone()
|
|
||||||
.join(&self.base.into_path().to_relative());
|
|
||||||
|
|
||||||
RmRequest::new(path, force)
|
let mut paths = self.base.into_paths();
|
||||||
|
|
||||||
|
paths
|
||||||
|
.paths_mut()
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|path| *path = warren.path.clone().join(&path.clone().to_relative()));
|
||||||
|
|
||||||
|
RmRequest::new(paths, force)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,34 +245,30 @@ impl Into<FetchWarrenRequest> for &WarrenRmRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Debug)]
|
||||||
pub struct WarrenRmResponse {
|
pub struct WarrenRmResponse {
|
||||||
warren: Warren,
|
warren: Warren,
|
||||||
path: AbsoluteFilePath,
|
results: Vec<Result<AbsoluteFilePath, RmError>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WarrenRmResponse {
|
impl WarrenRmResponse {
|
||||||
pub fn new(warren: Warren, path: AbsoluteFilePath) -> Self {
|
pub fn new(warren: Warren, results: Vec<Result<AbsoluteFilePath, RmError>>) -> Self {
|
||||||
Self { warren, path }
|
Self { warren, results }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn warren(&self) -> &Warren {
|
pub fn warren(&self) -> &Warren {
|
||||||
&self.warren
|
&self.warren
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path(&self) -> &AbsoluteFilePath {
|
pub fn results(&self) -> &Vec<Result<AbsoluteFilePath, RmError>> {
|
||||||
&self.path
|
&self.results
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum WarrenRmError {
|
pub enum WarrenRmError {
|
||||||
#[error(transparent)]
|
|
||||||
FileSystem(#[from] RmError),
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
FetchWarren(#[from] FetchWarrenError),
|
FetchWarren(#[from] FetchWarrenError),
|
||||||
#[error(transparent)]
|
|
||||||
Unknown(#[from] anyhow::Error),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct WarrenSaveRequest<'s> {
|
pub struct WarrenSaveRequest<'s> {
|
||||||
@@ -423,11 +422,16 @@ impl WarrenMvRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_fs_request(self, warren: &Warren) -> MvRequest {
|
pub fn build_fs_request(self, warren: &Warren) -> MvRequest {
|
||||||
let (base_path, base_target_path) = self.base.unpack();
|
let (mut base_paths, base_target_path) = self.base.unpack();
|
||||||
let path = warren.path().clone().join(&base_path.to_relative());
|
|
||||||
let target_path = warren.path().clone().join(&base_target_path.to_relative());
|
let target_path = warren.path().clone().join(&base_target_path.to_relative());
|
||||||
|
|
||||||
MvRequest::new(path, target_path)
|
base_paths
|
||||||
|
.paths_mut()
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|path| *path = warren.path.clone().join(&path.clone().to_relative()));
|
||||||
|
|
||||||
|
MvRequest::new(base_paths, target_path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,32 +447,26 @@ impl Into<FetchWarrenRequest> for &WarrenMvRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Debug)]
|
||||||
pub struct WarrenMvResponse {
|
pub struct WarrenMvResponse {
|
||||||
warren: Warren,
|
warren: Warren,
|
||||||
old_path: AbsoluteFilePath,
|
results: Vec<Result<(AbsoluteFilePath, AbsoluteFilePath), MvError>>,
|
||||||
path: AbsoluteFilePath,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WarrenMvResponse {
|
impl WarrenMvResponse {
|
||||||
pub fn new(warren: Warren, old_path: AbsoluteFilePath, path: AbsoluteFilePath) -> Self {
|
pub fn new(
|
||||||
Self {
|
warren: Warren,
|
||||||
warren,
|
results: Vec<Result<(AbsoluteFilePath, AbsoluteFilePath), MvError>>,
|
||||||
old_path,
|
) -> Self {
|
||||||
path,
|
Self { warren, results }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn warren(&self) -> &Warren {
|
pub fn warren(&self) -> &Warren {
|
||||||
&self.warren
|
&self.warren
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn old_path(&self) -> &AbsoluteFilePath {
|
pub fn results(&self) -> &Vec<Result<(AbsoluteFilePath, AbsoluteFilePath), MvError>> {
|
||||||
&self.old_path
|
&self.results
|
||||||
}
|
|
||||||
|
|
||||||
pub fn path(&self) -> &AbsoluteFilePath {
|
|
||||||
&self.path
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,8 +475,6 @@ pub enum WarrenMvError {
|
|||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
FetchWarren(#[from] FetchWarrenError),
|
FetchWarren(#[from] FetchWarrenError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
FileSystem(#[from] MvError),
|
|
||||||
#[error(transparent)]
|
|
||||||
Unknown(#[from] anyhow::Error),
|
Unknown(#[from] anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -597,12 +593,14 @@ impl WarrenCatRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_fs_request(self, warren: &Warren) -> CatRequest {
|
pub fn build_fs_request(self, warren: &Warren) -> CatRequest {
|
||||||
let path = warren
|
let mut paths = self.base.into_paths();
|
||||||
.path()
|
|
||||||
.clone()
|
|
||||||
.join(&self.base.into_path().to_relative());
|
|
||||||
|
|
||||||
CatRequest::new(path)
|
paths
|
||||||
|
.paths_mut()
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|path| *path = warren.path.clone().join(&path.clone().to_relative()));
|
||||||
|
|
||||||
|
CatRequest::new(paths)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ pub trait WarrenMetrics: Clone + Send + Sync + 'static {
|
|||||||
|
|
||||||
fn record_warren_share_cat_success(&self) -> impl Future<Output = ()> + Send;
|
fn record_warren_share_cat_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
fn record_warren_share_cat_failure(&self) -> impl Future<Output = ()> + Send;
|
fn record_warren_share_cat_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
fn record_warren_share_password_verification_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
fn record_warren_share_password_verification_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait FileSystemMetrics: Clone + Send + Sync + 'static {
|
pub trait FileSystemMetrics: Clone + Send + Sync + 'static {
|
||||||
@@ -182,3 +185,14 @@ pub trait AuthMetrics: Clone + Send + Sync + 'static {
|
|||||||
fn record_auth_share_deletion_success(&self) -> impl Future<Output = ()> + Send;
|
fn record_auth_share_deletion_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
fn record_auth_share_deletion_failure(&self) -> impl Future<Output = ()> + Send;
|
fn record_auth_share_deletion_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait OptionMetrics: Clone + Send + Sync + 'static {
|
||||||
|
fn record_option_get_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
fn record_option_get_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
fn record_option_set_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
fn record_option_set_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
fn record_option_delete_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
fn record_option_delete_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,15 +15,22 @@ use super::models::{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
file::{
|
file::{
|
||||||
CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream, LsError, LsRequest,
|
AbsoluteFilePath, CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream,
|
||||||
LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError, RmRequest, SaveError,
|
LsError, LsRequest, LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError,
|
||||||
SaveRequest, SaveResponse, StatError, StatRequest, StatResponse, TouchError, TouchRequest,
|
RmRequest, SaveError, SaveRequest, SaveResponse, StatError, StatRequest, StatResponse,
|
||||||
|
TouchError, TouchRequest,
|
||||||
|
},
|
||||||
|
option::{
|
||||||
|
DeleteOptionError, DeleteOptionRequest, DeleteOptionResponse, GetOptionError,
|
||||||
|
GetOptionRequest, GetOptionResponse, OptionType, SetOptionError, SetOptionRequest,
|
||||||
|
SetOptionResponse,
|
||||||
},
|
},
|
||||||
share::{
|
share::{
|
||||||
CreateShareBaseRequest, CreateShareError, CreateShareRequest, CreateShareResponse,
|
CreateShareBaseRequest, CreateShareError, CreateShareRequest, CreateShareResponse,
|
||||||
DeleteShareError, DeleteShareRequest, DeleteShareResponse, GetShareError, GetShareRequest,
|
DeleteShareError, DeleteShareRequest, DeleteShareResponse, GetShareError, GetShareRequest,
|
||||||
GetShareResponse, ListSharesError, ListSharesRequest, ListSharesResponse, ShareCatError,
|
GetShareResponse, ListSharesError, ListSharesRequest, ListSharesResponse, ShareCatError,
|
||||||
ShareCatRequest, ShareCatResponse, ShareLsError, ShareLsRequest, ShareLsResponse,
|
ShareCatRequest, ShareCatResponse, ShareLsError, ShareLsRequest, ShareLsResponse,
|
||||||
|
VerifySharePasswordError, VerifySharePasswordRequest, VerifySharePasswordResponse,
|
||||||
},
|
},
|
||||||
user::{
|
user::{
|
||||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError,
|
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError,
|
||||||
@@ -137,6 +144,10 @@ pub trait WarrenService: Clone + Send + Sync + 'static {
|
|||||||
&self,
|
&self,
|
||||||
request: ShareCatRequest,
|
request: ShareCatRequest,
|
||||||
) -> impl Future<Output = Result<ShareCatResponse, ShareCatError>> + Send;
|
) -> impl Future<Output = Result<ShareCatResponse, ShareCatError>> + Send;
|
||||||
|
fn verify_warren_share_password(
|
||||||
|
&self,
|
||||||
|
request: VerifySharePasswordRequest,
|
||||||
|
) -> impl Future<Output = Result<VerifySharePasswordResponse, VerifySharePasswordError>> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait FileSystemService: Clone + Send + Sync + 'static {
|
pub trait FileSystemService: Clone + Send + Sync + 'static {
|
||||||
@@ -144,8 +155,14 @@ pub trait FileSystemService: Clone + Send + Sync + 'static {
|
|||||||
fn cat(&self, request: CatRequest)
|
fn cat(&self, request: CatRequest)
|
||||||
-> impl Future<Output = Result<FileStream, CatError>> + Send;
|
-> impl Future<Output = Result<FileStream, CatError>> + Send;
|
||||||
fn mkdir(&self, request: MkdirRequest) -> impl Future<Output = Result<(), MkdirError>> + Send;
|
fn mkdir(&self, request: MkdirRequest) -> impl Future<Output = Result<(), MkdirError>> + Send;
|
||||||
fn rm(&self, request: RmRequest) -> impl Future<Output = Result<(), RmError>> + Send;
|
fn rm(
|
||||||
fn mv(&self, request: MvRequest) -> impl Future<Output = Result<(), MvError>> + Send;
|
&self,
|
||||||
|
request: RmRequest,
|
||||||
|
) -> impl Future<Output = Vec<Result<AbsoluteFilePath, RmError>>> + Send;
|
||||||
|
fn mv(
|
||||||
|
&self,
|
||||||
|
request: MvRequest,
|
||||||
|
) -> impl Future<Output = Vec<Result<(AbsoluteFilePath, AbsoluteFilePath), MvError>>> + Send;
|
||||||
fn save(
|
fn save(
|
||||||
&self,
|
&self,
|
||||||
request: SaveRequest,
|
request: SaveRequest,
|
||||||
@@ -325,3 +342,18 @@ pub trait AuthService: Clone + Send + Sync + 'static {
|
|||||||
warren_service: &WS,
|
warren_service: &WS,
|
||||||
) -> impl Future<Output = Result<DeleteShareResponse, AuthError<DeleteShareError>>> + Send;
|
) -> impl Future<Output = Result<DeleteShareResponse, AuthError<DeleteShareError>>> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait OptionService: Clone + Send + Sync + 'static {
|
||||||
|
fn get_option<T: OptionType>(
|
||||||
|
&self,
|
||||||
|
request: GetOptionRequest,
|
||||||
|
) -> impl Future<Output = Result<GetOptionResponse<T>, GetOptionError>> + Send;
|
||||||
|
fn set_option<T: OptionType>(
|
||||||
|
&self,
|
||||||
|
request: SetOptionRequest<T>,
|
||||||
|
) -> impl Future<Output = Result<SetOptionResponse<T>, SetOptionError>> + Send;
|
||||||
|
fn delete_option(
|
||||||
|
&self,
|
||||||
|
request: DeleteOptionRequest,
|
||||||
|
) -> impl Future<Output = Result<DeleteOptionResponse, DeleteOptionError>> + Send;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use crate::domain::warren::models::{
|
use crate::domain::warren::models::{
|
||||||
auth_session::requests::FetchAuthSessionResponse,
|
auth_session::requests::FetchAuthSessionResponse,
|
||||||
file::{AbsoluteFilePath, LsResponse},
|
file::{AbsoluteFilePath, AbsoluteFilePathList, LsResponse},
|
||||||
|
option::{DeleteOptionResponse, GetOptionResponse, OptionType, SetOptionResponse},
|
||||||
share::{
|
share::{
|
||||||
CreateShareResponse, DeleteShareResponse, GetShareResponse, ListSharesResponse,
|
CreateShareResponse, DeleteShareResponse, GetShareResponse, ListSharesResponse,
|
||||||
ShareCatResponse, ShareLsResponse,
|
ShareCatResponse, ShareLsResponse, VerifySharePasswordResponse,
|
||||||
},
|
},
|
||||||
user::{ListAllUsersAndWarrensResponse, LoginUserOidcResponse, LoginUserResponse, User},
|
user::{ListAllUsersAndWarrensResponse, LoginUserOidcResponse, LoginUserResponse, User},
|
||||||
user_warren::UserWarren,
|
user_warren::UserWarren,
|
||||||
@@ -28,7 +29,7 @@ pub trait WarrenNotifier: Clone + Send + Sync + 'static {
|
|||||||
fn warren_cat(
|
fn warren_cat(
|
||||||
&self,
|
&self,
|
||||||
warren: &Warren,
|
warren: &Warren,
|
||||||
path: &AbsoluteFilePath,
|
path: &AbsoluteFilePathList,
|
||||||
) -> impl Future<Output = ()> + Send;
|
) -> impl Future<Output = ()> + Send;
|
||||||
fn warren_mkdir(&self, response: &WarrenMkdirResponse) -> impl Future<Output = ()> + Send;
|
fn warren_mkdir(&self, response: &WarrenMkdirResponse) -> impl Future<Output = ()> + Send;
|
||||||
fn warren_rm(&self, response: &WarrenRmResponse) -> impl Future<Output = ()> + Send;
|
fn warren_rm(&self, response: &WarrenRmResponse) -> impl Future<Output = ()> + Send;
|
||||||
@@ -64,11 +65,15 @@ pub trait WarrenNotifier: Clone + Send + Sync + 'static {
|
|||||||
) -> impl Future<Output = ()> + Send;
|
) -> impl Future<Output = ()> + Send;
|
||||||
fn warren_share_ls(&self, response: &ShareLsResponse) -> impl Future<Output = ()> + Send;
|
fn warren_share_ls(&self, response: &ShareLsResponse) -> impl Future<Output = ()> + Send;
|
||||||
fn warren_share_cat(&self, response: &ShareCatResponse) -> impl Future<Output = ()> + Send;
|
fn warren_share_cat(&self, response: &ShareCatResponse) -> impl Future<Output = ()> + Send;
|
||||||
|
fn warren_share_password_verified(
|
||||||
|
&self,
|
||||||
|
response: &VerifySharePasswordResponse,
|
||||||
|
) -> impl Future<Output = ()> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait FileSystemNotifier: Clone + Send + Sync + 'static {
|
pub trait FileSystemNotifier: Clone + Send + Sync + 'static {
|
||||||
fn ls(&self, response: &LsResponse) -> impl Future<Output = ()> + Send;
|
fn ls(&self, response: &LsResponse) -> impl Future<Output = ()> + Send;
|
||||||
fn cat(&self, path: &AbsoluteFilePath) -> impl Future<Output = ()> + Send;
|
fn cat(&self, paths: &AbsoluteFilePathList) -> impl Future<Output = ()> + Send;
|
||||||
fn mkdir(&self, path: &AbsoluteFilePath) -> impl Future<Output = ()> + Send;
|
fn mkdir(&self, path: &AbsoluteFilePath) -> impl Future<Output = ()> + Send;
|
||||||
fn rm(&self, path: &AbsoluteFilePath) -> impl Future<Output = ()> + Send;
|
fn rm(&self, path: &AbsoluteFilePath) -> impl Future<Output = ()> + Send;
|
||||||
fn mv(
|
fn mv(
|
||||||
@@ -163,7 +168,7 @@ pub trait AuthNotifier: Clone + Send + Sync + 'static {
|
|||||||
&self,
|
&self,
|
||||||
user: &User,
|
user: &User,
|
||||||
warren_id: &Uuid,
|
warren_id: &Uuid,
|
||||||
path: &AbsoluteFilePath,
|
paths: &AbsoluteFilePathList,
|
||||||
) -> impl Future<Output = ()> + Send;
|
) -> impl Future<Output = ()> + Send;
|
||||||
fn auth_warren_mkdir(
|
fn auth_warren_mkdir(
|
||||||
&self,
|
&self,
|
||||||
@@ -215,3 +220,15 @@ pub trait AuthNotifier: Clone + Send + Sync + 'static {
|
|||||||
response: &DeleteShareResponse,
|
response: &DeleteShareResponse,
|
||||||
) -> impl Future<Output = ()> + Send;
|
) -> impl Future<Output = ()> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait OptionNotifier: Clone + Send + Sync + 'static {
|
||||||
|
fn got_option<T: OptionType>(
|
||||||
|
&self,
|
||||||
|
response: &GetOptionResponse<T>,
|
||||||
|
) -> impl Future<Output = ()> + Send;
|
||||||
|
fn set_option<T: OptionType>(
|
||||||
|
&self,
|
||||||
|
response: &SetOptionResponse<T>,
|
||||||
|
) -> impl Future<Output = ()> + Send;
|
||||||
|
fn deleted_option(&self, response: &DeleteOptionResponse) -> impl Future<Output = ()> + Send;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,9 +7,15 @@ use crate::domain::warren::models::{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
file::{
|
file::{
|
||||||
CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream, LsError, LsRequest,
|
AbsoluteFilePath, CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream,
|
||||||
LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError, RmRequest, SaveError,
|
LsError, LsRequest, LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError,
|
||||||
SaveRequest, SaveResponse, StatError, StatRequest, StatResponse, TouchError, TouchRequest,
|
RmRequest, SaveError, SaveRequest, SaveResponse, StatError, StatRequest, StatResponse,
|
||||||
|
TouchError, TouchRequest,
|
||||||
|
},
|
||||||
|
option::{
|
||||||
|
DeleteOptionError, DeleteOptionRequest, DeleteOptionResponse, GetOptionError,
|
||||||
|
GetOptionRequest, GetOptionResponse, OptionType, SetOptionError, SetOptionRequest,
|
||||||
|
SetOptionResponse,
|
||||||
},
|
},
|
||||||
share::{
|
share::{
|
||||||
CreateShareError, CreateShareRequest, CreateShareResponse, DeleteShareError,
|
CreateShareError, CreateShareRequest, CreateShareResponse, DeleteShareError,
|
||||||
@@ -97,8 +103,14 @@ pub trait FileSystemRepository: Clone + Send + Sync + 'static {
|
|||||||
fn cat(&self, request: CatRequest)
|
fn cat(&self, request: CatRequest)
|
||||||
-> impl Future<Output = Result<FileStream, CatError>> + Send;
|
-> impl Future<Output = Result<FileStream, CatError>> + Send;
|
||||||
fn mkdir(&self, request: MkdirRequest) -> impl Future<Output = Result<(), MkdirError>> + Send;
|
fn mkdir(&self, request: MkdirRequest) -> impl Future<Output = Result<(), MkdirError>> + Send;
|
||||||
fn rm(&self, request: RmRequest) -> impl Future<Output = Result<(), RmError>> + Send;
|
fn rm(
|
||||||
fn mv(&self, request: MvRequest) -> impl Future<Output = Result<(), MvError>> + Send;
|
&self,
|
||||||
|
request: RmRequest,
|
||||||
|
) -> impl Future<Output = Vec<Result<AbsoluteFilePath, RmError>>> + Send;
|
||||||
|
fn mv(
|
||||||
|
&self,
|
||||||
|
request: MvRequest,
|
||||||
|
) -> impl Future<Output = Vec<Result<(AbsoluteFilePath, AbsoluteFilePath), MvError>>> + Send;
|
||||||
fn save(
|
fn save(
|
||||||
&self,
|
&self,
|
||||||
request: SaveRequest,
|
request: SaveRequest,
|
||||||
@@ -181,3 +193,18 @@ pub trait AuthRepository: Clone + Send + Sync + 'static {
|
|||||||
request: FetchUserWarrenRequest,
|
request: FetchUserWarrenRequest,
|
||||||
) -> impl Future<Output = Result<UserWarren, FetchUserWarrenError>> + Send;
|
) -> impl Future<Output = Result<UserWarren, FetchUserWarrenError>> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait OptionRepository: Clone + Send + Sync + 'static {
|
||||||
|
fn get_option<T: OptionType>(
|
||||||
|
&self,
|
||||||
|
request: GetOptionRequest,
|
||||||
|
) -> impl Future<Output = Result<GetOptionResponse<T>, GetOptionError>> + Send;
|
||||||
|
fn set_option<T: OptionType>(
|
||||||
|
&self,
|
||||||
|
request: SetOptionRequest<T>,
|
||||||
|
) -> impl Future<Output = Result<SetOptionResponse<T>, SetOptionError>> + Send;
|
||||||
|
fn delete_option(
|
||||||
|
&self,
|
||||||
|
request: DeleteOptionRequest,
|
||||||
|
) -> impl Future<Output = Result<DeleteOptionResponse, DeleteOptionError>> + Send;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
file::FileStream,
|
file::FileStream,
|
||||||
|
option::{GetOptionError, GetOptionRequest, OptionKey, SetOptionRequest},
|
||||||
share::{
|
share::{
|
||||||
CreateShareBaseRequest, CreateShareError, CreateShareResponse,
|
CreateShareBaseRequest, CreateShareError, CreateShareResponse,
|
||||||
DeleteShareError, DeleteShareRequest, DeleteShareResponse, ListSharesError,
|
DeleteShareError, DeleteShareRequest, DeleteShareResponse, ListSharesError,
|
||||||
@@ -24,7 +25,7 @@ use crate::{
|
|||||||
ListAllUsersAndWarrensRequest, ListAllUsersAndWarrensResponse, ListUsersError,
|
ListAllUsersAndWarrensRequest, ListAllUsersAndWarrensResponse, ListUsersError,
|
||||||
ListUsersRequest, LoginUserError, LoginUserOidcError, LoginUserOidcRequest,
|
ListUsersRequest, LoginUserError, LoginUserOidcError, LoginUserOidcRequest,
|
||||||
LoginUserOidcResponse, LoginUserRequest, LoginUserResponse, RegisterUserError,
|
LoginUserOidcResponse, LoginUserRequest, LoginUserResponse, RegisterUserError,
|
||||||
RegisterUserRequest, User,
|
RegisterUserRequest, User, UserEmail, UserName, UserPassword,
|
||||||
},
|
},
|
||||||
user_warren::{
|
user_warren::{
|
||||||
UserWarren,
|
UserWarren,
|
||||||
@@ -46,7 +47,10 @@ use crate::{
|
|||||||
WarrenTouchResponse,
|
WarrenTouchResponse,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ports::{AuthMetrics, AuthNotifier, AuthRepository, AuthService, WarrenService},
|
ports::{
|
||||||
|
AuthMetrics, AuthNotifier, AuthRepository, AuthService, OptionService,
|
||||||
|
WarrenService,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -97,50 +101,100 @@ impl AuthConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Service<R, M, N, OIDC>
|
pub struct Service<R, M, N, OIDC, O>
|
||||||
where
|
where
|
||||||
R: AuthRepository,
|
R: AuthRepository,
|
||||||
M: AuthMetrics,
|
M: AuthMetrics,
|
||||||
N: AuthNotifier,
|
N: AuthNotifier,
|
||||||
OIDC: OidcService,
|
OIDC: OidcService,
|
||||||
|
O: OptionService,
|
||||||
{
|
{
|
||||||
repository: R,
|
repository: R,
|
||||||
metrics: M,
|
metrics: M,
|
||||||
notifier: N,
|
notifier: N,
|
||||||
oidc: Option<OIDC>,
|
oidc: Option<OIDC>,
|
||||||
|
option_service: O,
|
||||||
config: AuthConfig,
|
config: AuthConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R, M, N, OIDC> Service<R, M, N, OIDC>
|
impl<R, M, N, OIDC, O> Service<R, M, N, OIDC, O>
|
||||||
where
|
where
|
||||||
R: AuthRepository,
|
R: AuthRepository,
|
||||||
M: AuthMetrics,
|
M: AuthMetrics,
|
||||||
N: AuthNotifier,
|
N: AuthNotifier,
|
||||||
OIDC: OidcService,
|
OIDC: OidcService,
|
||||||
|
O: OptionService,
|
||||||
{
|
{
|
||||||
pub fn new(
|
pub async fn new(
|
||||||
repository: R,
|
repository: R,
|
||||||
metrics: M,
|
metrics: M,
|
||||||
notifier: N,
|
notifier: N,
|
||||||
config: AuthConfig,
|
config: AuthConfig,
|
||||||
oidc: Option<OIDC>,
|
oidc: Option<OIDC>,
|
||||||
) -> Self {
|
option_service: O,
|
||||||
Self {
|
) -> anyhow::Result<Self> {
|
||||||
|
let service = Self {
|
||||||
repository,
|
repository,
|
||||||
metrics,
|
metrics,
|
||||||
notifier,
|
notifier,
|
||||||
config,
|
config,
|
||||||
oidc,
|
oidc,
|
||||||
|
option_service,
|
||||||
|
};
|
||||||
|
|
||||||
|
service.init().await?;
|
||||||
|
|
||||||
|
Ok(service)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn oidc(&self) -> Option<&OIDC> {
|
||||||
|
self.oidc.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init(&self) -> anyhow::Result<()> {
|
||||||
|
self.create_admin_user_if_init().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_admin_user_if_init(&self) -> anyhow::Result<()> {
|
||||||
|
const CREATED_ADMIN_USER_KEY: &str = "CREATED_ADMIN_USER";
|
||||||
|
|
||||||
|
let key = OptionKey::new(CREATED_ADMIN_USER_KEY)?;
|
||||||
|
let request = GetOptionRequest::new(key.clone());
|
||||||
|
match self.option_service.get_option::<bool>(request).await {
|
||||||
|
// If the option is already set and true we don't have to do anything anymore
|
||||||
|
Ok(opt) if *opt.value() => return Ok(()),
|
||||||
|
Err(e) => match e {
|
||||||
|
// The option is not yet set so we proceed with the admin user creation
|
||||||
|
GetOptionError::NotFound(_) => (),
|
||||||
|
_ => return Err(e.into()),
|
||||||
|
},
|
||||||
|
// The option was set but it was false so we proceed with the admin user creation
|
||||||
|
_ => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let name = UserName::new("admin")?;
|
||||||
|
let email = UserEmail::new("admin@example.com")?;
|
||||||
|
let password = UserPassword::new("admin1234567")?;
|
||||||
|
let request = RegisterUserRequest::new_bypass_flag(name, email, password, true);
|
||||||
|
|
||||||
|
self.register_user(request).await?;
|
||||||
|
self.option_service
|
||||||
|
.set_option(SetOptionRequest::new(key, true))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R, M, N, OIDC> AuthService for Service<R, M, N, OIDC>
|
impl<R, M, N, OIDC, O> AuthService for Service<R, M, N, OIDC, O>
|
||||||
where
|
where
|
||||||
R: AuthRepository,
|
R: AuthRepository,
|
||||||
M: AuthMetrics,
|
M: AuthMetrics,
|
||||||
N: AuthNotifier,
|
N: AuthNotifier,
|
||||||
OIDC: OidcService,
|
OIDC: OidcService,
|
||||||
|
O: OptionService,
|
||||||
{
|
{
|
||||||
async fn create_warren<WS: WarrenService>(
|
async fn create_warren<WS: WarrenService>(
|
||||||
&self,
|
&self,
|
||||||
@@ -240,7 +294,7 @@ where
|
|||||||
&self,
|
&self,
|
||||||
request: GetOidcRedirectRequest,
|
request: GetOidcRedirectRequest,
|
||||||
) -> Result<GetOidcRedirectResponse, GetOidcRedirectError> {
|
) -> Result<GetOidcRedirectResponse, GetOidcRedirectError> {
|
||||||
let oidc = self.oidc.as_ref().ok_or(GetOidcRedirectError::Disabled)?;
|
let oidc = self.oidc().ok_or(GetOidcRedirectError::Disabled)?;
|
||||||
|
|
||||||
oidc.get_redirect(request.into())
|
oidc.get_redirect(request.into())
|
||||||
.await
|
.await
|
||||||
@@ -249,7 +303,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn register_user(&self, request: RegisterUserRequest) -> Result<User, RegisterUserError> {
|
async fn register_user(&self, request: RegisterUserRequest) -> Result<User, RegisterUserError> {
|
||||||
if !self.config.allow_registration {
|
if !self.config.allow_registration && !request.bypass_registration_flag() {
|
||||||
self.metrics.record_user_registration_failure().await;
|
self.metrics.record_user_registration_failure().await;
|
||||||
return Err(RegisterUserError::Disabled);
|
return Err(RegisterUserError::Disabled);
|
||||||
}
|
}
|
||||||
@@ -298,7 +352,7 @@ where
|
|||||||
&self,
|
&self,
|
||||||
request: LoginUserOidcRequest,
|
request: LoginUserOidcRequest,
|
||||||
) -> Result<LoginUserOidcResponse, LoginUserOidcError> {
|
) -> Result<LoginUserOidcResponse, LoginUserOidcError> {
|
||||||
let oidc = self.oidc.as_ref().ok_or(LoginUserOidcError::Disabled)?;
|
let oidc = self.oidc().ok_or(LoginUserOidcError::Disabled)?;
|
||||||
|
|
||||||
let user_info = oidc.get_user_info(request.into()).await?;
|
let user_info = oidc.get_user_info(request.into()).await?;
|
||||||
|
|
||||||
@@ -704,7 +758,7 @@ where
|
|||||||
return Err(AuthError::InsufficientPermissions);
|
return Err(AuthError::InsufficientPermissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = request.base().path().clone();
|
let paths = request.base().paths().clone();
|
||||||
|
|
||||||
let result = warren_service
|
let result = warren_service
|
||||||
.warren_cat(request)
|
.warren_cat(request)
|
||||||
@@ -714,7 +768,7 @@ where
|
|||||||
if let Ok(_stream) = result.as_ref() {
|
if let Ok(_stream) = result.as_ref() {
|
||||||
self.metrics.record_auth_warren_cat_success().await;
|
self.metrics.record_auth_warren_cat_success().await;
|
||||||
self.notifier
|
self.notifier
|
||||||
.auth_warren_cat(&user, user_warren.warren_id(), &path)
|
.auth_warren_cat(&user, user_warren.warren_id(), &paths)
|
||||||
.await;
|
.await;
|
||||||
} else {
|
} else {
|
||||||
self.metrics.record_auth_warren_cat_failure().await;
|
self.metrics.record_auth_warren_cat_failure().await;
|
||||||
@@ -968,12 +1022,13 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R, M, N, OIDC> Service<R, M, N, OIDC>
|
impl<R, M, N, OIDC, O> Service<R, M, N, OIDC, O>
|
||||||
where
|
where
|
||||||
R: AuthRepository,
|
R: AuthRepository,
|
||||||
M: AuthMetrics,
|
M: AuthMetrics,
|
||||||
N: AuthNotifier,
|
N: AuthNotifier,
|
||||||
OIDC: OidcService,
|
OIDC: OidcService,
|
||||||
|
O: OptionService,
|
||||||
{
|
{
|
||||||
/// A helper to get a [UserWarren], [User] and the underlying request from an [AuthRequest]
|
/// A helper to get a [UserWarren], [User] and the underlying request from an [AuthRequest]
|
||||||
async fn get_session_data_and_user_warren<T, E>(
|
async fn get_session_data_and_user_warren<T, E>(
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use crate::domain::warren::{
|
use crate::domain::warren::{
|
||||||
models::file::{
|
models::file::{
|
||||||
CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream, LsError, LsRequest,
|
AbsoluteFilePath, CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream,
|
||||||
LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError, RmRequest, SaveError,
|
LsError, LsRequest, LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError,
|
||||||
SaveRequest, SaveResponse, StatError, StatRequest, StatResponse, TouchError, TouchRequest,
|
RmRequest, SaveError, SaveRequest, SaveResponse, StatError, StatRequest, StatResponse,
|
||||||
|
TouchError, TouchRequest,
|
||||||
},
|
},
|
||||||
ports::{FileSystemMetrics, FileSystemNotifier, FileSystemRepository, FileSystemService},
|
ports::{FileSystemMetrics, FileSystemNotifier, FileSystemRepository, FileSystemService},
|
||||||
};
|
};
|
||||||
@@ -54,12 +55,12 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn cat(&self, request: CatRequest) -> Result<FileStream, CatError> {
|
async fn cat(&self, request: CatRequest) -> Result<FileStream, CatError> {
|
||||||
let path = request.path().clone();
|
let paths = request.paths().clone();
|
||||||
let result = self.repository.cat(request).await;
|
let result = self.repository.cat(request).await;
|
||||||
|
|
||||||
if result.is_ok() {
|
if result.is_ok() {
|
||||||
self.metrics.record_cat_success().await;
|
self.metrics.record_cat_success().await;
|
||||||
self.notifier.cat(&path).await;
|
self.notifier.cat(&paths).await;
|
||||||
} else {
|
} else {
|
||||||
self.metrics.record_cat_failure().await;
|
self.metrics.record_cat_failure().await;
|
||||||
}
|
}
|
||||||
@@ -81,33 +82,37 @@ where
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn rm(&self, request: RmRequest) -> Result<(), RmError> {
|
async fn rm(&self, request: RmRequest) -> Vec<Result<AbsoluteFilePath, RmError>> {
|
||||||
let path = request.path().clone();
|
let results = self.repository.rm(request).await;
|
||||||
let result = self.repository.rm(request).await;
|
|
||||||
|
|
||||||
if result.is_ok() {
|
for result in results.iter() {
|
||||||
self.metrics.record_rm_success().await;
|
if let Ok(path) = result.as_ref() {
|
||||||
self.notifier.rm(&path).await;
|
self.metrics.record_rm_success().await;
|
||||||
} else {
|
self.notifier.rm(path).await;
|
||||||
self.metrics.record_rm_failure().await;
|
} else {
|
||||||
|
self.metrics.record_rm_failure().await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
results
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn mv(&self, request: MvRequest) -> Result<(), MvError> {
|
async fn mv(
|
||||||
let old_path = request.path().clone();
|
&self,
|
||||||
let new_path = request.target_path().clone();
|
request: MvRequest,
|
||||||
let result = self.repository.mv(request).await;
|
) -> Vec<Result<(AbsoluteFilePath, AbsoluteFilePath), MvError>> {
|
||||||
|
let results = self.repository.mv(request).await;
|
||||||
|
|
||||||
if result.is_ok() {
|
for result in results.iter() {
|
||||||
self.metrics.record_mv_success().await;
|
if let Ok((old_path, new_path)) = result.as_ref() {
|
||||||
self.notifier.mv(&old_path, &new_path).await;
|
self.metrics.record_mv_success().await;
|
||||||
} else {
|
self.notifier.mv(old_path, new_path).await;
|
||||||
self.metrics.record_mv_failure().await;
|
} else {
|
||||||
|
self.metrics.record_mv_failure().await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
results
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn save(&self, request: SaveRequest<'_>) -> Result<SaveResponse, SaveError> {
|
async fn save(&self, request: SaveRequest<'_>) -> Result<SaveResponse, SaveError> {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod file_system;
|
pub mod file_system;
|
||||||
|
pub mod option;
|
||||||
pub mod warren;
|
pub mod warren;
|
||||||
|
|||||||
90
backend/src/lib/domain/warren/service/option.rs
Normal file
90
backend/src/lib/domain/warren/service/option.rs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
use crate::domain::warren::{
|
||||||
|
models::option::{
|
||||||
|
DeleteOptionError, DeleteOptionRequest, DeleteOptionResponse, GetOptionError,
|
||||||
|
GetOptionRequest, GetOptionResponse, OptionType, SetOptionError, SetOptionRequest,
|
||||||
|
SetOptionResponse,
|
||||||
|
},
|
||||||
|
ports::{OptionMetrics, OptionNotifier, OptionRepository, OptionService},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Service<R, M, N>
|
||||||
|
where
|
||||||
|
R: OptionRepository,
|
||||||
|
M: OptionMetrics,
|
||||||
|
N: OptionNotifier,
|
||||||
|
{
|
||||||
|
repository: R,
|
||||||
|
metrics: M,
|
||||||
|
notifier: N,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R, M, N> Service<R, M, N>
|
||||||
|
where
|
||||||
|
R: OptionRepository,
|
||||||
|
M: OptionMetrics,
|
||||||
|
N: OptionNotifier,
|
||||||
|
{
|
||||||
|
pub fn new(repository: R, metrics: M, notifier: N) -> Self {
|
||||||
|
Self {
|
||||||
|
repository,
|
||||||
|
metrics,
|
||||||
|
notifier,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R, M, N> OptionService for Service<R, M, N>
|
||||||
|
where
|
||||||
|
R: OptionRepository,
|
||||||
|
M: OptionMetrics,
|
||||||
|
N: OptionNotifier,
|
||||||
|
{
|
||||||
|
async fn get_option<T: OptionType>(
|
||||||
|
&self,
|
||||||
|
request: GetOptionRequest,
|
||||||
|
) -> Result<GetOptionResponse<T>, GetOptionError> {
|
||||||
|
let result = self.repository.get_option(request).await;
|
||||||
|
|
||||||
|
if let Ok(response) = result.as_ref() {
|
||||||
|
self.metrics.record_option_get_success().await;
|
||||||
|
self.notifier.got_option(response).await;
|
||||||
|
} else {
|
||||||
|
self.metrics.record_option_get_failure().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_option<T: OptionType>(
|
||||||
|
&self,
|
||||||
|
request: SetOptionRequest<T>,
|
||||||
|
) -> Result<SetOptionResponse<T>, SetOptionError> {
|
||||||
|
let result = self.repository.set_option(request).await;
|
||||||
|
|
||||||
|
if let Ok(response) = result.as_ref() {
|
||||||
|
self.metrics.record_option_set_success().await;
|
||||||
|
self.notifier.set_option(response).await;
|
||||||
|
} else {
|
||||||
|
self.metrics.record_option_set_failure().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_option(
|
||||||
|
&self,
|
||||||
|
request: DeleteOptionRequest,
|
||||||
|
) -> Result<DeleteOptionResponse, DeleteOptionError> {
|
||||||
|
let result = self.repository.delete_option(request).await;
|
||||||
|
|
||||||
|
if let Ok(response) = result.as_ref() {
|
||||||
|
self.metrics.record_option_delete_success().await;
|
||||||
|
self.notifier.deleted_option(response).await;
|
||||||
|
} else {
|
||||||
|
self.metrics.record_option_delete_failure().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,8 @@ use crate::domain::warren::{
|
|||||||
DeleteShareRequest, DeleteShareResponse, GetShareError, GetShareRequest,
|
DeleteShareRequest, DeleteShareResponse, GetShareError, GetShareRequest,
|
||||||
GetShareResponse, ListSharesError, ListSharesRequest, ListSharesResponse, Share,
|
GetShareResponse, ListSharesError, ListSharesRequest, ListSharesResponse, Share,
|
||||||
ShareCatError, ShareCatRequest, ShareCatResponse, ShareLsError, ShareLsRequest,
|
ShareCatError, ShareCatRequest, ShareCatResponse, ShareLsError, ShareLsRequest,
|
||||||
ShareLsResponse,
|
ShareLsResponse, VerifySharePasswordError, VerifySharePasswordRequest,
|
||||||
|
VerifySharePasswordResponse,
|
||||||
},
|
},
|
||||||
warren::{
|
warren::{
|
||||||
CreateWarrenError, CreateWarrenRequest, DeleteWarrenError, DeleteWarrenRequest,
|
CreateWarrenError, CreateWarrenRequest, DeleteWarrenError, DeleteWarrenRequest,
|
||||||
@@ -157,14 +158,14 @@ where
|
|||||||
async fn warren_cat(&self, request: WarrenCatRequest) -> Result<FileStream, WarrenCatError> {
|
async fn warren_cat(&self, request: WarrenCatRequest) -> Result<FileStream, WarrenCatError> {
|
||||||
let warren = self.repository.fetch_warren((&request).into()).await?;
|
let warren = self.repository.fetch_warren((&request).into()).await?;
|
||||||
|
|
||||||
let path = request.base().path().clone();
|
let paths = request.base().paths().clone();
|
||||||
let cat_request = request.build_fs_request(&warren);
|
let cat_request = request.build_fs_request(&warren);
|
||||||
|
|
||||||
let result = self.fs_service.cat(cat_request).await.map_err(Into::into);
|
let result = self.fs_service.cat(cat_request).await.map_err(Into::into);
|
||||||
|
|
||||||
if result.is_ok() {
|
if result.is_ok() {
|
||||||
self.metrics.record_warren_cat_success().await;
|
self.metrics.record_warren_cat_success().await;
|
||||||
self.notifier.warren_cat(&warren, &path).await;
|
self.notifier.warren_cat(&warren, &paths).await;
|
||||||
} else {
|
} else {
|
||||||
self.metrics.record_warren_cat_failure().await;
|
self.metrics.record_warren_cat_failure().await;
|
||||||
}
|
}
|
||||||
@@ -250,49 +251,40 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn warren_rm(&self, request: WarrenRmRequest) -> Result<WarrenRmResponse, WarrenRmError> {
|
async fn warren_rm(&self, request: WarrenRmRequest) -> Result<WarrenRmResponse, WarrenRmError> {
|
||||||
let warren = self.repository.fetch_warren((&request).into()).await?;
|
let warren = match self.repository.fetch_warren((&request).into()).await {
|
||||||
|
Ok(warren) => warren,
|
||||||
|
Err(e) => {
|
||||||
|
self.metrics.record_warren_rm_failure().await;
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let path = request.base().path().clone();
|
|
||||||
let rm_request = request.build_fs_request(&warren);
|
let rm_request = request.build_fs_request(&warren);
|
||||||
|
|
||||||
let result = self
|
let response = WarrenRmResponse::new(warren, self.fs_service.rm(rm_request).await);
|
||||||
.fs_service
|
|
||||||
.rm(rm_request)
|
|
||||||
.await
|
|
||||||
.map(|_| WarrenRmResponse::new(warren, path))
|
|
||||||
.map_err(Into::into);
|
|
||||||
|
|
||||||
if let Ok(response) = result.as_ref() {
|
self.metrics.record_warren_rm_success().await;
|
||||||
self.metrics.record_warren_rm_success().await;
|
self.notifier.warren_rm(&response).await;
|
||||||
self.notifier.warren_rm(response).await;
|
|
||||||
} else {
|
|
||||||
self.metrics.record_warren_rm_failure().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn warren_mv(&self, request: WarrenMvRequest) -> Result<WarrenMvResponse, WarrenMvError> {
|
async fn warren_mv(&self, request: WarrenMvRequest) -> Result<WarrenMvResponse, WarrenMvError> {
|
||||||
let warren = self.repository.fetch_warren((&request).into()).await?;
|
let warren = match self.repository.fetch_warren((&request).into()).await {
|
||||||
|
Ok(warren) => warren,
|
||||||
|
Err(e) => {
|
||||||
|
self.metrics.record_warren_mv_failure().await;
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let old_path = request.base().path().clone();
|
|
||||||
let new_path = request.base().target_path().clone();
|
|
||||||
let mv_request = request.build_fs_request(&warren);
|
let mv_request = request.build_fs_request(&warren);
|
||||||
let result = self
|
let response = WarrenMvResponse::new(warren, self.fs_service.mv(mv_request).await);
|
||||||
.fs_service
|
|
||||||
.mv(mv_request)
|
|
||||||
.await
|
|
||||||
.map(|_| WarrenMvResponse::new(warren, old_path, new_path))
|
|
||||||
.map_err(Into::into);
|
|
||||||
|
|
||||||
if let Ok(response) = result.as_ref() {
|
self.metrics.record_warren_mv_success().await;
|
||||||
self.metrics.record_warren_mv_success().await;
|
self.notifier.warren_mv(&response).await;
|
||||||
self.notifier.warren_mv(response).await;
|
|
||||||
} else {
|
|
||||||
self.metrics.record_warren_mv_failure().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn warren_touch(
|
async fn warren_touch(
|
||||||
@@ -517,7 +509,7 @@ where
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let path = request.path().clone();
|
let paths = request.base().paths().clone();
|
||||||
|
|
||||||
let stream = match self
|
let stream = match self
|
||||||
.warren_cat(request.build_warren_cat_request(&share, &warren))
|
.warren_cat(request.build_warren_cat_request(&share, &warren))
|
||||||
@@ -530,11 +522,31 @@ where
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = ShareCatResponse::new(share, warren, path, stream);
|
let response = ShareCatResponse::new(share, warren, paths, stream);
|
||||||
|
|
||||||
self.metrics.record_warren_share_cat_success().await;
|
self.metrics.record_warren_share_cat_success().await;
|
||||||
self.notifier.warren_share_cat(&response).await;
|
self.notifier.warren_share_cat(&response).await;
|
||||||
|
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn verify_warren_share_password(
|
||||||
|
&self,
|
||||||
|
request: VerifySharePasswordRequest,
|
||||||
|
) -> Result<VerifySharePasswordResponse, VerifySharePasswordError> {
|
||||||
|
let result = self.repository.verify_warren_share_password(request).await;
|
||||||
|
|
||||||
|
if let Ok(response) = result.as_ref() {
|
||||||
|
self.metrics
|
||||||
|
.record_warren_share_password_verification_success()
|
||||||
|
.await;
|
||||||
|
self.notifier.warren_share_password_verified(response).await;
|
||||||
|
} else {
|
||||||
|
self.metrics
|
||||||
|
.record_warren_share_password_verification_failure()
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,8 +41,12 @@ impl From<WarrenMkdirError> for ApiError {
|
|||||||
impl From<RmError> for ApiError {
|
impl From<RmError> for ApiError {
|
||||||
fn from(value: RmError) -> Self {
|
fn from(value: RmError) -> Self {
|
||||||
match value {
|
match value {
|
||||||
RmError::NotFound => Self::NotFound("The directory does not exist".to_string()),
|
RmError::NotFound(_) => {
|
||||||
RmError::NotEmpty => Self::BadRequest("The directory is not empty".to_string()),
|
Self::NotFound("At least one of the specified files does not exist".to_string())
|
||||||
|
}
|
||||||
|
RmError::NotEmpty(_) => Self::BadRequest(
|
||||||
|
"At least one of the specified directories does not exist".to_string(),
|
||||||
|
),
|
||||||
RmError::Unknown(e) => Self::InternalServerError(e.to_string()),
|
RmError::Unknown(e) => Self::InternalServerError(e.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,9 +55,7 @@ impl From<RmError> for ApiError {
|
|||||||
impl From<WarrenRmError> for ApiError {
|
impl From<WarrenRmError> for ApiError {
|
||||||
fn from(value: WarrenRmError) -> Self {
|
fn from(value: WarrenRmError) -> Self {
|
||||||
match value {
|
match value {
|
||||||
WarrenRmError::FileSystem(fs) => fs.into(),
|
|
||||||
WarrenRmError::FetchWarren(err) => err.into(),
|
WarrenRmError::FetchWarren(err) => err.into(),
|
||||||
WarrenRmError::Unknown(error) => Self::InternalServerError(error.to_string()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,9 +74,9 @@ where
|
|||||||
return Ok(Self(None));
|
return Ok(Self(None));
|
||||||
};
|
};
|
||||||
|
|
||||||
SharePassword::new(cookie.value())
|
Ok(SharePassword::new(cookie.value())
|
||||||
.map(|v| Self(Some(v)))
|
.map(|v| Self(Some(v)))
|
||||||
.map_err(|_| ApiError::BadRequest("Invalid password".to_string()))
|
.unwrap_or(Self(None)))
|
||||||
}
|
}
|
||||||
// Debug build
|
// Debug build
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
|
||||||
extract::{Query, State},
|
extract::{Query, State},
|
||||||
|
response::IntoResponse,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
@@ -9,7 +9,10 @@ use uuid::Uuid;
|
|||||||
use crate::{
|
use crate::{
|
||||||
domain::warren::{
|
domain::warren::{
|
||||||
models::{
|
models::{
|
||||||
file::{AbsoluteFilePathError, FilePath, FilePathError, FileStream},
|
file::{
|
||||||
|
AbsoluteFilePath, AbsoluteFilePathError, AbsoluteFilePathList,
|
||||||
|
AbsoluteFilePathListError, CatRequest, FilePath, FilePathError, FileStream,
|
||||||
|
},
|
||||||
share::{ShareCatRequest, SharePassword, SharePasswordError},
|
share::{ShareCatRequest, SharePassword, SharePasswordError},
|
||||||
},
|
},
|
||||||
ports::{AuthService, WarrenService},
|
ports::{AuthService, WarrenService},
|
||||||
@@ -24,6 +27,8 @@ enum ParseShareCatHttpRequestError {
|
|||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
AbsoluteFilePath(#[from] AbsoluteFilePathError),
|
AbsoluteFilePath(#[from] AbsoluteFilePathError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
PathList(#[from] AbsoluteFilePathListError),
|
||||||
|
#[error(transparent)]
|
||||||
Password(#[from] SharePasswordError),
|
Password(#[from] SharePasswordError),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +43,11 @@ impl From<ParseShareCatHttpRequestError> for ApiError {
|
|||||||
Self::BadRequest("The path must be absolute".to_string())
|
Self::BadRequest("The path must be absolute".to_string())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
ParseShareCatHttpRequestError::PathList(err) => match err {
|
||||||
|
AbsoluteFilePathListError::Empty => {
|
||||||
|
Self::BadRequest("You must provide at least 1 path".to_string())
|
||||||
|
}
|
||||||
|
},
|
||||||
ParseShareCatHttpRequestError::Password(err) => Self::BadRequest(
|
ParseShareCatHttpRequestError::Password(err) => Self::BadRequest(
|
||||||
match err {
|
match err {
|
||||||
SharePasswordError::Empty => "The provided password is empty",
|
SharePasswordError::Empty => "The provided password is empty",
|
||||||
@@ -56,7 +66,7 @@ impl From<ParseShareCatHttpRequestError> for ApiError {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(super) struct ShareCatHttpRequestBody {
|
pub(super) struct ShareCatHttpRequestBody {
|
||||||
share_id: Uuid,
|
share_id: Uuid,
|
||||||
path: String,
|
paths: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShareCatHttpRequestBody {
|
impl ShareCatHttpRequestBody {
|
||||||
@@ -64,9 +74,19 @@ impl ShareCatHttpRequestBody {
|
|||||||
self,
|
self,
|
||||||
password: Option<SharePassword>,
|
password: Option<SharePassword>,
|
||||||
) -> Result<ShareCatRequest, ParseShareCatHttpRequestError> {
|
) -> Result<ShareCatRequest, ParseShareCatHttpRequestError> {
|
||||||
let path = FilePath::new(&self.path)?.try_into()?;
|
let mut paths = Vec::<AbsoluteFilePath>::new();
|
||||||
|
|
||||||
Ok(ShareCatRequest::new(self.share_id, path, password))
|
for path in self.paths.split(':') {
|
||||||
|
paths.push(FilePath::new(path)?.try_into()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let path_list = AbsoluteFilePathList::new(paths)?;
|
||||||
|
|
||||||
|
Ok(ShareCatRequest::new(
|
||||||
|
self.share_id,
|
||||||
|
password,
|
||||||
|
CatRequest::new(path_list),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,13 +94,13 @@ pub async fn cat_share<WS: WarrenService, AS: AuthService>(
|
|||||||
State(state): State<AppState<WS, AS>>,
|
State(state): State<AppState<WS, AS>>,
|
||||||
SharePasswordHeader(password): SharePasswordHeader,
|
SharePasswordHeader(password): SharePasswordHeader,
|
||||||
Query(request): Query<ShareCatHttpRequestBody>,
|
Query(request): Query<ShareCatHttpRequestBody>,
|
||||||
) -> Result<Body, ApiError> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
let domain_request = request.try_into_domain(password)?;
|
let domain_request = request.try_into_domain(password)?;
|
||||||
|
|
||||||
state
|
state
|
||||||
.warren_service
|
.warren_service
|
||||||
.warren_share_cat(domain_request)
|
.warren_share_cat(domain_request)
|
||||||
.await
|
.await
|
||||||
.map(|response| FileStream::from(response).into())
|
.map(|response| FileStream::from(response))
|
||||||
.map_err(ApiError::from)
|
.map_err(ApiError::from)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
inbound::http::{
|
inbound::http::{
|
||||||
AppState,
|
AppState,
|
||||||
|
handlers::extractors::SharePasswordHeader,
|
||||||
responses::{ApiError, ApiSuccess},
|
responses::{ApiError, ApiSuccess},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -59,17 +60,14 @@ impl From<ParseShareLsHttpRequestError> for ApiError {
|
|||||||
pub(super) struct LsShareHttpRequestBody {
|
pub(super) struct LsShareHttpRequestBody {
|
||||||
share_id: Uuid,
|
share_id: Uuid,
|
||||||
path: String,
|
path: String,
|
||||||
password: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LsShareHttpRequestBody {
|
impl LsShareHttpRequestBody {
|
||||||
fn try_into_domain(self) -> Result<ShareLsRequest, ParseShareLsHttpRequestError> {
|
fn try_into_domain(
|
||||||
|
self,
|
||||||
|
password: Option<SharePassword>,
|
||||||
|
) -> Result<ShareLsRequest, ParseShareLsHttpRequestError> {
|
||||||
let path = FilePath::new(&self.path)?.try_into()?;
|
let path = FilePath::new(&self.path)?.try_into()?;
|
||||||
let password = if let Some(password) = self.password.as_ref() {
|
|
||||||
Some(SharePassword::new(password)?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(ShareLsRequest::new(self.share_id, path, password))
|
Ok(ShareLsRequest::new(self.share_id, path, password))
|
||||||
}
|
}
|
||||||
@@ -98,9 +96,10 @@ impl From<ShareLsResponse> for ShareLsResponseData {
|
|||||||
|
|
||||||
pub async fn ls_share<WS: WarrenService, AS: AuthService>(
|
pub async fn ls_share<WS: WarrenService, AS: AuthService>(
|
||||||
State(state): State<AppState<WS, AS>>,
|
State(state): State<AppState<WS, AS>>,
|
||||||
|
SharePasswordHeader(password): SharePasswordHeader,
|
||||||
Json(request): Json<LsShareHttpRequestBody>,
|
Json(request): Json<LsShareHttpRequestBody>,
|
||||||
) -> Result<ApiSuccess<ShareLsResponseData>, ApiError> {
|
) -> Result<ApiSuccess<ShareLsResponseData>, ApiError> {
|
||||||
let domain_request = request.try_into_domain()?;
|
let domain_request = request.try_into_domain(password)?;
|
||||||
|
|
||||||
state
|
state
|
||||||
.warren_service
|
.warren_service
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ mod list_shares;
|
|||||||
mod list_warrens;
|
mod list_warrens;
|
||||||
mod ls_share;
|
mod ls_share;
|
||||||
mod upload_warren_files;
|
mod upload_warren_files;
|
||||||
|
mod verify_share_password;
|
||||||
mod warren_cat;
|
mod warren_cat;
|
||||||
mod warren_cp;
|
mod warren_cp;
|
||||||
mod warren_ls;
|
mod warren_ls;
|
||||||
@@ -17,6 +18,8 @@ mod warren_rm;
|
|||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
extract::DefaultBodyLimit,
|
extract::DefaultBodyLimit,
|
||||||
|
http::{self, HeaderValue, Response},
|
||||||
|
response::IntoResponse,
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,13 +38,14 @@ use get_share::get_share;
|
|||||||
use list_shares::list_shares;
|
use list_shares::list_shares;
|
||||||
use ls_share::ls_share;
|
use ls_share::ls_share;
|
||||||
use upload_warren_files::warren_save;
|
use upload_warren_files::warren_save;
|
||||||
|
use verify_share_password::verify_share_password;
|
||||||
use warren_cat::fetch_file;
|
use warren_cat::fetch_file;
|
||||||
use warren_cp::warren_cp;
|
use warren_cp::warren_cp;
|
||||||
use warren_mv::warren_mv;
|
use warren_mv::warren_mv;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
domain::warren::{
|
domain::warren::{
|
||||||
models::file::{File, FileMimeType, FileType},
|
models::file::{File, FileMimeType, FileStream, FileStreamInner, FileType},
|
||||||
ports::{AuthService, WarrenService},
|
ports::{AuthService, WarrenService},
|
||||||
},
|
},
|
||||||
inbound::http::AppState,
|
inbound::http::AppState,
|
||||||
@@ -53,6 +57,7 @@ pub struct WarrenFileElement {
|
|||||||
name: String,
|
name: String,
|
||||||
file_type: FileType,
|
file_type: FileType,
|
||||||
mime_type: Option<String>,
|
mime_type: Option<String>,
|
||||||
|
size: u64,
|
||||||
created_at: Option<u64>,
|
created_at: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,11 +67,36 @@ impl From<&File> for WarrenFileElement {
|
|||||||
name: value.name().to_string(),
|
name: value.name().to_string(),
|
||||||
file_type: value.file_type().to_owned(),
|
file_type: value.file_type().to_owned(),
|
||||||
mime_type: value.mime_type().map(FileMimeType::to_string),
|
mime_type: value.mime_type().map(FileMimeType::to_string),
|
||||||
|
size: value.size().into(),
|
||||||
created_at: value.created_at(),
|
created_at: value.created_at(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for FileStream {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
let mut builder = Response::builder().header(http::header::TRANSFER_ENCODING, "chunked");
|
||||||
|
|
||||||
|
if let Some(headers) = builder.headers_mut() {
|
||||||
|
if let Some(mime_type) = self.mime_type() {
|
||||||
|
headers.insert(
|
||||||
|
http::header::CONTENT_TYPE,
|
||||||
|
HeaderValue::from_str(mime_type.as_str()).unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
headers.insert(
|
||||||
|
http::header::TRANSFER_ENCODING,
|
||||||
|
HeaderValue::from_str("chunked").unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder
|
||||||
|
.body(axum::body::Body::from_stream(FileStreamInner::from(self)))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn routes<WS: WarrenService, AS: AuthService>() -> Router<AppState<WS, AS>> {
|
pub fn routes<WS: WarrenService, AS: AuthService>() -> Router<AppState<WS, AS>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(list_warrens))
|
.route("/", get(list_warrens))
|
||||||
@@ -88,4 +118,5 @@ pub fn routes<WS: WarrenService, AS: AuthService>() -> Router<AppState<WS, AS>>
|
|||||||
.route("/files/get_share", post(get_share))
|
.route("/files/get_share", post(get_share))
|
||||||
.route("/files/ls_share", post(ls_share))
|
.route("/files/ls_share", post(ls_share))
|
||||||
.route("/files/cat_share", get(cat_share))
|
.route("/files/cat_share", get(cat_share))
|
||||||
|
.route("/files/verify_share_password", post(verify_share_password))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
use axum::{Json, extract::State, http::StatusCode};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use thiserror::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
domain::warren::{
|
||||||
|
models::share::{SharePassword, SharePasswordError, VerifySharePasswordRequest},
|
||||||
|
ports::{AuthService, WarrenService},
|
||||||
|
},
|
||||||
|
inbound::http::{
|
||||||
|
AppState,
|
||||||
|
responses::{ApiError, ApiSuccess},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
enum ParseVerifySharePasswordHttpRequestError {
|
||||||
|
#[error(transparent)]
|
||||||
|
SharePassword(#[from] SharePasswordError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ParseVerifySharePasswordHttpRequestError> for ApiError {
|
||||||
|
fn from(value: ParseVerifySharePasswordHttpRequestError) -> Self {
|
||||||
|
match value {
|
||||||
|
ParseVerifySharePasswordHttpRequestError::SharePassword(err) => Self::BadRequest(
|
||||||
|
match err {
|
||||||
|
SharePasswordError::Empty => "The provided password is empty",
|
||||||
|
SharePasswordError::LeadingWhitespace
|
||||||
|
| SharePasswordError::TrailingWhitespace
|
||||||
|
| SharePasswordError::TooShort
|
||||||
|
| SharePasswordError::TooLong => "",
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(super) struct VerifySharePasswordHttpRequestBody {
|
||||||
|
share_id: Uuid,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VerifySharePasswordHttpRequestBody {
|
||||||
|
fn try_into_domain(
|
||||||
|
self,
|
||||||
|
) -> Result<VerifySharePasswordRequest, ParseVerifySharePasswordHttpRequestError> {
|
||||||
|
let password = SharePassword::new(&self.password)?;
|
||||||
|
|
||||||
|
Ok(VerifySharePasswordRequest::new(
|
||||||
|
self.share_id,
|
||||||
|
Some(password),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn verify_share_password<WS: WarrenService, AS: AuthService>(
|
||||||
|
State(state): State<AppState<WS, AS>>,
|
||||||
|
Json(request): Json<VerifySharePasswordHttpRequestBody>,
|
||||||
|
) -> Result<ApiSuccess<()>, ApiError> {
|
||||||
|
let domain_request = request.try_into_domain()?;
|
||||||
|
|
||||||
|
state
|
||||||
|
.warren_service
|
||||||
|
.verify_warren_share_password(domain_request)
|
||||||
|
.await
|
||||||
|
.map(|_| ApiSuccess::new(StatusCode::OK, ()))
|
||||||
|
.map_err(ApiError::from)
|
||||||
|
}
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
|
||||||
extract::{Query, State},
|
extract::{Query, State},
|
||||||
|
response::IntoResponse,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio_util::io::ReaderStream;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
domain::warren::{
|
domain::warren::{
|
||||||
models::{
|
models::{
|
||||||
auth_session::AuthRequest,
|
auth_session::AuthRequest,
|
||||||
file::{AbsoluteFilePathError, CatRequest, FilePath, FilePathError, FileStream},
|
file::{
|
||||||
|
AbsoluteFilePath, AbsoluteFilePathError, AbsoluteFilePathList,
|
||||||
|
AbsoluteFilePathListError, CatRequest, FilePath, FilePathError,
|
||||||
|
},
|
||||||
warren::WarrenCatRequest,
|
warren::WarrenCatRequest,
|
||||||
},
|
},
|
||||||
ports::{AuthService, WarrenService},
|
ports::{AuthService, WarrenService},
|
||||||
@@ -23,15 +25,17 @@ use crate::{
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(super) struct WarrenCatHttpRequestBody {
|
pub(super) struct WarrenCatHttpRequestBody {
|
||||||
warren_id: Uuid,
|
warren_id: Uuid,
|
||||||
path: String,
|
paths: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum ParseWarrenCatHttpRequestError {
|
pub enum ParseWarrenCatHttpRequestError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
FilePath(#[from] FilePathError),
|
FilePath(#[from] FilePathError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
AbsoluteFilePath(#[from] AbsoluteFilePathError),
|
AbsoluteFilePath(#[from] AbsoluteFilePathError),
|
||||||
|
#[error(transparent)]
|
||||||
|
PathList(#[from] AbsoluteFilePathListError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ParseWarrenCatHttpRequestError> for ApiError {
|
impl From<ParseWarrenCatHttpRequestError> for ApiError {
|
||||||
@@ -47,21 +51,29 @@ impl From<ParseWarrenCatHttpRequestError> for ApiError {
|
|||||||
ApiError::BadRequest("The file path must be absolute".to_string())
|
ApiError::BadRequest("The file path must be absolute".to_string())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
ParseWarrenCatHttpRequestError::PathList(err) => match err {
|
||||||
|
AbsoluteFilePathListError::Empty => {
|
||||||
|
Self::BadRequest("You must provide at least 1 path".to_string())
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WarrenCatHttpRequestBody {
|
impl WarrenCatHttpRequestBody {
|
||||||
fn try_into_domain(self) -> Result<WarrenCatRequest, ParseWarrenCatHttpRequestError> {
|
fn try_into_domain(self) -> Result<WarrenCatRequest, ParseWarrenCatHttpRequestError> {
|
||||||
let path = FilePath::new(&self.path)?.try_into()?;
|
let mut paths = Vec::<AbsoluteFilePath>::new();
|
||||||
|
|
||||||
Ok(WarrenCatRequest::new(self.warren_id, CatRequest::new(path)))
|
for path in self.paths.split(':') {
|
||||||
}
|
paths.push(FilePath::new(path)?.try_into()?);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<FileStream> for Body {
|
let path_list = AbsoluteFilePathList::new(paths)?;
|
||||||
fn from(value: FileStream) -> Self {
|
|
||||||
Body::from_stream::<ReaderStream<tokio::fs::File>>(value.into())
|
Ok(WarrenCatRequest::new(
|
||||||
|
self.warren_id,
|
||||||
|
CatRequest::new(path_list),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,13 +81,12 @@ pub async fn fetch_file<WS: WarrenService, AS: AuthService>(
|
|||||||
State(state): State<AppState<WS, AS>>,
|
State(state): State<AppState<WS, AS>>,
|
||||||
SessionIdHeader(session): SessionIdHeader,
|
SessionIdHeader(session): SessionIdHeader,
|
||||||
Query(request): Query<WarrenCatHttpRequestBody>,
|
Query(request): Query<WarrenCatHttpRequestBody>,
|
||||||
) -> Result<Body, ApiError> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
let domain_request = AuthRequest::new(session, request.try_into_domain()?);
|
let domain_request = AuthRequest::new(session, request.try_into_domain()?);
|
||||||
|
|
||||||
state
|
state
|
||||||
.auth_service
|
.auth_service
|
||||||
.auth_warren_cat(domain_request, state.warren_service.as_ref())
|
.auth_warren_cat(domain_request, state.warren_service.as_ref())
|
||||||
.await
|
.await
|
||||||
.map(|contents| contents.into())
|
|
||||||
.map_err(ApiError::from)
|
.map_err(ApiError::from)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ use crate::{
|
|||||||
domain::warren::{
|
domain::warren::{
|
||||||
models::{
|
models::{
|
||||||
auth_session::AuthRequest,
|
auth_session::AuthRequest,
|
||||||
file::{AbsoluteFilePath, AbsoluteFilePathError, FilePath, FilePathError, MvRequest},
|
file::{
|
||||||
|
AbsoluteFilePath, AbsoluteFilePathError, AbsoluteFilePathList,
|
||||||
|
AbsoluteFilePathListError, FilePath, FilePathError, MvRequest,
|
||||||
|
},
|
||||||
warren::WarrenMvRequest,
|
warren::WarrenMvRequest,
|
||||||
},
|
},
|
||||||
ports::{AuthService, WarrenService},
|
ports::{AuthService, WarrenService},
|
||||||
@@ -23,7 +26,7 @@ use crate::{
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct MvWarrenEntryHttpRequestBody {
|
pub struct MvWarrenEntryHttpRequestBody {
|
||||||
warren_id: Uuid,
|
warren_id: Uuid,
|
||||||
path: String,
|
paths: Vec<String>,
|
||||||
target_path: String,
|
target_path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,16 +36,25 @@ pub enum ParseWarrenMvHttpRequestError {
|
|||||||
FilePath(#[from] FilePathError),
|
FilePath(#[from] FilePathError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
AbsoluteFilePath(#[from] AbsoluteFilePathError),
|
AbsoluteFilePath(#[from] AbsoluteFilePathError),
|
||||||
|
#[error(transparent)]
|
||||||
|
AbsoluteFilePathList(#[from] AbsoluteFilePathListError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MvWarrenEntryHttpRequestBody {
|
impl MvWarrenEntryHttpRequestBody {
|
||||||
fn try_into_domain(self) -> Result<WarrenMvRequest, ParseWarrenMvHttpRequestError> {
|
fn try_into_domain(self) -> Result<WarrenMvRequest, ParseWarrenMvHttpRequestError> {
|
||||||
let path: AbsoluteFilePath = FilePath::new(&self.path)?.try_into()?;
|
let mut paths = Vec::<AbsoluteFilePath>::new();
|
||||||
|
|
||||||
|
for path in self.paths.iter() {
|
||||||
|
paths.push(FilePath::new(path)?.try_into()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let path_list = AbsoluteFilePathList::new(paths)?;
|
||||||
|
|
||||||
let target_path: AbsoluteFilePath = FilePath::new(&self.target_path)?.try_into()?;
|
let target_path: AbsoluteFilePath = FilePath::new(&self.target_path)?.try_into()?;
|
||||||
|
|
||||||
Ok(WarrenMvRequest::new(
|
Ok(WarrenMvRequest::new(
|
||||||
self.warren_id,
|
self.warren_id,
|
||||||
MvRequest::new(path, target_path),
|
MvRequest::new(path_list, target_path),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,12 +64,17 @@ impl From<ParseWarrenMvHttpRequestError> for ApiError {
|
|||||||
match value {
|
match value {
|
||||||
ParseWarrenMvHttpRequestError::FilePath(err) => match err {
|
ParseWarrenMvHttpRequestError::FilePath(err) => match err {
|
||||||
FilePathError::InvalidPath => {
|
FilePathError::InvalidPath => {
|
||||||
ApiError::BadRequest("The file path must be valid".to_string())
|
Self::BadRequest("The file path must be valid".to_string())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ParseWarrenMvHttpRequestError::AbsoluteFilePath(err) => match err {
|
ParseWarrenMvHttpRequestError::AbsoluteFilePath(err) => match err {
|
||||||
AbsoluteFilePathError::NotAbsolute => {
|
AbsoluteFilePathError::NotAbsolute => {
|
||||||
ApiError::BadRequest("The file path must be absolute".to_string())
|
Self::BadRequest("The file path must be absolute".to_string())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ParseWarrenMvHttpRequestError::AbsoluteFilePathList(err) => match err {
|
||||||
|
AbsoluteFilePathListError::Empty => {
|
||||||
|
Self::BadRequest("You must provide at least 1 path".to_string())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ use crate::{
|
|||||||
domain::warren::{
|
domain::warren::{
|
||||||
models::{
|
models::{
|
||||||
auth_session::AuthRequest,
|
auth_session::AuthRequest,
|
||||||
file::{AbsoluteFilePathError, FilePath, FilePathError, RmRequest},
|
file::{
|
||||||
|
AbsoluteFilePath, AbsoluteFilePathError, AbsoluteFilePathList,
|
||||||
|
AbsoluteFilePathListError, FilePath, FilePathError, RmRequest,
|
||||||
|
},
|
||||||
warren::WarrenRmRequest,
|
warren::WarrenRmRequest,
|
||||||
},
|
},
|
||||||
ports::{AuthService, WarrenService},
|
ports::{AuthService, WarrenService},
|
||||||
@@ -23,7 +26,7 @@ use crate::{
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(super) struct WarrenRmHttpRequestBody {
|
pub(super) struct WarrenRmHttpRequestBody {
|
||||||
warren_id: Uuid,
|
warren_id: Uuid,
|
||||||
path: String,
|
paths: Vec<String>,
|
||||||
force: bool,
|
force: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +36,8 @@ pub(super) enum ParseWarrenRmHttpRequestError {
|
|||||||
FilePath(#[from] FilePathError),
|
FilePath(#[from] FilePathError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
AbsoluteFilePath(#[from] AbsoluteFilePathError),
|
AbsoluteFilePath(#[from] AbsoluteFilePathError),
|
||||||
|
#[error(transparent)]
|
||||||
|
AbsoluteFilePathList(#[from] AbsoluteFilePathListError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ParseWarrenRmHttpRequestError> for ApiError {
|
impl From<ParseWarrenRmHttpRequestError> for ApiError {
|
||||||
@@ -40,12 +45,17 @@ impl From<ParseWarrenRmHttpRequestError> for ApiError {
|
|||||||
match value {
|
match value {
|
||||||
ParseWarrenRmHttpRequestError::FilePath(err) => match err {
|
ParseWarrenRmHttpRequestError::FilePath(err) => match err {
|
||||||
FilePathError::InvalidPath => {
|
FilePathError::InvalidPath => {
|
||||||
ApiError::BadRequest("The file path must be valid".to_string())
|
ApiError::BadRequest("File paths must be valid".to_string())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ParseWarrenRmHttpRequestError::AbsoluteFilePath(err) => match err {
|
ParseWarrenRmHttpRequestError::AbsoluteFilePath(err) => match err {
|
||||||
AbsoluteFilePathError::NotAbsolute => {
|
AbsoluteFilePathError::NotAbsolute => {
|
||||||
ApiError::BadRequest("The file path must be absolute".to_string())
|
ApiError::BadRequest("File paths must be absolute".to_string())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ParseWarrenRmHttpRequestError::AbsoluteFilePathList(err) => match err {
|
||||||
|
AbsoluteFilePathListError::Empty => {
|
||||||
|
Self::BadRequest("At least one file path is required".to_string())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -54,11 +64,17 @@ impl From<ParseWarrenRmHttpRequestError> for ApiError {
|
|||||||
|
|
||||||
impl WarrenRmHttpRequestBody {
|
impl WarrenRmHttpRequestBody {
|
||||||
fn try_into_domain(self) -> Result<WarrenRmRequest, ParseWarrenRmHttpRequestError> {
|
fn try_into_domain(self) -> Result<WarrenRmRequest, ParseWarrenRmHttpRequestError> {
|
||||||
let path = FilePath::new(&self.path)?;
|
let mut paths = Vec::<AbsoluteFilePath>::new();
|
||||||
|
|
||||||
|
for path in self.paths.iter() {
|
||||||
|
paths.push(FilePath::new(path)?.try_into()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let path_list = AbsoluteFilePathList::new(paths)?;
|
||||||
|
|
||||||
Ok(WarrenRmRequest::new(
|
Ok(WarrenRmRequest::new(
|
||||||
self.warren_id,
|
self.warren_id,
|
||||||
RmRequest::new(path.try_into()?, self.force),
|
RmRequest::new(path_list, self.force),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
use std::{os::unix::fs::MetadataExt, path::PathBuf};
|
use anyhow::{Context as _, anyhow, bail};
|
||||||
|
use futures_util::{TryStreamExt, future::join_all};
|
||||||
use anyhow::{Context, anyhow, bail};
|
|
||||||
use futures_util::TryStreamExt;
|
|
||||||
use rustix::fs::{Statx, statx};
|
use rustix::fs::{Statx, statx};
|
||||||
use tokio::{
|
use std::{
|
||||||
fs,
|
collections::HashSet,
|
||||||
io::{self, AsyncWriteExt as _},
|
io::Write,
|
||||||
|
os::unix::fs::MetadataExt,
|
||||||
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
use tokio::{
|
||||||
|
fs::{self},
|
||||||
|
io::{self, AsyncReadExt as _, AsyncWriteExt as _},
|
||||||
|
};
|
||||||
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
use tokio_util::io::ReaderStream;
|
use tokio_util::io::ReaderStream;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -15,10 +20,10 @@ use crate::{
|
|||||||
models::{
|
models::{
|
||||||
file::{
|
file::{
|
||||||
AbsoluteFilePath, CatError, CatRequest, CpError, CpRequest, CpResponse, File,
|
AbsoluteFilePath, CatError, CatRequest, CpError, CpRequest, CpResponse, File,
|
||||||
FileMimeType, FileName, FilePath, FileStream, FileType, LsError, LsRequest,
|
FileMimeType, FileName, FilePath, FileSize, FileStream, FileType, LsError,
|
||||||
LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RelativeFilePath,
|
LsRequest, LsResponse, MkdirError, MkdirRequest, MvError, MvRequest,
|
||||||
RmError, RmRequest, SaveError, SaveRequest, SaveResponse, StatError, StatRequest,
|
RelativeFilePath, RmError, RmRequest, SaveError, SaveRequest, SaveResponse,
|
||||||
StatResponse, TouchError, TouchRequest,
|
StatError, StatRequest, StatResponse, TouchError, TouchRequest,
|
||||||
},
|
},
|
||||||
warren::UploadFileStream,
|
warren::UploadFileStream,
|
||||||
},
|
},
|
||||||
@@ -26,30 +31,46 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_FILE_FETCH_BYTES: &str = "MAX_FILE_FETCH_BYTES";
|
const MAX_FILE_FETCH_BYTES_KEY: &str = "MAX_FILE_FETCH_BYTES";
|
||||||
|
const ZIP_READ_BUFFER_BYTES_KEY: &str = "ZIP_READ_BUFFER_BYTES";
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FileSystemConfig {
|
pub struct FileSystemConfig {
|
||||||
base_directory: String,
|
base_directory: String,
|
||||||
max_file_fetch_bytes: u64,
|
max_file_fetch_bytes: u64,
|
||||||
|
zip_read_buffer_bytes: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileSystemConfig {
|
impl FileSystemConfig {
|
||||||
pub fn new(base_directory: String, max_file_fetch_bytes: u64) -> Self {
|
pub fn new(
|
||||||
|
base_directory: String,
|
||||||
|
max_file_fetch_bytes: u64,
|
||||||
|
zip_read_buffer_bytes: usize,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
base_directory,
|
base_directory,
|
||||||
max_file_fetch_bytes,
|
max_file_fetch_bytes,
|
||||||
|
zip_read_buffer_bytes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_env(serve_dir: String) -> anyhow::Result<Self> {
|
pub fn from_env(serve_dir: String) -> anyhow::Result<Self> {
|
||||||
// 268435456 bytes = 0.25GB
|
// 268435456 bytes = 0.25GB
|
||||||
let max_file_fetch_bytes: u64 = match Config::load_env(MAX_FILE_FETCH_BYTES) {
|
let max_file_fetch_bytes: u64 = match Config::load_env(MAX_FILE_FETCH_BYTES_KEY) {
|
||||||
Ok(value) => value.parse()?,
|
Ok(value) => value.parse()?,
|
||||||
Err(_) => 268435456,
|
Err(_) => 268435456,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self::new(serve_dir, max_file_fetch_bytes))
|
let zip_read_buffer_bytes: usize = match Config::load_env(ZIP_READ_BUFFER_BYTES_KEY) {
|
||||||
|
Ok(value) => value.parse()?,
|
||||||
|
Err(_) => 4096,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self::new(
|
||||||
|
serve_dir,
|
||||||
|
max_file_fetch_bytes,
|
||||||
|
zip_read_buffer_bytes,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +78,7 @@ impl FileSystemConfig {
|
|||||||
pub struct FileSystem {
|
pub struct FileSystem {
|
||||||
base_directory: FilePath,
|
base_directory: FilePath,
|
||||||
max_file_fetch_bytes: u64,
|
max_file_fetch_bytes: u64,
|
||||||
|
zip_read_buffer_bytes: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileSystem {
|
impl FileSystem {
|
||||||
@@ -64,6 +86,7 @@ impl FileSystem {
|
|||||||
let file_system = Self {
|
let file_system = Self {
|
||||||
base_directory: FilePath::new(&config.base_directory)?,
|
base_directory: FilePath::new(&config.base_directory)?,
|
||||||
max_file_fetch_bytes: config.max_file_fetch_bytes,
|
max_file_fetch_bytes: config.max_file_fetch_bytes,
|
||||||
|
zip_read_buffer_bytes: config.zip_read_buffer_bytes,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(file_system)
|
Ok(file_system)
|
||||||
@@ -85,28 +108,8 @@ impl FileSystem {
|
|||||||
|
|
||||||
let mut files = Vec::new();
|
let mut files = Vec::new();
|
||||||
|
|
||||||
let parent = if include_parent {
|
|
||||||
let dir_name = FileName::new(
|
|
||||||
&dir_path
|
|
||||||
.file_name()
|
|
||||||
.context("Failed to get directory name")?
|
|
||||||
.to_owned()
|
|
||||||
.into_string()
|
|
||||||
.ok()
|
|
||||||
.context("Failed to get directory name")?,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Some(File::new(
|
|
||||||
dir_name,
|
|
||||||
FileType::Directory,
|
|
||||||
None,
|
|
||||||
get_btime(&dir_path),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut dir = fs::read_dir(&dir_path).await?;
|
let mut dir = fs::read_dir(&dir_path).await?;
|
||||||
|
let parent_size = FileSize::new(file_size_recursive(&dir_path).await?);
|
||||||
|
|
||||||
while let Ok(Some(entry)) = dir.next_entry().await {
|
while let Ok(Some(entry)) = dir.next_entry().await {
|
||||||
let name = entry
|
let name = entry
|
||||||
@@ -126,6 +129,8 @@ impl FileSystem {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let file_size = FileSize::new(file_size_recursive(entry.path()).await?);
|
||||||
|
|
||||||
let created_at = get_btime(entry.path());
|
let created_at = get_btime(entry.path());
|
||||||
|
|
||||||
let mime_type = match file_type {
|
let mime_type = match file_type {
|
||||||
@@ -137,10 +142,33 @@ impl FileSystem {
|
|||||||
FileName::new(&name)?,
|
FileName::new(&name)?,
|
||||||
file_type,
|
file_type,
|
||||||
mime_type,
|
mime_type,
|
||||||
|
file_size,
|
||||||
created_at,
|
created_at,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let parent = if include_parent {
|
||||||
|
let dir_name = FileName::new(
|
||||||
|
&dir_path
|
||||||
|
.file_name()
|
||||||
|
.context("Failed to get directory name")?
|
||||||
|
.to_owned()
|
||||||
|
.into_string()
|
||||||
|
.ok()
|
||||||
|
.context("Failed to get directory name")?,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Some(File::new(
|
||||||
|
dir_name,
|
||||||
|
FileType::Directory,
|
||||||
|
None,
|
||||||
|
parent_size,
|
||||||
|
get_btime(&dir_path),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
Ok(LsResponse::new(files, parent))
|
Ok(LsResponse::new(files, parent))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,10 +187,10 @@ impl FileSystem {
|
|||||||
|
|
||||||
/// Actually removes a file or directory from the underlying file system
|
/// Actually removes a file or directory from the underlying file system
|
||||||
///
|
///
|
||||||
/// * `path`: The directory's absolute path (absolute not in relation to the root file system but `self.base_directory`)
|
/// * `path`: The file's absolute path (absolute not in relation to the root file system but `self.base_directory`)
|
||||||
/// * `force`: Whether to delete directories that are not empty
|
/// * `force`: Whether to delete directories that are not empty
|
||||||
async fn rm(&self, path: &AbsoluteFilePath, force: bool) -> io::Result<()> {
|
async fn rm(&self, path: &AbsoluteFilePath, force: bool) -> io::Result<()> {
|
||||||
let file_path = self.get_target_path(path);
|
let file_path = self.get_target_path(&path);
|
||||||
|
|
||||||
if fs::metadata(&file_path).await?.is_file() {
|
if fs::metadata(&file_path).await?.is_file() {
|
||||||
return fs::remove_file(&file_path).await;
|
return fs::remove_file(&file_path).await;
|
||||||
@@ -191,6 +219,7 @@ impl FileSystem {
|
|||||||
let mut file = fs::OpenOptions::new()
|
let mut file = fs::OpenOptions::new()
|
||||||
.write(true)
|
.write(true)
|
||||||
.create(true)
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
.open(&file_path)
|
.open(&file_path)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -204,40 +233,139 @@ impl FileSystem {
|
|||||||
Ok(paths)
|
Ok(paths)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn cat(&self, path: &AbsoluteFilePath) -> anyhow::Result<FileStream> {
|
async fn cat(&self, paths: &Vec<AbsoluteFilePath>) -> anyhow::Result<FileStream> {
|
||||||
let path = self.get_target_path(path);
|
let paths: Vec<FilePath> = paths
|
||||||
|
.into_iter()
|
||||||
|
.map(|path| self.get_target_path(path))
|
||||||
|
.collect();
|
||||||
|
|
||||||
let file = fs::OpenOptions::new()
|
let path_request = PathRequest::from_paths(paths)?;
|
||||||
.create(false)
|
|
||||||
.write(false)
|
|
||||||
.read(true)
|
|
||||||
.open(&path)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let file_size = file.metadata().await?.size();
|
fn build_zip_stream(
|
||||||
|
prefix: String,
|
||||||
|
paths: Vec<FilePath>,
|
||||||
|
buffer_size: usize,
|
||||||
|
) -> FileStream {
|
||||||
|
let (sync_tx, sync_rx) =
|
||||||
|
std::sync::mpsc::channel::<Result<bytes::Bytes, std::io::Error>>();
|
||||||
|
let (tx, rx) = tokio::sync::mpsc::channel::<Result<bytes::Bytes, std::io::Error>>(1024);
|
||||||
|
|
||||||
if file_size > self.max_file_fetch_bytes {
|
tokio::task::spawn(create_zip(
|
||||||
bail!("File size exceeds configured limit");
|
prefix,
|
||||||
|
paths
|
||||||
|
.into_iter()
|
||||||
|
.map(|p| PathBuf::from(p.as_str()))
|
||||||
|
.collect(),
|
||||||
|
sync_tx,
|
||||||
|
buffer_size,
|
||||||
|
));
|
||||||
|
tokio::task::spawn(async move {
|
||||||
|
while let Ok(v) = sync_rx.recv() {
|
||||||
|
let _ = tx.send(v).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
FileStream::new(
|
||||||
|
FileType::Directory,
|
||||||
|
Some(FileMimeType::new_raw("application/zip")),
|
||||||
|
ReceiverStream::new(rx),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let stream = FileStream::new(ReaderStream::new(file));
|
match path_request {
|
||||||
|
PathRequest::Single(file_path) => {
|
||||||
|
let file = fs::OpenOptions::new()
|
||||||
|
.create(false)
|
||||||
|
.write(false)
|
||||||
|
.read(true)
|
||||||
|
.open(&file_path)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(stream)
|
let metadata = file.metadata().await?;
|
||||||
|
|
||||||
|
if metadata.is_dir() {
|
||||||
|
drop(file);
|
||||||
|
|
||||||
|
return Ok(build_zip_stream(
|
||||||
|
file_path.to_string(),
|
||||||
|
vec![file_path],
|
||||||
|
self.zip_read_buffer_bytes,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if metadata.size() > self.max_file_fetch_bytes {
|
||||||
|
bail!("File size exceeds configured limit");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mime_type = {
|
||||||
|
let file_name = {
|
||||||
|
let path = file_path.as_str();
|
||||||
|
if let Some(last_slash_index) = path.rfind("/") {
|
||||||
|
&path[last_slash_index + 1..]
|
||||||
|
} else {
|
||||||
|
path
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
FileMimeType::from_name(file_name)
|
||||||
|
};
|
||||||
|
|
||||||
|
let stream = FileStream::new(FileType::File, mime_type, ReaderStream::new(file));
|
||||||
|
|
||||||
|
Ok(stream)
|
||||||
|
}
|
||||||
|
PathRequest::Multiple {
|
||||||
|
lowest_common_prefix,
|
||||||
|
paths,
|
||||||
|
} => Ok(build_zip_stream(
|
||||||
|
lowest_common_prefix,
|
||||||
|
paths,
|
||||||
|
self.zip_read_buffer_bytes,
|
||||||
|
)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn mv(&self, path: &AbsoluteFilePath, target_path: &AbsoluteFilePath) -> io::Result<()> {
|
async fn mv(
|
||||||
let current_path = self.get_target_path(path);
|
&self,
|
||||||
let target_path = self.get_target_path(target_path);
|
path: &AbsoluteFilePath,
|
||||||
|
target_path: &AbsoluteFilePath,
|
||||||
|
) -> io::Result<(AbsoluteFilePath, AbsoluteFilePath)> {
|
||||||
|
let mut target_path = target_path.clone();
|
||||||
|
|
||||||
if !fs::try_exists(¤t_path).await? {
|
let current_fs_path = self.get_target_path(path);
|
||||||
|
let mut target_fs_path = self.get_target_path(&target_path);
|
||||||
|
|
||||||
|
if !fs::try_exists(¤t_fs_path).await? {
|
||||||
return Err(io::ErrorKind::NotFound.into());
|
return Err(io::ErrorKind::NotFound.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
if fs::try_exists(&target_path).await? {
|
if !fs::try_exists(&target_fs_path).await? {
|
||||||
|
return fs::rename(current_fs_path, target_fs_path)
|
||||||
|
.await
|
||||||
|
.map(|_| (path.clone(), target_path.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let target_is_dir = fs::metadata(target_fs_path).await?.is_dir();
|
||||||
|
|
||||||
|
if !target_is_dir {
|
||||||
return Err(io::ErrorKind::AlreadyExists.into());
|
return Err(io::ErrorKind::AlreadyExists.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::rename(current_path, &target_path).await
|
let name = {
|
||||||
|
let current_path = path.as_str();
|
||||||
|
// This unwrap is safe because an `AbsoluteFilePath` always starts with a slash
|
||||||
|
let last_slash_index = current_path.rfind("/").unwrap();
|
||||||
|
|
||||||
|
¤t_path[last_slash_index + 1..]
|
||||||
|
};
|
||||||
|
|
||||||
|
target_path =
|
||||||
|
target_path.join(&RelativeFilePath::new(FilePath::new(name).unwrap()).unwrap());
|
||||||
|
target_fs_path = self.get_target_path(&target_path);
|
||||||
|
|
||||||
|
fs::rename(current_fs_path, target_fs_path)
|
||||||
|
.await
|
||||||
|
.map(|_| (path.clone(), target_path.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn touch(&self, path: &AbsoluteFilePath) -> io::Result<()> {
|
async fn touch(&self, path: &AbsoluteFilePath) -> io::Result<()> {
|
||||||
@@ -299,9 +427,10 @@ impl FileSystem {
|
|||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let file_size = FileSize::new(file_size_recursive(target_path).await?);
|
||||||
let created_at = get_btime(&fs_path);
|
let created_at = get_btime(&fs_path);
|
||||||
|
|
||||||
Ok(File::new(name, file_type, mime_type, created_at))
|
Ok(File::new(name, file_type, mime_type, file_size, created_at))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,9 +450,9 @@ impl FileSystemRepository for FileSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn cat(&self, request: CatRequest) -> Result<FileStream, CatError> {
|
async fn cat(&self, request: CatRequest) -> Result<FileStream, CatError> {
|
||||||
self.cat(request.path())
|
self.cat(request.paths().paths()).await.map_err(|e| {
|
||||||
.await
|
anyhow!("Failed to fetch files {:?}: {e:?}", request.paths().paths()).into()
|
||||||
.map_err(|e| anyhow!("Failed to fetch file {}: {e:?}", request.path()).into())
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn mkdir(&self, request: MkdirRequest) -> Result<(), MkdirError> {
|
async fn mkdir(&self, request: MkdirRequest) -> Result<(), MkdirError> {
|
||||||
@@ -335,28 +464,61 @@ impl FileSystemRepository for FileSystem {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn rm(&self, request: RmRequest) -> Result<(), RmError> {
|
async fn rm(&self, request: RmRequest) -> Vec<Result<AbsoluteFilePath, RmError>> {
|
||||||
self.rm(request.path(), request.force())
|
let force = request.force();
|
||||||
.await
|
let paths: Vec<AbsoluteFilePath> = request.into_paths().into();
|
||||||
.map_err(|e| match e.kind() {
|
|
||||||
std::io::ErrorKind::NotFound => RmError::NotFound,
|
async fn _rm(
|
||||||
std::io::ErrorKind::DirectoryNotEmpty => RmError::NotEmpty,
|
fs: &FileSystem,
|
||||||
_ => anyhow!("Failed to delete file at {}: {e:?}", request.path()).into(),
|
path: AbsoluteFilePath,
|
||||||
})
|
force: bool,
|
||||||
|
) -> Result<AbsoluteFilePath, RmError> {
|
||||||
|
fs.rm(&path, force)
|
||||||
|
.await
|
||||||
|
.map(|_| path.clone())
|
||||||
|
.map_err(|e| match e.kind() {
|
||||||
|
std::io::ErrorKind::NotFound => RmError::NotFound(path),
|
||||||
|
std::io::ErrorKind::DirectoryNotEmpty => RmError::NotEmpty(path),
|
||||||
|
_ => anyhow!("Failed to delete file at {}: {e:?}", path).into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
join_all(
|
||||||
|
paths
|
||||||
|
.into_iter()
|
||||||
|
.map(|path| _rm(&self, path, force))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn mv(&self, request: MvRequest) -> Result<(), MvError> {
|
async fn mv(
|
||||||
self.mv(request.path(), request.target_path())
|
&self,
|
||||||
.await
|
request: MvRequest,
|
||||||
.map_err(|e| match e.kind() {
|
) -> Vec<Result<(AbsoluteFilePath, AbsoluteFilePath), MvError>> {
|
||||||
std::io::ErrorKind::NotFound => MvError::NotFound,
|
async fn _mv(
|
||||||
_ => anyhow!(
|
fs: &FileSystem,
|
||||||
"Failed to move {} to {}: {e:?}",
|
path: AbsoluteFilePath,
|
||||||
request.path(),
|
target_path: &AbsoluteFilePath,
|
||||||
request.target_path()
|
) -> Result<(AbsoluteFilePath, AbsoluteFilePath), MvError> {
|
||||||
)
|
fs.mv(&path, target_path).await.map_err(|e| match e.kind() {
|
||||||
.into(),
|
std::io::ErrorKind::NotFound => MvError::NotFound(path),
|
||||||
|
_ => MvError::Unknown(
|
||||||
|
anyhow!("Failed to move {} to {}: {e:?}", path, target_path).into(),
|
||||||
|
),
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let (path_list, target_path) = request.unpack();
|
||||||
|
let paths = Vec::<AbsoluteFilePath>::from(path_list);
|
||||||
|
|
||||||
|
join_all(
|
||||||
|
paths
|
||||||
|
.into_iter()
|
||||||
|
.map(|path| _mv(&self, path, &target_path))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn touch(&self, request: TouchRequest) -> Result<(), TouchError> {
|
async fn touch(&self, request: TouchRequest) -> Result<(), TouchError> {
|
||||||
@@ -410,3 +572,178 @@ where
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn walk_dir<P>(dir: P) -> Result<Vec<PathBuf>, tokio::io::Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
let mut dirs = vec![dir.as_ref().to_owned()];
|
||||||
|
let mut files = vec![];
|
||||||
|
|
||||||
|
while !dirs.is_empty() {
|
||||||
|
let path = dirs.remove(0);
|
||||||
|
match tokio::fs::read_dir(path.clone()).await {
|
||||||
|
Ok(mut dir_iter) => {
|
||||||
|
while let Some(entry) = dir_iter.next_entry().await? {
|
||||||
|
let entry_path_buf = entry.path();
|
||||||
|
|
||||||
|
if entry_path_buf.is_dir() {
|
||||||
|
dirs.push(entry_path_buf);
|
||||||
|
} else {
|
||||||
|
files.push(entry_path_buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => match e.kind() {
|
||||||
|
std::io::ErrorKind::NotADirectory => files.push(path),
|
||||||
|
_ => return Err(e),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(files)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a ZIP archive from a directory
|
||||||
|
///
|
||||||
|
/// * `path`: The directory's path
|
||||||
|
/// * `tx`: The sender for the new ZIP archive's bytes
|
||||||
|
/// * `buffer_size`: The size of the file read buffer. A large buffer increases both the speed and the memory usage
|
||||||
|
async fn create_zip(
|
||||||
|
prefix: String,
|
||||||
|
paths: Vec<PathBuf>,
|
||||||
|
tx: std::sync::mpsc::Sender<Result<bytes::Bytes, std::io::Error>>,
|
||||||
|
buffer_size: usize,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let options = zip::write::SimpleFileOptions::default()
|
||||||
|
.compression_method(zip::CompressionMethod::Stored)
|
||||||
|
.compression_level(None)
|
||||||
|
.large_file(true)
|
||||||
|
.unix_permissions(0o644);
|
||||||
|
|
||||||
|
let mut file_buf = vec![0; buffer_size];
|
||||||
|
let mut zip = zip::write::ZipWriter::new_stream(ChannelWriter(tx));
|
||||||
|
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
for path in &paths {
|
||||||
|
entries.append(&mut walk_dir(&path).await?);
|
||||||
|
}
|
||||||
|
|
||||||
|
for entry_path_buf in entries {
|
||||||
|
let entry_path = entry_path_buf.as_path();
|
||||||
|
let entry_str = entry_path
|
||||||
|
.strip_prefix(&prefix)?
|
||||||
|
.to_str()
|
||||||
|
.context("Failed to get directory entry name")?;
|
||||||
|
|
||||||
|
if entry_path.is_dir() {
|
||||||
|
zip.add_directory(entry_str, options)?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
zip.start_file(entry_str, options)?;
|
||||||
|
let mut entry_file = tokio::fs::File::open(entry_path).await?;
|
||||||
|
|
||||||
|
while let Ok(len) = entry_file.read(&mut file_buf).await
|
||||||
|
&& len > 0
|
||||||
|
{
|
||||||
|
zip.write(&file_buf[..len])?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
zip.finish()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChannelWriter(std::sync::mpsc::Sender<Result<bytes::Bytes, std::io::Error>>);
|
||||||
|
|
||||||
|
impl Write for ChannelWriter {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||||
|
let len = buf.len();
|
||||||
|
let data = bytes::Bytes::copy_from_slice(buf);
|
||||||
|
|
||||||
|
self.0
|
||||||
|
.send(Ok(data))
|
||||||
|
.map(|_| len)
|
||||||
|
.map_err(|_| std::io::ErrorKind::Other.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> std::io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PathRequest {
|
||||||
|
Single(FilePath),
|
||||||
|
Multiple {
|
||||||
|
lowest_common_prefix: String,
|
||||||
|
paths: Vec<FilePath>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PathRequest {
|
||||||
|
fn from_paths(paths: Vec<FilePath>) -> anyhow::Result<Self> {
|
||||||
|
let mut input_paths: Vec<FilePath> = HashSet::<FilePath>::from_iter(paths.into_iter())
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if input_paths.len() == 1 {
|
||||||
|
return Ok(Self::Single(input_paths.pop().unwrap()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut lowest_common_prefix = input_paths
|
||||||
|
.first()
|
||||||
|
.expect("paths to contain at least 1 entry")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
for path in &input_paths {
|
||||||
|
let chars = lowest_common_prefix
|
||||||
|
.chars()
|
||||||
|
.zip(path.as_str().chars())
|
||||||
|
.enumerate();
|
||||||
|
|
||||||
|
for (index, (a, b)) in chars {
|
||||||
|
if a != b {
|
||||||
|
lowest_common_prefix = lowest_common_prefix[..index].to_string();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let lowest_common_prefix = if let Some(last_slash_index) = lowest_common_prefix.rfind("/")
|
||||||
|
&& last_slash_index > 1
|
||||||
|
{
|
||||||
|
lowest_common_prefix.truncate(last_slash_index);
|
||||||
|
lowest_common_prefix
|
||||||
|
} else {
|
||||||
|
lowest_common_prefix
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self::Multiple {
|
||||||
|
lowest_common_prefix,
|
||||||
|
paths: input_paths,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn file_size_recursive<P>(path: P) -> io::Result<u64>
|
||||||
|
where
|
||||||
|
P: AsRef<Path> + std::fmt::Debug,
|
||||||
|
{
|
||||||
|
let metadata = fs::metadata(&path).await?;
|
||||||
|
|
||||||
|
if !metadata.is_dir() {
|
||||||
|
return Ok(metadata.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut entries = fs::read_dir(path).await?;
|
||||||
|
|
||||||
|
let mut total = metadata.size();
|
||||||
|
|
||||||
|
while let Some(entry) = entries.next_entry().await? {
|
||||||
|
total += Box::pin(file_size_recursive(entry.path())).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(total)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::domain::{
|
use crate::domain::{
|
||||||
oidc::ports::OidcMetrics,
|
oidc::ports::OidcMetrics,
|
||||||
warren::ports::{AuthMetrics, FileSystemMetrics, WarrenMetrics},
|
warren::ports::{AuthMetrics, FileSystemMetrics, OptionMetrics, WarrenMetrics},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
@@ -151,6 +151,13 @@ impl WarrenMetrics for MetricsDebugLogger {
|
|||||||
async fn record_warren_share_cat_failure(&self) {
|
async fn record_warren_share_cat_failure(&self) {
|
||||||
tracing::debug!("[Metrics] Warren share cat failed");
|
tracing::debug!("[Metrics] Warren share cat failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn record_warren_share_password_verification_success(&self) {
|
||||||
|
tracing::debug!("[Metrics] Warren share password verification succeeded");
|
||||||
|
}
|
||||||
|
async fn record_warren_share_password_verification_failure(&self) {
|
||||||
|
tracing::debug!("[Metrics] Warren share password verification failed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileSystemMetrics for MetricsDebugLogger {
|
impl FileSystemMetrics for MetricsDebugLogger {
|
||||||
@@ -445,3 +452,26 @@ impl OidcMetrics for MetricsDebugLogger {
|
|||||||
tracing::debug!("[Metrics] OIDC get user info failed");
|
tracing::debug!("[Metrics] OIDC get user info failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl OptionMetrics for MetricsDebugLogger {
|
||||||
|
async fn record_option_get_success(&self) {
|
||||||
|
tracing::debug!("[Metrics] Get option succeeded");
|
||||||
|
}
|
||||||
|
async fn record_option_get_failure(&self) {
|
||||||
|
tracing::debug!("[Metrics] Get option failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn record_option_set_success(&self) {
|
||||||
|
tracing::debug!("[Metrics] Set option succeeded");
|
||||||
|
}
|
||||||
|
async fn record_option_set_failure(&self) {
|
||||||
|
tracing::debug!("[Metrics] Set option failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn record_option_delete_success(&self) {
|
||||||
|
tracing::debug!("[Metrics] Delete option succeeded");
|
||||||
|
}
|
||||||
|
async fn record_option_delete_failure(&self) {
|
||||||
|
tracing::debug!("[Metrics] Delete option failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ pub mod file_system;
|
|||||||
pub mod metrics_debug_logger;
|
pub mod metrics_debug_logger;
|
||||||
pub mod notifier_debug_logger;
|
pub mod notifier_debug_logger;
|
||||||
pub mod oidc;
|
pub mod oidc;
|
||||||
pub mod postgres;
|
pub mod sqlite;
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ use crate::domain::{
|
|||||||
warren::{
|
warren::{
|
||||||
models::{
|
models::{
|
||||||
auth_session::requests::FetchAuthSessionResponse,
|
auth_session::requests::FetchAuthSessionResponse,
|
||||||
file::{AbsoluteFilePath, LsResponse},
|
file::{AbsoluteFilePath, AbsoluteFilePathList, LsResponse},
|
||||||
|
option::{DeleteOptionResponse, GetOptionResponse, OptionType, SetOptionResponse},
|
||||||
share::{
|
share::{
|
||||||
CreateShareResponse, DeleteShareResponse, GetShareResponse, ListSharesResponse,
|
CreateShareResponse, DeleteShareResponse, GetShareResponse, ListSharesResponse,
|
||||||
ShareCatResponse, ShareLsResponse,
|
ShareCatResponse, ShareLsResponse, VerifySharePasswordResponse,
|
||||||
},
|
},
|
||||||
user::{
|
user::{
|
||||||
ListAllUsersAndWarrensResponse, LoginUserOidcResponse, LoginUserResponse, User,
|
ListAllUsersAndWarrensResponse, LoginUserOidcResponse, LoginUserResponse, User,
|
||||||
@@ -22,7 +23,7 @@ use crate::domain::{
|
|||||||
WarrenRmResponse, WarrenSaveResponse, WarrenTouchResponse,
|
WarrenRmResponse, WarrenSaveResponse, WarrenTouchResponse,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ports::{AuthNotifier, FileSystemNotifier, WarrenNotifier},
|
ports::{AuthNotifier, FileSystemNotifier, OptionNotifier, WarrenNotifier},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -58,10 +59,10 @@ impl WarrenNotifier for NotifierDebugLogger {
|
|||||||
tracing::debug!("[Notifier] Fetched warren {}", warren.name());
|
tracing::debug!("[Notifier] Fetched warren {}", warren.name());
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn warren_cat(&self, warren: &Warren, path: &AbsoluteFilePath) {
|
async fn warren_cat(&self, warren: &Warren, paths: &AbsoluteFilePathList) {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"[Notifier] Fetched file {} in warren {}",
|
"[Notifier] Fetched {} file(s) in warren {}",
|
||||||
path,
|
paths.paths().len(),
|
||||||
warren.name(),
|
warren.name(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -87,20 +88,51 @@ impl WarrenNotifier for NotifierDebugLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn warren_rm(&self, response: &WarrenRmResponse) {
|
async fn warren_rm(&self, response: &WarrenRmResponse) {
|
||||||
tracing::debug!(
|
let span = tracing::debug_span!("warren_rm", "{}", response.warren().name()).entered();
|
||||||
"[Notifier] Deleted file {} from warren {}",
|
|
||||||
response.path(),
|
let results = response.results();
|
||||||
response.warren().name(),
|
|
||||||
);
|
for result in results {
|
||||||
|
match result.as_ref() {
|
||||||
|
Ok(path) => tracing::debug!("Deleted file: {path}"),
|
||||||
|
Err(e) => match e {
|
||||||
|
crate::domain::warren::models::file::RmError::NotFound(path) => {
|
||||||
|
tracing::debug!("File not found: {path}")
|
||||||
|
}
|
||||||
|
crate::domain::warren::models::file::RmError::NotEmpty(path) => {
|
||||||
|
tracing::debug!("Directory not empty: {path}")
|
||||||
|
}
|
||||||
|
crate::domain::warren::models::file::RmError::Unknown(_) => (),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn warren_mv(&self, response: &WarrenMvResponse) {
|
async fn warren_mv(&self, response: &WarrenMvResponse) {
|
||||||
tracing::debug!(
|
let span = tracing::debug_span!("warren_mv", "{}", response.warren().name()).entered();
|
||||||
"[Notifier] Renamed file {} to {} in warren {}",
|
|
||||||
response.old_path(),
|
let results = response.results();
|
||||||
response.path(),
|
|
||||||
response.warren().name(),
|
for result in results {
|
||||||
);
|
match result.as_ref() {
|
||||||
|
Ok((old_path, new_path)) => {
|
||||||
|
tracing::debug!("Moved file {old_path} to {new_path}")
|
||||||
|
}
|
||||||
|
Err(e) => match e {
|
||||||
|
crate::domain::warren::models::file::MvError::NotFound(path) => {
|
||||||
|
tracing::debug!("File not found: {path}")
|
||||||
|
}
|
||||||
|
crate::domain::warren::models::file::MvError::AlreadyExists(path) => {
|
||||||
|
tracing::debug!("File already exists: {path}")
|
||||||
|
}
|
||||||
|
crate::domain::warren::models::file::MvError::Unknown(_) => (),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn warren_touch(&self, warren: &Warren, path: &AbsoluteFilePath) {
|
async fn warren_touch(&self, warren: &Warren, path: &AbsoluteFilePath) {
|
||||||
@@ -165,11 +197,18 @@ impl WarrenNotifier for NotifierDebugLogger {
|
|||||||
|
|
||||||
async fn warren_share_cat(&self, response: &ShareCatResponse) {
|
async fn warren_share_cat(&self, response: &ShareCatResponse) {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"[Notifier] Fetched file {} from share {}",
|
"[Notifier] Fetched {} file(s) from share {}",
|
||||||
response.path(),
|
response.paths().paths().len(),
|
||||||
response.share().id(),
|
response.share().id(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn warren_share_password_verified(&self, response: &VerifySharePasswordResponse) {
|
||||||
|
tracing::debug!(
|
||||||
|
"[Notifier] Verified password for share {}",
|
||||||
|
response.share().id()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileSystemNotifier for NotifierDebugLogger {
|
impl FileSystemNotifier for NotifierDebugLogger {
|
||||||
@@ -177,8 +216,8 @@ impl FileSystemNotifier for NotifierDebugLogger {
|
|||||||
tracing::debug!("[Notifier] Listed {} file(s)", response.files().len());
|
tracing::debug!("[Notifier] Listed {} file(s)", response.files().len());
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn cat(&self, path: &AbsoluteFilePath) {
|
async fn cat(&self, paths: &AbsoluteFilePathList) {
|
||||||
tracing::debug!("[Notifier] Fetched file {path}");
|
tracing::debug!("[Notifier] Fetched {} file(s)", paths.paths().len());
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn mkdir(&self, path: &AbsoluteFilePath) {
|
async fn mkdir(&self, path: &AbsoluteFilePath) {
|
||||||
@@ -356,10 +395,11 @@ impl AuthNotifier for NotifierDebugLogger {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn auth_warren_cat(&self, user: &User, warren_id: &Uuid, path: &AbsoluteFilePath) {
|
async fn auth_warren_cat(&self, user: &User, warren_id: &Uuid, paths: &AbsoluteFilePathList) {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"[Notifier] User {} fetched file {path} in warren {warren_id}",
|
"[Notifier] User {} fetched {} file(s) in warren {warren_id}",
|
||||||
user.id(),
|
user.id(),
|
||||||
|
paths.paths().len(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,19 +431,22 @@ impl AuthNotifier for NotifierDebugLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn auth_warren_rm(&self, user: &User, response: &WarrenRmResponse) {
|
async fn auth_warren_rm(&self, user: &User, response: &WarrenRmResponse) {
|
||||||
|
let results = response.results();
|
||||||
|
let successes = results.iter().filter(|r| r.is_ok()).count();
|
||||||
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"[Notifier] Deleted file {} from warren {} for authenticated user {}",
|
"[Notifier] Deleted {successes} file(s) from warren {} for authenticated user {}",
|
||||||
response.path(),
|
|
||||||
response.warren().name(),
|
response.warren().name(),
|
||||||
user.id(),
|
user.id(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn auth_warren_mv(&self, user: &User, response: &WarrenMvResponse) {
|
async fn auth_warren_mv(&self, user: &User, response: &WarrenMvResponse) {
|
||||||
|
let results = response.results();
|
||||||
|
let successes = results.iter().filter(|r| r.is_ok()).count();
|
||||||
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"[Notifier] Renamed file {} to {} in warren {} for authenticated user {}",
|
"[Notifier] Moved {successes} file(s) in warren {} for authenticated user {}",
|
||||||
response.old_path(),
|
|
||||||
response.path(),
|
|
||||||
response.warren().name(),
|
response.warren().name(),
|
||||||
user.id(),
|
user.id(),
|
||||||
);
|
);
|
||||||
@@ -473,3 +516,25 @@ impl OidcNotifier for NotifierDebugLogger {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl OptionNotifier for NotifierDebugLogger {
|
||||||
|
async fn got_option<T: OptionType>(&self, response: &GetOptionResponse<T>) {
|
||||||
|
tracing::debug!(
|
||||||
|
"[Notifier] Got option {}: {}",
|
||||||
|
response.key().to_string(),
|
||||||
|
response.value().to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_option<T: OptionType>(&self, response: &SetOptionResponse<T>) {
|
||||||
|
tracing::debug!(
|
||||||
|
"[Notifier] Set option {} to {}",
|
||||||
|
response.key().to_string(),
|
||||||
|
response.value().to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn deleted_option(&self, response: &DeleteOptionResponse) {
|
||||||
|
tracing::debug!("[Notifier] Deleted option {}", response.key().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
use std::{str::FromStr as _, time::Duration};
|
|
||||||
|
|
||||||
use anyhow::Context as _;
|
|
||||||
use sqlx::{
|
|
||||||
ConnectOptions as _, Connection as _, PgConnection, PgPool,
|
|
||||||
postgres::{PgConnectOptions, PgPoolOptions},
|
|
||||||
};
|
|
||||||
use tokio::task::JoinHandle;
|
|
||||||
pub mod auth;
|
|
||||||
pub mod share;
|
|
||||||
pub mod warrens;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct PostgresConfig {
|
|
||||||
database_url: String,
|
|
||||||
database_name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PostgresConfig {
|
|
||||||
pub fn new(database_url: String, database_name: String) -> Self {
|
|
||||||
Self {
|
|
||||||
database_url,
|
|
||||||
database_name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Postgres {
|
|
||||||
pool: PgPool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Postgres {
|
|
||||||
pub async fn new(config: PostgresConfig) -> anyhow::Result<Self> {
|
|
||||||
let opts = PgConnectOptions::from_str(&config.database_url)?.disable_statement_logging();
|
|
||||||
|
|
||||||
let mut connection = PgConnection::connect_with(&opts)
|
|
||||||
.await
|
|
||||||
.context("Failed to connect to the PostgreSQL database")?;
|
|
||||||
|
|
||||||
match sqlx::query("SELECT datname FROM pg_database WHERE datname = $1")
|
|
||||||
.bind(&config.database_name)
|
|
||||||
.fetch_one(&mut connection)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(_) => (),
|
|
||||||
Err(sqlx::Error::RowNotFound) => {
|
|
||||||
sqlx::query(&format!("CREATE DATABASE {}", config.database_name))
|
|
||||||
.execute(&mut connection)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
Err(e) => return Err(e.into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
connection.close().await?;
|
|
||||||
|
|
||||||
let pool = PgPoolOptions::new()
|
|
||||||
.connect_with(opts.database(&config.database_name))
|
|
||||||
.await?;
|
|
||||||
sqlx::migrate!("./migrations").run(&pool).await?;
|
|
||||||
|
|
||||||
// 3600 seconds = 1 hour
|
|
||||||
Self::start_cleanup_tasks(pool.clone(), Duration::from_secs(3600));
|
|
||||||
|
|
||||||
Ok(Self { pool })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn start_cleanup_tasks(pool: PgPool, interval: Duration) -> JoinHandle<()> {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
{
|
|
||||||
let Ok(mut connection) = pool.acquire().await else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Ok(count) = Self::delete_expired_auth_sessions(&mut connection).await {
|
|
||||||
tracing::debug!("Removed {count} expired auth session(s)");
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(count) = Self::delete_expired_shares(&mut connection).await {
|
|
||||||
tracing::debug!("Deleted {count} expired share(s)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tokio::time::sleep(interval).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::debug!("Session cleanup task stopped");
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn is_not_found_error(err: &sqlx::Error) -> bool {
|
|
||||||
matches!(err, sqlx::Error::RowNotFound)
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ use argon2::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use sqlx::{Acquire as _, PgConnection};
|
use sqlx::{Acquire as _, SqliteConnection};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::domain::warren::{
|
use crate::domain::warren::{
|
||||||
@@ -40,15 +40,15 @@ use crate::domain::warren::{
|
|||||||
ports::{AuthRepository, WarrenService},
|
ports::{AuthRepository, WarrenService},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Postgres, is_not_found_error};
|
use super::{Sqlite, is_not_found_error};
|
||||||
|
|
||||||
impl AuthRepository for Postgres {
|
impl AuthRepository for Sqlite {
|
||||||
async fn create_user(&self, request: CreateUserRequest) -> Result<User, CreateUserError> {
|
async fn create_user(&self, request: CreateUserRequest) -> Result<User, CreateUserError> {
|
||||||
let mut connection = self
|
let mut connection = self
|
||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let user = self
|
let user = self
|
||||||
.create_user(
|
.create_user(
|
||||||
@@ -72,7 +72,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let user = self
|
let user = self
|
||||||
.create_or_update_user(
|
.create_or_update_user(
|
||||||
@@ -93,7 +93,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let user = self
|
let user = self
|
||||||
.edit_user(
|
.edit_user(
|
||||||
@@ -115,7 +115,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
self.delete_user_from_database(&mut connection, request.user_id())
|
self.delete_user_from_database(&mut connection, request.user_id())
|
||||||
.await
|
.await
|
||||||
@@ -136,7 +136,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let user = self
|
let user = self
|
||||||
.get_user_from_email(&mut connection, request.email())
|
.get_user_from_email(&mut connection, request.email())
|
||||||
@@ -166,7 +166,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let session = self
|
let session = self
|
||||||
.create_session(&mut connection, request.user(), request.expiration())
|
.create_session(&mut connection, request.user(), request.expiration())
|
||||||
@@ -184,7 +184,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let session = self
|
let session = self
|
||||||
.get_auth_session(&mut connection, request.session_id())
|
.get_auth_session(&mut connection, request.session_id())
|
||||||
@@ -212,7 +212,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let user_warren = self
|
let user_warren = self
|
||||||
.add_user_to_warren(&mut connection, request.user_warren())
|
.add_user_to_warren(&mut connection, request.user_warren())
|
||||||
@@ -230,7 +230,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let user_warren = self
|
let user_warren = self
|
||||||
.update_user_warren(&mut connection, request.user_warren())
|
.update_user_warren(&mut connection, request.user_warren())
|
||||||
@@ -248,7 +248,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let user_warren = self
|
let user_warren = self
|
||||||
.remove_user_from_warren(&mut connection, request.user_id(), request.warren_id())
|
.remove_user_from_warren(&mut connection, request.user_id(), request.warren_id())
|
||||||
@@ -272,7 +272,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let user_warrens = self
|
let user_warrens = self
|
||||||
.get_user_warrens(&mut connection, request.user_id())
|
.get_user_warrens(&mut connection, request.user_id())
|
||||||
@@ -290,7 +290,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let user_warrens = self
|
let user_warrens = self
|
||||||
.get_all_user_warrens(&mut connection)
|
.get_all_user_warrens(&mut connection)
|
||||||
@@ -308,7 +308,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
self.get_user_warren(&mut connection, request.user_id(), request.warren_id())
|
self.get_user_warren(&mut connection, request.user_id(), request.warren_id())
|
||||||
.await
|
.await
|
||||||
@@ -326,7 +326,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let users = self
|
let users = self
|
||||||
.fetch_users(&mut connection)
|
.fetch_users(&mut connection)
|
||||||
@@ -345,7 +345,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let users = self
|
let users = self
|
||||||
.fetch_users(&mut connection)
|
.fetch_users(&mut connection)
|
||||||
@@ -368,9 +368,9 @@ impl AuthRepository for Postgres {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Postgres {
|
impl Sqlite {
|
||||||
pub(super) async fn delete_expired_auth_sessions(
|
pub(super) async fn delete_expired_auth_sessions(
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
) -> Result<u64, sqlx::Error> {
|
) -> Result<u64, sqlx::Error> {
|
||||||
let delete_count = sqlx::query(
|
let delete_count = sqlx::query(
|
||||||
"
|
"
|
||||||
@@ -389,7 +389,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn create_user(
|
async fn create_user(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
name: &UserName,
|
name: &UserName,
|
||||||
email: &UserEmail,
|
email: &UserEmail,
|
||||||
password: &UserPassword,
|
password: &UserPassword,
|
||||||
@@ -402,6 +402,7 @@ impl Postgres {
|
|||||||
|
|
||||||
let user: User = sqlx::query_as(
|
let user: User = sqlx::query_as(
|
||||||
"INSERT INTO users (
|
"INSERT INTO users (
|
||||||
|
id,
|
||||||
name,
|
name,
|
||||||
email,
|
email,
|
||||||
hash,
|
hash,
|
||||||
@@ -411,12 +412,14 @@ impl Postgres {
|
|||||||
$1,
|
$1,
|
||||||
$2,
|
$2,
|
||||||
$3,
|
$3,
|
||||||
$4
|
$4,
|
||||||
|
$5
|
||||||
)
|
)
|
||||||
RETURNING
|
RETURNING
|
||||||
*
|
*
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
|
.bind(Uuid::new_v4())
|
||||||
.bind(name)
|
.bind(name)
|
||||||
.bind(email)
|
.bind(email)
|
||||||
.bind(password_hash)
|
.bind(password_hash)
|
||||||
@@ -431,7 +434,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn create_or_update_user(
|
async fn create_or_update_user(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
sub: &String,
|
sub: &String,
|
||||||
name: &UserName,
|
name: &UserName,
|
||||||
email: &UserEmail,
|
email: &UserEmail,
|
||||||
@@ -546,7 +549,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn edit_user(
|
async fn edit_user(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
id: &Uuid,
|
id: &Uuid,
|
||||||
name: &UserName,
|
name: &UserName,
|
||||||
email: &UserEmail,
|
email: &UserEmail,
|
||||||
@@ -592,7 +595,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn delete_user_sessions(
|
async fn delete_user_sessions(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
user_id: &Uuid,
|
user_id: &Uuid,
|
||||||
) -> Result<u64, sqlx::Error> {
|
) -> Result<u64, sqlx::Error> {
|
||||||
let rows_affected = sqlx::query(
|
let rows_affected = sqlx::query(
|
||||||
@@ -613,7 +616,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn delete_user_from_database(
|
async fn delete_user_from_database(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
user_id: &Uuid,
|
user_id: &Uuid,
|
||||||
) -> Result<User, sqlx::Error> {
|
) -> Result<User, sqlx::Error> {
|
||||||
let user: User = sqlx::query_as(
|
let user: User = sqlx::query_as(
|
||||||
@@ -635,7 +638,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn get_user_from_id(
|
async fn get_user_from_id(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
id: &Uuid,
|
id: &Uuid,
|
||||||
) -> Result<User, sqlx::Error> {
|
) -> Result<User, sqlx::Error> {
|
||||||
let user: User = sqlx::query_as(
|
let user: User = sqlx::query_as(
|
||||||
@@ -657,7 +660,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn get_user_from_email(
|
async fn get_user_from_email(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
email: &UserEmail,
|
email: &UserEmail,
|
||||||
) -> Result<User, sqlx::Error> {
|
) -> Result<User, sqlx::Error> {
|
||||||
let user: User = sqlx::query_as(
|
let user: User = sqlx::query_as(
|
||||||
@@ -698,7 +701,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn create_session(
|
async fn create_session(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
user: &User,
|
user: &User,
|
||||||
expiration: &SessionExpirationTime,
|
expiration: &SessionExpirationTime,
|
||||||
) -> anyhow::Result<AuthSession> {
|
) -> anyhow::Result<AuthSession> {
|
||||||
@@ -721,7 +724,7 @@ impl Postgres {
|
|||||||
) VALUES (
|
) VALUES (
|
||||||
$1,
|
$1,
|
||||||
$2,
|
$2,
|
||||||
TO_TIMESTAMP($3::double precision / 1000)
|
datetime($3, 'unixepoch')
|
||||||
)
|
)
|
||||||
RETURNING
|
RETURNING
|
||||||
*
|
*
|
||||||
@@ -729,7 +732,7 @@ impl Postgres {
|
|||||||
)
|
)
|
||||||
.bind(session_id)
|
.bind(session_id)
|
||||||
.bind(user.id())
|
.bind(user.id())
|
||||||
.bind(expiration_time)
|
.bind(expiration_time / 1000)
|
||||||
.fetch_one(&mut *tx)
|
.fetch_one(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -740,7 +743,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn get_auth_session(
|
async fn get_auth_session(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
session_id: &AuthSessionId,
|
session_id: &AuthSessionId,
|
||||||
) -> Result<AuthSession, sqlx::Error> {
|
) -> Result<AuthSession, sqlx::Error> {
|
||||||
let session: AuthSession = sqlx::query_as(
|
let session: AuthSession = sqlx::query_as(
|
||||||
@@ -762,7 +765,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn get_user_warrens(
|
async fn get_user_warrens(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
user_id: &Uuid,
|
user_id: &Uuid,
|
||||||
) -> Result<Vec<UserWarren>, sqlx::Error> {
|
) -> Result<Vec<UserWarren>, sqlx::Error> {
|
||||||
let user_warrens: Vec<UserWarren> = sqlx::query_as(
|
let user_warrens: Vec<UserWarren> = sqlx::query_as(
|
||||||
@@ -784,7 +787,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn get_all_user_warrens(
|
async fn get_all_user_warrens(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
) -> Result<Vec<UserWarren>, sqlx::Error> {
|
) -> Result<Vec<UserWarren>, sqlx::Error> {
|
||||||
let user_warrens: Vec<UserWarren> = sqlx::query_as(
|
let user_warrens: Vec<UserWarren> = sqlx::query_as(
|
||||||
"
|
"
|
||||||
@@ -802,7 +805,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn get_user_warren(
|
async fn get_user_warren(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
user_id: &Uuid,
|
user_id: &Uuid,
|
||||||
warren_id: &Uuid,
|
warren_id: &Uuid,
|
||||||
) -> Result<UserWarren, sqlx::Error> {
|
) -> Result<UserWarren, sqlx::Error> {
|
||||||
@@ -825,7 +828,10 @@ impl Postgres {
|
|||||||
Ok(ids)
|
Ok(ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_users(&self, connection: &mut PgConnection) -> Result<Vec<User>, sqlx::Error> {
|
async fn fetch_users(
|
||||||
|
&self,
|
||||||
|
connection: &mut SqliteConnection,
|
||||||
|
) -> Result<Vec<User>, sqlx::Error> {
|
||||||
let users: Vec<User> = sqlx::query_as(
|
let users: Vec<User> = sqlx::query_as(
|
||||||
"
|
"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -844,7 +850,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn add_user_to_warren(
|
async fn add_user_to_warren(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
user_warren: &UserWarren,
|
user_warren: &UserWarren,
|
||||||
) -> Result<UserWarren, sqlx::Error> {
|
) -> Result<UserWarren, sqlx::Error> {
|
||||||
let user_warren: UserWarren = sqlx::query_as(
|
let user_warren: UserWarren = sqlx::query_as(
|
||||||
@@ -855,14 +861,22 @@ impl Postgres {
|
|||||||
can_list_files,
|
can_list_files,
|
||||||
can_read_files,
|
can_read_files,
|
||||||
can_modify_files,
|
can_modify_files,
|
||||||
can_delete_files
|
can_delete_files,
|
||||||
|
can_list_shares,
|
||||||
|
can_create_shares,
|
||||||
|
can_modify_shares,
|
||||||
|
can_delete_shares
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1,
|
$1,
|
||||||
$2,
|
$2,
|
||||||
$3,
|
$3,
|
||||||
$4,
|
$4,
|
||||||
$5,
|
$5,
|
||||||
$6
|
$6,
|
||||||
|
$7,
|
||||||
|
$8,
|
||||||
|
$9,
|
||||||
|
$10
|
||||||
)
|
)
|
||||||
RETURNING
|
RETURNING
|
||||||
*
|
*
|
||||||
@@ -874,6 +888,10 @@ impl Postgres {
|
|||||||
.bind(user_warren.can_read_files())
|
.bind(user_warren.can_read_files())
|
||||||
.bind(user_warren.can_modify_files())
|
.bind(user_warren.can_modify_files())
|
||||||
.bind(user_warren.can_delete_files())
|
.bind(user_warren.can_delete_files())
|
||||||
|
.bind(user_warren.can_list_shares())
|
||||||
|
.bind(user_warren.can_create_shares())
|
||||||
|
.bind(user_warren.can_modify_shares())
|
||||||
|
.bind(user_warren.can_delete_shares())
|
||||||
.fetch_one(connection)
|
.fetch_one(connection)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -882,7 +900,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn update_user_warren(
|
async fn update_user_warren(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
user_warren: &UserWarren,
|
user_warren: &UserWarren,
|
||||||
) -> Result<UserWarren, sqlx::Error> {
|
) -> Result<UserWarren, sqlx::Error> {
|
||||||
let user_warren: UserWarren = sqlx::query_as(
|
let user_warren: UserWarren = sqlx::query_as(
|
||||||
@@ -923,7 +941,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn remove_user_from_warren(
|
async fn remove_user_from_warren(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
user_id: &Uuid,
|
user_id: &Uuid,
|
||||||
warren_id: &Uuid,
|
warren_id: &Uuid,
|
||||||
) -> Result<UserWarren, sqlx::Error> {
|
) -> Result<UserWarren, sqlx::Error> {
|
||||||
72
backend/src/lib/outbound/sqlite/mod.rs
Normal file
72
backend/src/lib/outbound/sqlite/mod.rs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
use std::{str::FromStr as _, time::Duration};
|
||||||
|
|
||||||
|
use sqlx::{
|
||||||
|
ConnectOptions as _, SqlitePool,
|
||||||
|
sqlite::{SqliteConnectOptions, SqlitePoolOptions},
|
||||||
|
};
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
|
pub mod auth;
|
||||||
|
pub mod options;
|
||||||
|
pub mod share;
|
||||||
|
pub mod warrens;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SqliteConfig {
|
||||||
|
database_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SqliteConfig {
|
||||||
|
pub fn new(database_url: String) -> Self {
|
||||||
|
Self { database_url }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Sqlite {
|
||||||
|
pool: SqlitePool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sqlite {
|
||||||
|
pub async fn new(config: SqliteConfig) -> anyhow::Result<Self> {
|
||||||
|
let opts = SqliteConnectOptions::from_str(&config.database_url)?
|
||||||
|
.create_if_missing(true)
|
||||||
|
.read_only(false)
|
||||||
|
.disable_statement_logging();
|
||||||
|
|
||||||
|
let pool = SqlitePoolOptions::new().connect_with(opts).await?;
|
||||||
|
sqlx::migrate!("./migrations").run(&pool).await?;
|
||||||
|
|
||||||
|
// 3600 seconds = 1 hour
|
||||||
|
Self::start_cleanup_tasks(pool.clone(), Duration::from_secs(3600));
|
||||||
|
|
||||||
|
Ok(Self { pool })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn start_cleanup_tasks(pool: SqlitePool, interval: Duration) -> JoinHandle<()> {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
{
|
||||||
|
let Ok(mut connection) = pool.acquire().await else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(count) = Self::delete_expired_auth_sessions(&mut connection).await {
|
||||||
|
tracing::debug!("Removed {count} expired auth session(s)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(count) = Self::delete_expired_shares(&mut connection).await {
|
||||||
|
tracing::debug!("Deleted {count} expired share(s)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::time::sleep(interval).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::debug!("Session cleanup task stopped");
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn is_not_found_error(err: &sqlx::Error) -> bool {
|
||||||
|
matches!(err, sqlx::Error::RowNotFound)
|
||||||
|
}
|
||||||
134
backend/src/lib/outbound/sqlite/options.rs
Normal file
134
backend/src/lib/outbound/sqlite/options.rs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
use anyhow::Context;
|
||||||
|
use sqlx::FromRow;
|
||||||
|
|
||||||
|
use crate::domain::warren::{
|
||||||
|
models::option::{
|
||||||
|
DeleteOptionError, DeleteOptionRequest, DeleteOptionResponse, GetOptionError,
|
||||||
|
GetOptionRequest, GetOptionResponse, OptionKey, OptionType, SetOptionError,
|
||||||
|
SetOptionRequest, SetOptionResponse,
|
||||||
|
},
|
||||||
|
ports::OptionRepository,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{Sqlite, is_not_found_error};
|
||||||
|
|
||||||
|
#[derive(Debug, FromRow)]
|
||||||
|
struct OptionRow {
|
||||||
|
key: String,
|
||||||
|
value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OptionRepository for Sqlite {
|
||||||
|
async fn get_option<T: OptionType>(
|
||||||
|
&self,
|
||||||
|
request: GetOptionRequest,
|
||||||
|
) -> Result<GetOptionResponse<T>, GetOptionError> {
|
||||||
|
let mut connection = self
|
||||||
|
.pool
|
||||||
|
.acquire()
|
||||||
|
.await
|
||||||
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
|
let key: OptionKey = request.into();
|
||||||
|
|
||||||
|
let row: OptionRow = sqlx::query_as(
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
FROM
|
||||||
|
application_options
|
||||||
|
WHERE
|
||||||
|
key = $1",
|
||||||
|
)
|
||||||
|
.bind(key.as_str())
|
||||||
|
.fetch_one(&mut *connection)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
if is_not_found_error(&e) {
|
||||||
|
GetOptionError::NotFound(key)
|
||||||
|
} else {
|
||||||
|
GetOptionError::Unknown(e.into())
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let parsed = T::parse(&row.value).map_err(|_| GetOptionError::Parse)?;
|
||||||
|
|
||||||
|
Ok(GetOptionResponse::new(
|
||||||
|
OptionKey::new(&row.key).unwrap(),
|
||||||
|
parsed,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_option<T: OptionType>(
|
||||||
|
&self,
|
||||||
|
request: SetOptionRequest<T>,
|
||||||
|
) -> Result<SetOptionResponse<T>, SetOptionError> {
|
||||||
|
let mut connection = self
|
||||||
|
.pool
|
||||||
|
.acquire()
|
||||||
|
.await
|
||||||
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
|
let (key, value) = request.unpack();
|
||||||
|
|
||||||
|
sqlx::query_as::<_, OptionRow>(
|
||||||
|
"
|
||||||
|
INSERT INTO application_options (
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2
|
||||||
|
)
|
||||||
|
RETURNING
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.bind(key.as_str())
|
||||||
|
.bind(value.inner().to_string())
|
||||||
|
.fetch_one(&mut *connection)
|
||||||
|
.await
|
||||||
|
.map_err(|e| SetOptionError::Unknown(e.into()))?;
|
||||||
|
|
||||||
|
Ok(SetOptionResponse::new(key, value.get_inner()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_option(
|
||||||
|
&self,
|
||||||
|
request: DeleteOptionRequest,
|
||||||
|
) -> Result<DeleteOptionResponse, DeleteOptionError> {
|
||||||
|
let mut connection = self
|
||||||
|
.pool
|
||||||
|
.acquire()
|
||||||
|
.await
|
||||||
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
|
let key: OptionKey = request.into();
|
||||||
|
|
||||||
|
sqlx::query_as::<_, OptionRow>(
|
||||||
|
"
|
||||||
|
DELETE FROM
|
||||||
|
application_options
|
||||||
|
WHERE
|
||||||
|
key = $1
|
||||||
|
RETURNING
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.bind(key.as_str())
|
||||||
|
.fetch_one(&mut *connection)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
if is_not_found_error(&e) {
|
||||||
|
DeleteOptionError::NotFound(key.clone())
|
||||||
|
} else {
|
||||||
|
DeleteOptionError::Unknown(e.into())
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(DeleteOptionResponse::new(key))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use argon2::{
|
use argon2::{
|
||||||
Argon2, PasswordHash, PasswordVerifier as _,
|
Argon2, PasswordHash, PasswordVerifier as _,
|
||||||
password_hash::{PasswordHasher as _, SaltString, rand_core::OsRng},
|
password_hash::{PasswordHasher as _, SaltString},
|
||||||
};
|
};
|
||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
use sqlx::{Acquire as _, PgConnection};
|
use sqlx::{Acquire as _, SqliteConnection};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ use crate::domain::warren::models::{
|
|||||||
warren::HasWarrenId as _,
|
warren::HasWarrenId as _,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Postgres, is_not_found_error};
|
use super::{Sqlite, is_not_found_error};
|
||||||
|
|
||||||
#[derive(sqlx::FromRow)]
|
#[derive(sqlx::FromRow)]
|
||||||
struct ShareRow {
|
struct ShareRow {
|
||||||
@@ -62,7 +62,7 @@ impl TryFrom<ShareRow> for Share {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn get_share(
|
pub(super) async fn get_share(
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
request: GetShareRequest,
|
request: GetShareRequest,
|
||||||
) -> anyhow::Result<Share> {
|
) -> anyhow::Result<Share> {
|
||||||
let share_row: ShareRow = sqlx::query_as(
|
let share_row: ShareRow = sqlx::query_as(
|
||||||
@@ -90,7 +90,7 @@ pub(super) async fn get_share(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn list_shares(
|
pub(super) async fn list_shares(
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
request: ListSharesRequest,
|
request: ListSharesRequest,
|
||||||
) -> anyhow::Result<Vec<Share>> {
|
) -> anyhow::Result<Vec<Share>> {
|
||||||
let share_rows: Vec<ShareRow> = sqlx::query_as(
|
let share_rows: Vec<ShareRow> = sqlx::query_as(
|
||||||
@@ -107,7 +107,8 @@ pub(super) async fn list_shares(
|
|||||||
shares
|
shares
|
||||||
WHERE
|
WHERE
|
||||||
warren_id = $1 AND
|
warren_id = $1 AND
|
||||||
path = $2
|
path = $2 AND
|
||||||
|
(expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
|
||||||
ORDER BY
|
ORDER BY
|
||||||
created_at DESC
|
created_at DESC
|
||||||
",
|
",
|
||||||
@@ -126,13 +127,13 @@ pub(super) async fn list_shares(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn create_share(
|
pub(super) async fn create_share(
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
request: CreateShareRequest,
|
request: CreateShareRequest,
|
||||||
) -> anyhow::Result<Share> {
|
) -> anyhow::Result<Share> {
|
||||||
let mut tx = connection.begin().await?;
|
let mut tx = connection.begin().await?;
|
||||||
|
|
||||||
let password_hash = if let Some(password) = request.base().password() {
|
let password_hash = if let Some(password) = request.base().password() {
|
||||||
let salt = SaltString::generate(&mut OsRng);
|
let salt = SaltString::generate(&mut argon2::password_hash::rand_core::OsRng);
|
||||||
let argon2 = Argon2::default();
|
let argon2 = Argon2::default();
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
@@ -154,6 +155,7 @@ pub(super) async fn create_share(
|
|||||||
let share: ShareRow = sqlx::query_as(
|
let share: ShareRow = sqlx::query_as(
|
||||||
"
|
"
|
||||||
INSERT INTO shares (
|
INSERT INTO shares (
|
||||||
|
id,
|
||||||
creator_id,
|
creator_id,
|
||||||
warren_id,
|
warren_id,
|
||||||
path,
|
path,
|
||||||
@@ -164,17 +166,19 @@ pub(super) async fn create_share(
|
|||||||
$2,
|
$2,
|
||||||
$3,
|
$3,
|
||||||
$4,
|
$4,
|
||||||
TO_TIMESTAMP($5::double precision / 1000)
|
$5,
|
||||||
|
datetime($6, 'unixepoch')
|
||||||
)
|
)
|
||||||
RETURNING
|
RETURNING
|
||||||
*
|
*
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
|
.bind(Uuid::new_v4())
|
||||||
.bind(request.creator_id())
|
.bind(request.creator_id())
|
||||||
.bind(request.warren_id())
|
.bind(request.warren_id())
|
||||||
.bind(request.base().path())
|
.bind(request.base().path())
|
||||||
.bind(password_hash)
|
.bind(password_hash)
|
||||||
.bind(expires_at)
|
.bind(expires_at.map(|v| v / 1000))
|
||||||
.fetch_one(&mut *tx)
|
.fetch_one(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -184,7 +188,7 @@ pub(super) async fn create_share(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn delete_share(
|
pub(super) async fn delete_share(
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
request: DeleteShareRequest,
|
request: DeleteShareRequest,
|
||||||
) -> anyhow::Result<Share> {
|
) -> anyhow::Result<Share> {
|
||||||
let mut tx = connection.begin().await?;
|
let mut tx = connection.begin().await?;
|
||||||
@@ -209,7 +213,7 @@ pub(super) async fn delete_share(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn verify_password(
|
pub(super) async fn verify_password(
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
request: VerifySharePasswordRequest,
|
request: VerifySharePasswordRequest,
|
||||||
) -> Result<Share, VerifySharePasswordError> {
|
) -> Result<Share, VerifySharePasswordError> {
|
||||||
let share_row: ShareRow = sqlx::query_as(
|
let share_row: ShareRow = sqlx::query_as(
|
||||||
@@ -264,9 +268,9 @@ pub(super) async fn verify_password(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Postgres {
|
impl Sqlite {
|
||||||
pub(super) async fn delete_expired_shares(
|
pub(super) async fn delete_expired_shares(
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
) -> Result<u64, sqlx::Error> {
|
) -> Result<u64, sqlx::Error> {
|
||||||
let delete_count = sqlx::query(
|
let delete_count = sqlx::query(
|
||||||
"
|
"
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use anyhow::{Context as _, anyhow};
|
use anyhow::{Context as _, anyhow};
|
||||||
use sqlx::{Acquire as _, PgConnection};
|
use sqlx::{Acquire as _, SqliteConnection};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::domain::warren::{
|
use crate::domain::warren::{
|
||||||
@@ -21,9 +21,9 @@ use crate::domain::warren::{
|
|||||||
ports::WarrenRepository,
|
ports::WarrenRepository,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Postgres, is_not_found_error};
|
use super::{Sqlite, is_not_found_error};
|
||||||
|
|
||||||
impl WarrenRepository for Postgres {
|
impl WarrenRepository for Sqlite {
|
||||||
async fn create_warren(
|
async fn create_warren(
|
||||||
&self,
|
&self,
|
||||||
request: CreateWarrenRequest,
|
request: CreateWarrenRequest,
|
||||||
@@ -32,7 +32,7 @@ impl WarrenRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let warren = self
|
let warren = self
|
||||||
.create_warren(&mut connection, request.name(), request.path())
|
.create_warren(&mut connection, request.name(), request.path())
|
||||||
@@ -47,7 +47,7 @@ impl WarrenRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let warren = self
|
let warren = self
|
||||||
.edit_warren(
|
.edit_warren(
|
||||||
@@ -70,7 +70,7 @@ impl WarrenRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let warren = self
|
let warren = self
|
||||||
.delete_warren(&mut connection, request.id())
|
.delete_warren(&mut connection, request.id())
|
||||||
@@ -88,7 +88,7 @@ impl WarrenRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let warrens = self
|
let warrens = self
|
||||||
.fetch_warrens(&mut connection, request.ids())
|
.fetch_warrens(&mut connection, request.ids())
|
||||||
@@ -106,7 +106,7 @@ impl WarrenRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let warrens = self
|
let warrens = self
|
||||||
.fetch_all_warrens(&mut connection)
|
.fetch_all_warrens(&mut connection)
|
||||||
@@ -121,7 +121,7 @@ impl WarrenRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let warren = self
|
let warren = self
|
||||||
.get_warren(&mut connection, request.id())
|
.get_warren(&mut connection, request.id())
|
||||||
@@ -144,7 +144,7 @@ impl WarrenRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
super::share::get_share(&mut connection, request)
|
super::share::get_share(&mut connection, request)
|
||||||
.await
|
.await
|
||||||
@@ -159,7 +159,7 @@ impl WarrenRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
super::share::create_share(&mut connection, request)
|
super::share::create_share(&mut connection, request)
|
||||||
.await
|
.await
|
||||||
@@ -177,7 +177,7 @@ impl WarrenRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let path = request.path().clone();
|
let path = request.path().clone();
|
||||||
|
|
||||||
@@ -195,7 +195,7 @@ impl WarrenRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
super::share::delete_share(&mut connection, request)
|
super::share::delete_share(&mut connection, request)
|
||||||
.await
|
.await
|
||||||
@@ -211,7 +211,7 @@ impl WarrenRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
super::share::verify_password(&mut connection, request)
|
super::share::verify_password(&mut connection, request)
|
||||||
.await
|
.await
|
||||||
@@ -220,10 +220,10 @@ impl WarrenRepository for Postgres {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Postgres {
|
impl Sqlite {
|
||||||
async fn create_warren(
|
async fn create_warren(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
name: &WarrenName,
|
name: &WarrenName,
|
||||||
path: &AbsoluteFilePath,
|
path: &AbsoluteFilePath,
|
||||||
) -> Result<Warren, sqlx::Error> {
|
) -> Result<Warren, sqlx::Error> {
|
||||||
@@ -232,16 +232,19 @@ impl Postgres {
|
|||||||
let warren: Warren = sqlx::query_as(
|
let warren: Warren = sqlx::query_as(
|
||||||
"
|
"
|
||||||
INSERT INTO warrens (
|
INSERT INTO warrens (
|
||||||
|
id,
|
||||||
name,
|
name,
|
||||||
path
|
path
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1,
|
$1,
|
||||||
$2
|
$2,
|
||||||
|
$3
|
||||||
)
|
)
|
||||||
RETURNING
|
RETURNING
|
||||||
*
|
*
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
|
.bind(Uuid::new_v4())
|
||||||
.bind(name)
|
.bind(name)
|
||||||
.bind(path)
|
.bind(path)
|
||||||
.fetch_one(&mut *tx)
|
.fetch_one(&mut *tx)
|
||||||
@@ -254,7 +257,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn edit_warren(
|
async fn edit_warren(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
id: &Uuid,
|
id: &Uuid,
|
||||||
name: &WarrenName,
|
name: &WarrenName,
|
||||||
path: &AbsoluteFilePath,
|
path: &AbsoluteFilePath,
|
||||||
@@ -287,7 +290,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn delete_warren(
|
async fn delete_warren(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
id: &Uuid,
|
id: &Uuid,
|
||||||
) -> Result<Warren, sqlx::Error> {
|
) -> Result<Warren, sqlx::Error> {
|
||||||
let mut tx = connection.begin().await?;
|
let mut tx = connection.begin().await?;
|
||||||
@@ -313,7 +316,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn get_warren(
|
async fn get_warren(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
id: &Uuid,
|
id: &Uuid,
|
||||||
) -> Result<Warren, sqlx::Error> {
|
) -> Result<Warren, sqlx::Error> {
|
||||||
let warren: Warren = sqlx::query_as(
|
let warren: Warren = sqlx::query_as(
|
||||||
@@ -335,20 +338,28 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn fetch_warrens(
|
async fn fetch_warrens(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
ids: &[Uuid],
|
ids: &[Uuid],
|
||||||
) -> Result<Vec<Warren>, sqlx::Error> {
|
) -> Result<Vec<Warren>, sqlx::Error> {
|
||||||
let warrens: Vec<Warren> = sqlx::query_as::<sqlx::Postgres, Warren>(
|
let mut ids_as_string = ids.into_iter().fold(String::new(), |mut acc, id| {
|
||||||
|
let encoded = hex::encode(id.as_bytes());
|
||||||
|
acc.push_str("x'");
|
||||||
|
acc.push_str(encoded.as_str());
|
||||||
|
acc.push_str("',");
|
||||||
|
acc
|
||||||
|
});
|
||||||
|
ids_as_string.pop();
|
||||||
|
|
||||||
|
let warrens: Vec<Warren> = sqlx::query_as::<sqlx::Sqlite, Warren>(&format!(
|
||||||
"
|
"
|
||||||
SELECT
|
SELECT
|
||||||
*
|
*
|
||||||
FROM
|
FROM
|
||||||
warrens
|
warrens
|
||||||
WHERE
|
WHERE
|
||||||
id = ANY($1)
|
id IN ({ids_as_string})
|
||||||
",
|
",
|
||||||
)
|
))
|
||||||
.bind(ids)
|
|
||||||
.fetch_all(&mut *connection)
|
.fetch_all(&mut *connection)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -357,9 +368,9 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn fetch_all_warrens(
|
async fn fetch_all_warrens(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
) -> Result<Vec<Warren>, sqlx::Error> {
|
) -> Result<Vec<Warren>, sqlx::Error> {
|
||||||
let warrens: Vec<Warren> = sqlx::query_as::<sqlx::Postgres, Warren>(
|
let warrens: Vec<Warren> = sqlx::query_as::<sqlx::Sqlite, Warren>(
|
||||||
"
|
"
|
||||||
SELECT
|
SELECT
|
||||||
*
|
*
|
||||||
19
compose.yaml
19
compose.yaml
@@ -1,7 +1,5 @@
|
|||||||
services:
|
services:
|
||||||
warren:
|
warren:
|
||||||
depends_on:
|
|
||||||
- 'postgres'
|
|
||||||
image: 'warren:latest'
|
image: 'warren:latest'
|
||||||
container_name: 'warren'
|
container_name: 'warren'
|
||||||
build: '.'
|
build: '.'
|
||||||
@@ -13,26 +11,15 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- 'SERVER_ADDRESS=0.0.0.0'
|
- 'SERVER_ADDRESS=0.0.0.0'
|
||||||
- 'SERVER_PORT=8080'
|
- 'SERVER_PORT=8080'
|
||||||
- 'DATABASE_URL=postgres://postgres:pg@warren-postgres:5432'
|
- 'DATABASE_URL=sqlite:///var/lib/warren/data/warren.db'
|
||||||
- 'DATABASE_NAME=warren'
|
|
||||||
- 'SERVE_DIRECTORY=/serve'
|
- 'SERVE_DIRECTORY=/serve'
|
||||||
- 'CORS_ALLOW_ORIGIN=http://localhost:8081'
|
- 'CORS_ALLOW_ORIGIN=http://localhost:8081'
|
||||||
- 'LOG_LEVEL=debug'
|
- 'LOG_LEVEL=debug'
|
||||||
- 'MAX_FILE_FETCH_BYTES=10737418240'
|
- 'MAX_FILE_FETCH_BYTES=10737418240'
|
||||||
|
- 'ZIP_READ_BUFFER_BYTES=4096'
|
||||||
volumes:
|
volumes:
|
||||||
- './backend/serve:/serve:rw'
|
- './backend/serve:/serve:rw'
|
||||||
postgres:
|
- './backend/data:/var/lib/warren/data:rw'
|
||||||
image: 'postgres:17'
|
|
||||||
container_name: 'warren-db'
|
|
||||||
hostname: 'warren-postgres'
|
|
||||||
networks:
|
|
||||||
- 'warren-net'
|
|
||||||
volumes:
|
|
||||||
- './postgres-data:/var/lib/postgresql/data'
|
|
||||||
environment:
|
|
||||||
- 'POSTGRES_PASSWORD=pg'
|
|
||||||
ports:
|
|
||||||
- '5432:5432/tcp'
|
|
||||||
networks:
|
networks:
|
||||||
warren-net:
|
warren-net:
|
||||||
name: 'warren-net'
|
name: 'warren-net'
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
# Nuxt Minimal Starter
|
|
||||||
|
|
||||||
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
Make sure to install dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# npm
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# pnpm
|
|
||||||
pnpm install
|
|
||||||
|
|
||||||
# yarn
|
|
||||||
yarn install
|
|
||||||
|
|
||||||
# bun
|
|
||||||
bun install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development Server
|
|
||||||
|
|
||||||
Start the development server on `http://localhost:3000`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# npm
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
# pnpm
|
|
||||||
pnpm dev
|
|
||||||
|
|
||||||
# yarn
|
|
||||||
yarn dev
|
|
||||||
|
|
||||||
# bun
|
|
||||||
bun run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Production
|
|
||||||
|
|
||||||
Build the application for production:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# npm
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# pnpm
|
|
||||||
pnpm build
|
|
||||||
|
|
||||||
# yarn
|
|
||||||
yarn build
|
|
||||||
|
|
||||||
# bun
|
|
||||||
bun run build
|
|
||||||
```
|
|
||||||
|
|
||||||
Locally preview production build:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# npm
|
|
||||||
npm run preview
|
|
||||||
|
|
||||||
# pnpm
|
|
||||||
pnpm preview
|
|
||||||
|
|
||||||
# yarn
|
|
||||||
yarn preview
|
|
||||||
|
|
||||||
# bun
|
|
||||||
bun run preview
|
|
||||||
```
|
|
||||||
|
|
||||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
|
||||||
@@ -20,6 +20,7 @@ body,
|
|||||||
#__layout {
|
#__layout {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import byteSize from 'byte-size';
|
||||||
import type { DirectoryEntry } from '~/shared/types';
|
import type { DirectoryEntry } from '~/shared/types';
|
||||||
|
|
||||||
const { entry } = defineProps<{ entry: DirectoryEntry }>();
|
const { entry } = defineProps<{ entry: DirectoryEntry }>();
|
||||||
@@ -28,12 +29,9 @@ const onDrop = onDirectoryEntryDrop(entry, true);
|
|||||||
>({{ entry.name }})</span
|
>({{ entry.name }})</span
|
||||||
></span
|
></span
|
||||||
>
|
>
|
||||||
<NuxtTime
|
<span class="text-muted-foreground w-full truncate text-sm">{{
|
||||||
v-if="entry.createdAt != null"
|
byteSize(entry.size)
|
||||||
:datetime="entry.createdAt * 1000"
|
}}</span>
|
||||||
class="text-muted-foreground w-full truncate text-sm"
|
|
||||||
relative
|
|
||||||
></NuxtTime>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -6,25 +6,31 @@ import {
|
|||||||
ContextMenuItem,
|
ContextMenuItem,
|
||||||
ContextMenuSeparator,
|
ContextMenuSeparator,
|
||||||
} from '@/components/ui/context-menu';
|
} from '@/components/ui/context-menu';
|
||||||
import { deleteWarrenDirectory, deleteWarrenFile } from '~/lib/api/warrens';
|
|
||||||
import type { DirectoryEntry } from '#shared/types';
|
import type { DirectoryEntry } from '#shared/types';
|
||||||
import { toast } from 'vue-sonner';
|
import byteSize from 'byte-size';
|
||||||
|
|
||||||
const warrenStore = useWarrenStore();
|
const warrenStore = useWarrenStore();
|
||||||
const copyStore = useCopyStore();
|
const copyStore = useCopyStore();
|
||||||
const renameDialog = useRenameDirectoryDialog();
|
const renameDialog = useRenameDirectoryDialog();
|
||||||
|
|
||||||
const { entry, disabled } = defineProps<{
|
const {
|
||||||
|
entry,
|
||||||
|
entryIndex,
|
||||||
|
disabled,
|
||||||
|
draggable = true,
|
||||||
|
} = defineProps<{
|
||||||
entry: DirectoryEntry;
|
entry: DirectoryEntry;
|
||||||
|
entryIndex: number;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
draggable?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'entry-click': [entry: DirectoryEntry];
|
'entry-click': [entry: DirectoryEntry, event: MouseEvent];
|
||||||
'entry-download': [entry: DirectoryEntry];
|
'entry-download': [entry: DirectoryEntry];
|
||||||
|
'entry-delete': [entry: DirectoryEntry, force: boolean];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const deleting = ref(false);
|
|
||||||
const isCopied = computed(
|
const isCopied = computed(
|
||||||
() =>
|
() =>
|
||||||
warrenStore.current != null &&
|
warrenStore.current != null &&
|
||||||
@@ -33,38 +39,18 @@ const isCopied = computed(
|
|||||||
warrenStore.current.path === copyStore.file.path &&
|
warrenStore.current.path === copyStore.file.path &&
|
||||||
entry.name === copyStore.file.name
|
entry.name === copyStore.file.name
|
||||||
);
|
);
|
||||||
|
const isSelected = computed(() => warrenStore.isSelected(entry));
|
||||||
|
|
||||||
async function submitDelete(force: boolean = false) {
|
function onDelete(force: boolean = false) {
|
||||||
if (warrenStore.current == null) {
|
emit('entry-delete', entry, force);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
deleting.value = true;
|
|
||||||
|
|
||||||
if (entry.fileType === 'directory') {
|
|
||||||
await deleteWarrenDirectory(
|
|
||||||
warrenStore.current.warrenId,
|
|
||||||
warrenStore.current.path,
|
|
||||||
entry.name,
|
|
||||||
force
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await deleteWarrenFile(
|
|
||||||
warrenStore.current.warrenId,
|
|
||||||
warrenStore.current.path,
|
|
||||||
entry.name
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
deleting.value = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openRenameDialog() {
|
function openRenameDialog() {
|
||||||
renameDialog.openDialog(entry);
|
renameDialog.openDialog(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onClick() {
|
function onClick(event: MouseEvent) {
|
||||||
emit('entry-click', entry);
|
emit('entry-click', entry, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragStart(e: DragEvent) {
|
function onDragStart(e: DragEvent) {
|
||||||
@@ -97,18 +83,25 @@ function onShare() {
|
|||||||
function onDownload() {
|
function onDownload() {
|
||||||
emit('entry-download', entry);
|
emit('entry-download', entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onClearCopy() {
|
||||||
|
copyStore.clearFile();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<ContextMenuTrigger class="flex sm:w-52">
|
<ContextMenuTrigger class="flex sm:w-52">
|
||||||
<button
|
<button
|
||||||
|
:data-entry-index="entryIndex"
|
||||||
:disabled="warrenStore.loading || disabled"
|
:disabled="warrenStore.loading || disabled"
|
||||||
:class="[
|
:class="[
|
||||||
'bg-accent/30 border-border flex w-full translate-0 flex-row gap-4 overflow-hidden rounded-md border-1 px-4 py-2 select-none',
|
'bg-accent/30 border-border flex w-full translate-0 flex-row gap-4 overflow-hidden rounded-md border-1 px-4 py-2 outline-0 select-none',
|
||||||
isCopied && 'border-primary/50 border',
|
isCopied && 'border-primary/50 border',
|
||||||
|
isSelected && 'bg-primary/20',
|
||||||
]"
|
]"
|
||||||
draggable="true"
|
:draggable
|
||||||
|
@pointerdown.stop
|
||||||
@dragstart="onDragStart"
|
@dragstart="onDragStart"
|
||||||
@drop="onDrop"
|
@drop="onDrop"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
@@ -119,39 +112,49 @@ function onDownload() {
|
|||||||
class="flex w-full flex-col items-start justify-stretch gap-0 overflow-hidden text-left leading-6"
|
class="flex w-full flex-col items-start justify-stretch gap-0 overflow-hidden text-left leading-6"
|
||||||
>
|
>
|
||||||
<span class="w-full truncate">{{ entry.name }}</span>
|
<span class="w-full truncate">{{ entry.name }}</span>
|
||||||
<NuxtTime
|
<span
|
||||||
v-if="entry.createdAt != null"
|
|
||||||
:datetime="entry.createdAt * 1000"
|
|
||||||
class="text-muted-foreground w-full truncate text-sm"
|
class="text-muted-foreground w-full truncate text-sm"
|
||||||
relative
|
>{{ byteSize(entry.size) }}</span
|
||||||
></NuxtTime>
|
>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</ContextMenuTrigger>
|
</ContextMenuTrigger>
|
||||||
<ContextMenuContent>
|
<ContextMenuContent>
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
:class="[warrenStore.current == null && 'hidden']"
|
:class="[warrenStore.current == null && 'hidden']"
|
||||||
|
@pointerdown.stop
|
||||||
@select="openRenameDialog"
|
@select="openRenameDialog"
|
||||||
>
|
>
|
||||||
<Icon name="lucide:pencil" />
|
<Icon name="lucide:pencil" />
|
||||||
Rename
|
Rename
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuItem
|
<template v-if="warrenStore.current != null">
|
||||||
:class="[warrenStore.current == null && 'hidden']"
|
<ContextMenuItem
|
||||||
@select="onCopy"
|
v-if="
|
||||||
>
|
copyStore.file == null ||
|
||||||
<Icon name="lucide:copy" />
|
copyStore.file.warrenId !==
|
||||||
Copy
|
warrenStore.current.warrenId ||
|
||||||
</ContextMenuItem>
|
copyStore.file.path !== warrenStore.current.path ||
|
||||||
<ContextMenuItem
|
copyStore.file.name !== entry.name
|
||||||
:disabled="entry.fileType !== 'file'"
|
"
|
||||||
@select="onDownload"
|
@pointerdown.stop
|
||||||
>
|
@select="onCopy"
|
||||||
|
>
|
||||||
|
<Icon name="lucide:copy" />
|
||||||
|
Copy
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem v-else @pointerdown.stop @select="onClearCopy">
|
||||||
|
<Icon name="lucide:copy-x" />
|
||||||
|
Clear clipboard
|
||||||
|
</ContextMenuItem>
|
||||||
|
</template>
|
||||||
|
<ContextMenuItem @pointerdown.stop @select="onDownload">
|
||||||
<Icon name="lucide:download" />
|
<Icon name="lucide:download" />
|
||||||
Download
|
Download
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
:class="[warrenStore.current == null && 'hidden']"
|
:class="[warrenStore.current == null && 'hidden']"
|
||||||
|
@pointerdown.stop
|
||||||
@select="onShare"
|
@select="onShare"
|
||||||
>
|
>
|
||||||
<Icon name="lucide:share" />
|
<Icon name="lucide:share" />
|
||||||
@@ -164,15 +167,16 @@ function onDownload() {
|
|||||||
|
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
:class="[warrenStore.current == null && 'hidden']"
|
:class="[warrenStore.current == null && 'hidden']"
|
||||||
@select="() => submitDelete(false)"
|
@pointerdown.stop
|
||||||
|
@select="() => onDelete(false)"
|
||||||
>
|
>
|
||||||
<Icon name="lucide:trash-2" />
|
<Icon name="lucide:trash-2" />
|
||||||
Delete
|
Delete
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
v-if="entry.fileType === 'directory'"
|
|
||||||
:class="[warrenStore.current == null && 'hidden']"
|
:class="[warrenStore.current == null && 'hidden']"
|
||||||
@select="() => submitDelete(true)"
|
@pointerdown.stop
|
||||||
|
@select="() => onDelete(true)"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
class="text-destructive-foreground"
|
class="text-destructive-foreground"
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ const warrenStore = useWarrenStore();
|
|||||||
:data="
|
:data="
|
||||||
route.meta.layout === 'share'
|
route.meta.layout === 'share'
|
||||||
? getApiUrl(
|
? getApiUrl(
|
||||||
`warrens/files/cat_share?shareId=${route.query.id}&path=${joinPaths(warrenStore.current.path, entry.name)}`
|
`warrens/files/cat_share?shareId=${route.query.id}&paths=${joinPaths(warrenStore.current.path, entry.name)}`
|
||||||
)
|
)
|
||||||
: getApiUrl(
|
: getApiUrl(
|
||||||
`warrens/files/cat?warrenId=${warrenStore.current!.warrenId}&path=${joinPaths(warrenStore.current.path, entry.name)}`
|
`warrens/files/cat?warrenId=${warrenStore.current!.warrenId}&paths=${joinPaths(warrenStore.current.path, entry.name)}`
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -7,32 +7,35 @@ const {
|
|||||||
parent,
|
parent,
|
||||||
isOverDropZone,
|
isOverDropZone,
|
||||||
disableEntries = false,
|
disableEntries = false,
|
||||||
|
entriesDraggable = true,
|
||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
entries: DirectoryEntry[];
|
entries: DirectoryEntry[];
|
||||||
parent: DirectoryEntry | null;
|
parent: DirectoryEntry | null;
|
||||||
isOverDropZone?: boolean;
|
isOverDropZone?: boolean;
|
||||||
disableEntries?: boolean;
|
disableEntries?: boolean;
|
||||||
|
entriesDraggable?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'entry-click': [entry: DirectoryEntry];
|
'entry-click': [entry: DirectoryEntry, event: MouseEvent];
|
||||||
'entry-download': [entry: DirectoryEntry];
|
'entry-download': [entry: DirectoryEntry];
|
||||||
|
'entry-delete': [entry: DirectoryEntry, force: boolean];
|
||||||
back: [];
|
back: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { isLoading } = useLoadingIndicator();
|
const { isLoading } = useLoadingIndicator();
|
||||||
|
|
||||||
const sortedEntries = computed(() =>
|
function onEntryClicked(entry: DirectoryEntry, event: MouseEvent) {
|
||||||
entries.toSorted((a, b) => a.name.localeCompare(b.name))
|
emit('entry-click', entry, event);
|
||||||
);
|
|
||||||
|
|
||||||
function onEntryClicked(entry: DirectoryEntry) {
|
|
||||||
emit('entry-click', entry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEntryDownload(entry: DirectoryEntry) {
|
function onEntryDownload(entry: DirectoryEntry) {
|
||||||
emit('entry-download', entry);
|
emit('entry-download', entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onEntryDelete(entry: DirectoryEntry, force: boolean) {
|
||||||
|
emit('entry-delete', entry, force);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -52,12 +55,15 @@ function onEntryDownload(entry: DirectoryEntry) {
|
|||||||
@back="() => emit('back')"
|
@back="() => emit('back')"
|
||||||
/>
|
/>
|
||||||
<DirectoryEntry
|
<DirectoryEntry
|
||||||
v-for="entry in sortedEntries"
|
v-for="(entry, i) in entries"
|
||||||
:key="entry.name"
|
:key="entry.name"
|
||||||
|
:entry-index="i"
|
||||||
:entry="entry"
|
:entry="entry"
|
||||||
:disabled="isLoading || disableEntries"
|
:disabled="isLoading || disableEntries"
|
||||||
|
:draggable="entriesDraggable"
|
||||||
@entry-click="onEntryClicked"
|
@entry-click="onEntryClicked"
|
||||||
@entry-download="onEntryDownload"
|
@entry-download="onEntryDownload"
|
||||||
|
@entry-delete="onEntryDelete"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
const warrenStore = useWarrenStore();
|
const warrenStore = useWarrenStore();
|
||||||
const copyStore = useCopyStore();
|
const copyStore = useCopyStore();
|
||||||
const createDirectoryDialog = useCreateDirectoryDialog();
|
const createDirectoryDialog = useCreateDirectoryDialog();
|
||||||
|
const createFileDialog = useCreateFileDialog();
|
||||||
|
|
||||||
const pasting = ref<boolean>(false);
|
const pasting = ref<boolean>(false);
|
||||||
const validPaste = computed(
|
const validPaste = computed(
|
||||||
@@ -27,12 +28,16 @@ async function onPaste() {
|
|||||||
|
|
||||||
pasting.value = true;
|
pasting.value = true;
|
||||||
|
|
||||||
await pasteFile(copyStore.file!, {
|
const success = await pasteFile(copyStore.file!, {
|
||||||
warrenId: warrenStore.current!.warrenId,
|
warrenId: warrenStore.current!.warrenId,
|
||||||
name: copyStore.file!.name,
|
name: copyStore.file!.name,
|
||||||
path: warrenStore.current!.path,
|
path: warrenStore.current!.path,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
copyStore.clearFile();
|
||||||
|
}
|
||||||
|
|
||||||
pasting.value = false;
|
pasting.value = false;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -47,6 +52,10 @@ async function onPaste() {
|
|||||||
<Icon name="lucide:clipboard-paste" />
|
<Icon name="lucide:clipboard-paste" />
|
||||||
Paste
|
Paste
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem @select="createFileDialog.openDialog">
|
||||||
|
<Icon name="lucide:file-plus" />
|
||||||
|
Create file
|
||||||
|
</ContextMenuItem>
|
||||||
<ContextMenuItem @select="createDirectoryDialog.openDialog">
|
<ContextMenuItem @select="createDirectoryDialog.openDialog">
|
||||||
<Icon name="lucide:folder-plus" />
|
<Icon name="lucide:folder-plus" />
|
||||||
Create directory
|
Create directory
|
||||||
|
|||||||
122
frontend/components/SelectionRect.vue
Normal file
122
frontend/components/SelectionRect.vue
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const rect = useSelectionRect();
|
||||||
|
const warrenStore = useWarrenStore();
|
||||||
|
|
||||||
|
const left = computed(() => Math.min(rect.a.x, rect.b.x));
|
||||||
|
const top = computed(() => Math.min(rect.a.y, rect.b.y));
|
||||||
|
const width = computed(() => Math.abs(rect.a.x - rect.b.x));
|
||||||
|
const height = computed(() => Math.abs(rect.a.y - rect.b.y));
|
||||||
|
|
||||||
|
function onDocumentPointerDown(e: MouseEvent) {
|
||||||
|
if (e.button !== 0 || matchMedia('(pointer:coarse)').matches) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const point = { x: e.x, y: e.y };
|
||||||
|
rect.set(
|
||||||
|
point,
|
||||||
|
point,
|
||||||
|
!e.shiftKey ? 'set' : e.ctrlKey ? 'subtract' : 'add'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDocumentPointerMove(e: MouseEvent) {
|
||||||
|
if (!rect.enabled || matchMedia('(pointer:coarse)').matches) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rect.updateB(e.x, e.y);
|
||||||
|
|
||||||
|
if (
|
||||||
|
rect.mode !== 'set' ||
|
||||||
|
warrenStore.selection.size === 0 ||
|
||||||
|
!rect.isMinSize()
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
warrenStore.clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDocumentPointerUp(e: MouseEvent) {
|
||||||
|
if (
|
||||||
|
!rect.enabled ||
|
||||||
|
e.button !== 0 ||
|
||||||
|
matchMedia('(pointer:coarse)').matches
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const left = Math.min(rect.a.x, rect.b.x);
|
||||||
|
const top = Math.min(rect.a.y, rect.b.y);
|
||||||
|
const width = Math.abs(rect.a.x - rect.b.x);
|
||||||
|
const height = Math.abs(rect.a.y - rect.b.y);
|
||||||
|
const selectionRect = new DOMRect(left, top, width, height);
|
||||||
|
|
||||||
|
const isMinSize = rect.isMinSize();
|
||||||
|
|
||||||
|
rect.disable();
|
||||||
|
|
||||||
|
if (warrenStore.current == null || warrenStore.current.dir == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMinSize) {
|
||||||
|
warrenStore.clearSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryElements = document.querySelectorAll('[data-entry-index]');
|
||||||
|
|
||||||
|
const targetEntries = [];
|
||||||
|
|
||||||
|
for (const entry of entryElements) {
|
||||||
|
const attributeValue = entry.getAttribute('data-entry-index');
|
||||||
|
if (attributeValue == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = parseInt(attributeValue);
|
||||||
|
if (isNaN(index)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryRect = entry.getBoundingClientRect();
|
||||||
|
if (intersectRect(selectionRect, entryRect)) {
|
||||||
|
targetEntries.push(warrenStore.current.dir.entries[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rect.mode === 'set') {
|
||||||
|
warrenStore.setSelection(targetEntries);
|
||||||
|
} else if (rect.mode === 'add') {
|
||||||
|
warrenStore.addMultipleToSelection(targetEntries);
|
||||||
|
} else if (rect.mode === 'subtract') {
|
||||||
|
warrenStore.removeMultipleFromSelection(targetEntries);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('pointerdown', onDocumentPointerDown);
|
||||||
|
document.addEventListener('pointermove', onDocumentPointerMove);
|
||||||
|
document.addEventListener('pointerup', onDocumentPointerUp);
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener('pointerdown', onDocumentPointerDown);
|
||||||
|
document.removeEventListener('pointermove', onDocumentPointerMove);
|
||||||
|
document.removeEventListener('pointerup', onDocumentPointerUp);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="rect.enabled && rect.isMinSize()"
|
||||||
|
:class="[
|
||||||
|
'pointer-events-none absolute z-50',
|
||||||
|
rect.mode === 'set' && 'bg-primary/20',
|
||||||
|
rect.mode === 'add' && 'bg-green-300/20',
|
||||||
|
rect.mode === 'subtract' && 'bg-destructive/20',
|
||||||
|
]"
|
||||||
|
:style="`left: ${left}px; top: ${top}px; width: ${width}px; height: ${height}px;`"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
87
frontend/components/actions/CreateFileDialog.vue
Normal file
87
frontend/components/actions/CreateFileDialog.vue
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { uploadToWarren } from '~/lib/api/warrens';
|
||||||
|
|
||||||
|
const warrenStore = useWarrenStore();
|
||||||
|
const dialog = useCreateFileDialog();
|
||||||
|
|
||||||
|
const creating = ref(false);
|
||||||
|
const fileNameValid = computed(() => dialog.value.trim().length > 0);
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
if (!fileNameValid.value || creating.value || warrenStore.current == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
creating.value = true;
|
||||||
|
|
||||||
|
const dt = new DataTransfer();
|
||||||
|
dt.items.add(new File([], dialog.value));
|
||||||
|
|
||||||
|
const { success } = await uploadToWarren(
|
||||||
|
warrenStore.current.warrenId,
|
||||||
|
warrenStore.current.path,
|
||||||
|
dt.files,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
creating.value = false;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
dialog.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyDown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOpenChange(state: boolean) {
|
||||||
|
if (!state) {
|
||||||
|
dialog.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog v-model:open="dialog.open" @update:open="onOpenChange">
|
||||||
|
<DialogTrigger as-child>
|
||||||
|
<slot />
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Create a file</DialogTitle>
|
||||||
|
<DialogDescription
|
||||||
|
>Give your file a memorable name</DialogDescription
|
||||||
|
>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
v-model="dialog.value"
|
||||||
|
type="text"
|
||||||
|
name="file-name"
|
||||||
|
placeholder="my-awesome-file"
|
||||||
|
aria-required="true"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
@keydown="onKeyDown"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button :disabled="!fileNameValid || creating" @click="submit"
|
||||||
|
>Create</Button
|
||||||
|
>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
@@ -35,7 +35,7 @@ const fileInputElement = ref<HTMLInputElement>(
|
|||||||
const uploading = ref(false);
|
const uploading = ref(false);
|
||||||
const dropZoneRef = ref<HTMLElement>();
|
const dropZoneRef = ref<HTMLElement>();
|
||||||
|
|
||||||
const dropZone = useDropZone(dropZoneRef, {
|
useDropZone(dropZoneRef, {
|
||||||
onDrop,
|
onDrop,
|
||||||
multiple: true,
|
multiple: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ const form = useForm({
|
|||||||
canReadFiles: false,
|
canReadFiles: false,
|
||||||
canModifyFiles: false,
|
canModifyFiles: false,
|
||||||
canDeleteFiles: false,
|
canDeleteFiles: false,
|
||||||
|
|
||||||
|
canListShares: false,
|
||||||
|
canCreateShares: false,
|
||||||
|
canModifyShares: false,
|
||||||
|
canDeleteShares: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -231,6 +236,70 @@ const onSubmit = form.handleSubmit(async (values) => {
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
v-slot="{ value, handleChange }"
|
||||||
|
name="canListShares"
|
||||||
|
>
|
||||||
|
<FormItem class="flex flex-row justify-between">
|
||||||
|
<FormLabel class="grow">List shares</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
:model-value="value"
|
||||||
|
@update:model-value="handleChange"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
v-slot="{ value, handleChange }"
|
||||||
|
name="canCreateShares"
|
||||||
|
>
|
||||||
|
<FormItem class="flex flex-row justify-between">
|
||||||
|
<FormLabel class="grow">Create shares</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
:model-value="value"
|
||||||
|
@update:model-value="handleChange"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
v-slot="{ value, handleChange }"
|
||||||
|
name="canModifyShares"
|
||||||
|
>
|
||||||
|
<FormItem class="flex flex-row justify-between">
|
||||||
|
<FormLabel class="grow">Modify shares</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
:model-value="value"
|
||||||
|
@update:model-value="handleChange"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
v-slot="{ value, handleChange }"
|
||||||
|
name="canDeleteShares"
|
||||||
|
>
|
||||||
|
<FormItem class="flex flex-row justify-between">
|
||||||
|
<FormLabel class="grow">Delete shares</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
:model-value="value"
|
||||||
|
@update:model-value="handleChange"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const warrenStore = useWarrenStore();
|
import { useImageViewer } from '~/stores/viewers';
|
||||||
|
|
||||||
|
const imageViewer = useImageViewer();
|
||||||
|
|
||||||
function onOpenUpdate(state: boolean) {
|
function onOpenUpdate(state: boolean) {
|
||||||
if (!state) {
|
if (!state) {
|
||||||
warrenStore.imageViewer.src = null;
|
imageViewer.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Dialog
|
<Dialog :open="imageViewer.src != null" @update:open="onOpenUpdate">
|
||||||
:open="warrenStore.imageViewer.src != null"
|
|
||||||
@update:open="onOpenUpdate"
|
|
||||||
>
|
|
||||||
<DialogTrigger>
|
<DialogTrigger>
|
||||||
<slot />
|
<slot />
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
@@ -20,9 +19,9 @@ function onOpenUpdate(state: boolean) {
|
|||||||
class="w-full overflow-hidden p-0 sm:!max-h-[90vh] sm:!max-w-[90vw]"
|
class="w-full overflow-hidden p-0 sm:!max-h-[90vh] sm:!max-w-[90vw]"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="warrenStore.imageViewer.src"
|
v-if="imageViewer.src"
|
||||||
class="h-full w-full overflow-hidden !object-contain"
|
class="h-full w-full overflow-hidden !object-contain"
|
||||||
:src="warrenStore.imageViewer.src"
|
:src="imageViewer.src"
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
152
frontend/components/viewers/TextEditor.vue
Normal file
152
frontend/components/viewers/TextEditor.vue
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { toast } from 'vue-sonner';
|
||||||
|
import { useTextEditor } from '~/stores/viewers';
|
||||||
|
|
||||||
|
const editor = useTextEditor();
|
||||||
|
const textarea = ref<HTMLTextAreaElement>(
|
||||||
|
null as unknown as HTMLTextAreaElement
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentLine = ref<number>(1);
|
||||||
|
const totalLines = computed(
|
||||||
|
() => editor.data?.editedContent.split('\n').length ?? 0
|
||||||
|
);
|
||||||
|
|
||||||
|
function onOpenUpdate(state: boolean) {
|
||||||
|
if (!state) {
|
||||||
|
editor.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveFile() {
|
||||||
|
if (editor.data == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = editor.data.entry.name;
|
||||||
|
|
||||||
|
const success = await editor.save();
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
toast.success('Save', {
|
||||||
|
id: 'TEXT_EDITOR_SAVE_TOAST',
|
||||||
|
description: `Successfully saved ${name}`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.error('Save', {
|
||||||
|
id: 'TEXT_EDITOR_SAVE_TOAST',
|
||||||
|
description: `Failed to save ${name}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEditorKeyDown(e: KeyboardEvent) {
|
||||||
|
if (editor.saving) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(updateCurrentLine);
|
||||||
|
|
||||||
|
if (!e.ctrlKey || e.key !== 's') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
saveFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCurrentLine() {
|
||||||
|
if (editor.data == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLine.value = editor.data.editedContent
|
||||||
|
.substring(0, textarea.value.selectionStart)
|
||||||
|
.split('\n').length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEditorClick() {
|
||||||
|
requestAnimationFrame(updateCurrentLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
requestAnimationFrame(updateCurrentLine);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog :open="editor.data != null" @update:open="onOpenUpdate">
|
||||||
|
<DialogContent
|
||||||
|
v-if="editor.data"
|
||||||
|
class="flex h-full max-h-[min(98vh,700px)] w-full !max-w-[min(98vw,1000px)] flex-col"
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{{ editor.data.entry.name }}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div v-if="editor.data.content == null">
|
||||||
|
<span class="text-muted-foreground animate-pulse"
|
||||||
|
>Loading content...</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex h-full w-full flex-col gap-2">
|
||||||
|
<div
|
||||||
|
class="flex h-fit w-full flex-row items-center justify-end gap-1"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
title="Reset"
|
||||||
|
variant="destructive"
|
||||||
|
size="icon"
|
||||||
|
:disabled="
|
||||||
|
editor.data.content === editor.data.editedContent ||
|
||||||
|
editor.saving
|
||||||
|
"
|
||||||
|
@click="() => editor.discardEdits()"
|
||||||
|
>
|
||||||
|
<Icon name="lucide:bomb" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Separator orientation="vertical" class="mx-1" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
title="Save"
|
||||||
|
size="icon"
|
||||||
|
:disabled="
|
||||||
|
editor.saving ||
|
||||||
|
editor.data.content === editor.data.editedContent
|
||||||
|
"
|
||||||
|
@click="saveFile"
|
||||||
|
>
|
||||||
|
<Icon name="lucide:save" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
id="editor"
|
||||||
|
ref="textarea"
|
||||||
|
v-model="editor.data.editedContent"
|
||||||
|
class="h-full w-full resize-none overflow-auto rounded-md border p-4 whitespace-pre focus:outline-0"
|
||||||
|
@keydown="onEditorKeyDown"
|
||||||
|
@click="onEditorClick"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
<div class="flex w-full flex-row justify-end">
|
||||||
|
<p class="text-muted-foreground">
|
||||||
|
Line {{ currentLine }} / {{ totalLines }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#editor {
|
||||||
|
scrollbar-width: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor::-webkit-scrollbar {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
export function useWarrenPath() {
|
export function useWarrenPath(): { warrenId: string; path: string } | null {
|
||||||
const store = useWarrenStore();
|
const store = useWarrenStore();
|
||||||
|
|
||||||
if (store.current == null) {
|
if (store.current == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${store.current.warrenId}${store.current.path}`;
|
return { warrenId: store.current.warrenId, path: store.current.path };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import TextEditor from '~/components/viewers/TextEditor.vue';
|
||||||
|
import ImageViewer from '@/components/viewers/ImageViewer.vue';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import CreateFileDialog from '~/components/actions/CreateFileDialog.vue';
|
||||||
import CreateDirectoryDialog from '~/components/actions/CreateDirectoryDialog.vue';
|
import CreateDirectoryDialog from '~/components/actions/CreateDirectoryDialog.vue';
|
||||||
import UploadDialog from '~/components/actions/UploadDialog.vue';
|
import UploadDialog from '~/components/actions/UploadDialog.vue';
|
||||||
import { getWarrens } from '~/lib/api/warrens';
|
import { getWarrens } from '~/lib/api/warrens';
|
||||||
@@ -16,8 +19,12 @@ await useAsyncData('warrens', async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
|
<SelectionRect />
|
||||||
<ActionsShareDialog />
|
<ActionsShareDialog />
|
||||||
|
|
||||||
|
<TextEditor />
|
||||||
<ImageViewer />
|
<ImageViewer />
|
||||||
|
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
<SidebarInset class="flex flex-col-reverse md:flex-col">
|
<SidebarInset class="flex flex-col-reverse md:flex-col">
|
||||||
<header
|
<header
|
||||||
@@ -31,6 +38,34 @@ await useAsyncData('warrens', async () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ml-auto flex flex-row items-center gap-2">
|
<div class="ml-auto flex flex-row items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
:disabled="
|
||||||
|
store.current != null &&
|
||||||
|
store.current.dir != null &&
|
||||||
|
store.selection.size >=
|
||||||
|
store.current.dir.entries.length
|
||||||
|
"
|
||||||
|
@click="
|
||||||
|
() =>
|
||||||
|
store.current != null &&
|
||||||
|
store.current.dir != null &&
|
||||||
|
store.addMultipleToSelection(
|
||||||
|
store.current.dir.entries
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<Icon name="lucide:list" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
:disabled="store.selection.size < 1"
|
||||||
|
@click="() => store.clearSelection()"
|
||||||
|
>
|
||||||
|
<Icon name="lucide:list-x" />
|
||||||
|
</Button>
|
||||||
<Separator
|
<Separator
|
||||||
orientation="vertical"
|
orientation="vertical"
|
||||||
class="mr-2 hidden !h-4 md:block"
|
class="mr-2 hidden !h-4 md:block"
|
||||||
@@ -48,6 +83,15 @@ await useAsyncData('warrens', async () => {
|
|||||||
></div>
|
></div>
|
||||||
</Button>
|
</Button>
|
||||||
</UploadDialog>
|
</UploadDialog>
|
||||||
|
<CreateFileDialog>
|
||||||
|
<Button
|
||||||
|
v-if="route.path.startsWith('/warrens/')"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
>
|
||||||
|
<Icon name="lucide:file-plus" />
|
||||||
|
</Button>
|
||||||
|
</CreateFileDialog>
|
||||||
<CreateDirectoryDialog>
|
<CreateDirectoryDialog>
|
||||||
<Button
|
<Button
|
||||||
v-if="route.path.startsWith('/warrens/')"
|
v-if="route.path.startsWith('/warrens/')"
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
<script lang="ts" setup></script>
|
<script lang="ts" setup>
|
||||||
|
import TextEditor from '~/components/viewers/TextEditor.vue';
|
||||||
|
import ImageViewer from '@/components/viewers/ImageViewer.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="flex h-full w-full items-center justify-center">
|
<main class="flex h-full w-full items-center justify-center">
|
||||||
|
<SelectionRect />
|
||||||
|
|
||||||
|
<TextEditor />
|
||||||
<ImageViewer />
|
<ImageViewer />
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -120,19 +120,24 @@ export async function listShareFiles(
|
|||||||
| { success: true; files: DirectoryEntry[]; parent: DirectoryEntry | null }
|
| { success: true; files: DirectoryEntry[]; parent: DirectoryEntry | null }
|
||||||
| { success: false }
|
| { success: false }
|
||||||
> {
|
> {
|
||||||
const { data } = await useFetch<
|
const { data, error } = await useFetch<
|
||||||
ApiResponse<{ files: DirectoryEntry[]; parent: DirectoryEntry | null }>
|
ApiResponse<{ files: DirectoryEntry[]; parent: DirectoryEntry | null }>
|
||||||
>(getApiUrl('warrens/files/ls_share'), {
|
>(getApiUrl('warrens/files/ls_share'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: getApiHeaders(),
|
// This is only required for development
|
||||||
|
headers:
|
||||||
|
password != null
|
||||||
|
? { ...getApiHeaders(), 'X-Share-Password': password }
|
||||||
|
: getApiHeaders(),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
shareId: shareId,
|
shareId: shareId,
|
||||||
path: path,
|
path: path,
|
||||||
password: password,
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.value == null) {
|
if (data.value == null) {
|
||||||
|
const errorMessage = await error.value?.data;
|
||||||
|
console.log(errorMessage);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
};
|
};
|
||||||
@@ -149,7 +154,7 @@ export async function fetchShareFile(
|
|||||||
password: string | null
|
password: string | null
|
||||||
): Promise<{ success: true; data: Blob } | { success: false }> {
|
): Promise<{ success: true; data: Blob } | { success: false }> {
|
||||||
const { data } = await useFetch<Blob>(
|
const { data } = await useFetch<Blob>(
|
||||||
getApiUrl(`warrens/files/cat_share?shareId=${shareId}&path=${path}`),
|
getApiUrl(`warrens/files/cat_share?shareId=${shareId}&paths=${path}`),
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers:
|
headers:
|
||||||
@@ -174,3 +179,32 @@ export async function fetchShareFile(
|
|||||||
data: data.value,
|
data: data.value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function verifySharePassword(
|
||||||
|
shareId: string,
|
||||||
|
password: string
|
||||||
|
): Promise<{ success: boolean }> {
|
||||||
|
const { data } = await useFetch<ApiResponse<null>>(
|
||||||
|
getApiUrl(`warrens/files/verify_share_password`),
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: getApiHeaders(),
|
||||||
|
body: JSON.stringify({
|
||||||
|
shareId: shareId,
|
||||||
|
password: password,
|
||||||
|
}),
|
||||||
|
responseType: 'json',
|
||||||
|
cache: 'default',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (data.value == null || data.value.status !== 200) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export async function getWarrenDirectory(
|
|||||||
warrenId,
|
warrenId,
|
||||||
path,
|
path,
|
||||||
}),
|
}),
|
||||||
|
key: `${warrenId}-${path}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.value == null) {
|
if (data.value == null) {
|
||||||
@@ -87,6 +88,40 @@ export async function createDirectory(
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function warrenRm(
|
||||||
|
warrenId: string,
|
||||||
|
paths: string[],
|
||||||
|
force: boolean
|
||||||
|
): Promise<{ success: boolean }> {
|
||||||
|
const { status } = await useFetch(getApiUrl(`warrens/files/rm`), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getApiHeaders(),
|
||||||
|
body: JSON.stringify({
|
||||||
|
warrenId,
|
||||||
|
paths,
|
||||||
|
force,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const TOAST_TITLE = 'Delete';
|
||||||
|
|
||||||
|
if (status.value !== 'success') {
|
||||||
|
toast.error(TOAST_TITLE, {
|
||||||
|
id: 'WARREN_RM_TOAST',
|
||||||
|
description: `Failed to delete directory`,
|
||||||
|
});
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
await refreshNuxtData('current-directory');
|
||||||
|
|
||||||
|
toast.success(TOAST_TITLE, {
|
||||||
|
id: 'WARREN_RM_TOAST',
|
||||||
|
description: `Successfully deleted files`,
|
||||||
|
});
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteWarrenDirectory(
|
export async function deleteWarrenDirectory(
|
||||||
warrenId: string,
|
warrenId: string,
|
||||||
path: string,
|
path: string,
|
||||||
@@ -104,7 +139,7 @@ export async function deleteWarrenDirectory(
|
|||||||
headers: getApiHeaders(),
|
headers: getApiHeaders(),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
warrenId,
|
warrenId,
|
||||||
path,
|
paths: [path],
|
||||||
force,
|
force,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -113,7 +148,7 @@ export async function deleteWarrenDirectory(
|
|||||||
|
|
||||||
if (status.value !== 'success') {
|
if (status.value !== 'success') {
|
||||||
toast.error(TOAST_TITLE, {
|
toast.error(TOAST_TITLE, {
|
||||||
id: 'DELETE_DIRECTORY_TOAST',
|
id: 'WARREN_RM_TOAST',
|
||||||
description: `Failed to delete directory`,
|
description: `Failed to delete directory`,
|
||||||
});
|
});
|
||||||
return { success: false };
|
return { success: false };
|
||||||
@@ -122,7 +157,7 @@ export async function deleteWarrenDirectory(
|
|||||||
await refreshNuxtData('current-directory');
|
await refreshNuxtData('current-directory');
|
||||||
|
|
||||||
toast.success(TOAST_TITLE, {
|
toast.success(TOAST_TITLE, {
|
||||||
id: 'DELETE_DIRECTORY_TOAST',
|
id: 'WARREN_RM_TOAST',
|
||||||
description: `Successfully deleted ${directoryName}`,
|
description: `Successfully deleted ${directoryName}`,
|
||||||
});
|
});
|
||||||
return { success: true };
|
return { success: true };
|
||||||
@@ -144,7 +179,7 @@ export async function deleteWarrenFile(
|
|||||||
headers: getApiHeaders(),
|
headers: getApiHeaders(),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
warrenId,
|
warrenId,
|
||||||
path,
|
paths: [path],
|
||||||
force: false,
|
force: false,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -153,7 +188,7 @@ export async function deleteWarrenFile(
|
|||||||
|
|
||||||
if (status.value !== 'success') {
|
if (status.value !== 'success') {
|
||||||
toast.error(TOAST_TITLE, {
|
toast.error(TOAST_TITLE, {
|
||||||
id: 'DELETE_FILE_TOAST',
|
id: 'WARREN_RM_TOAST',
|
||||||
description: `Failed to delete file`,
|
description: `Failed to delete file`,
|
||||||
});
|
});
|
||||||
return { success: false };
|
return { success: false };
|
||||||
@@ -162,7 +197,7 @@ export async function deleteWarrenFile(
|
|||||||
await refreshNuxtData('current-directory');
|
await refreshNuxtData('current-directory');
|
||||||
|
|
||||||
toast.success(TOAST_TITLE, {
|
toast.success(TOAST_TITLE, {
|
||||||
id: 'DELETE_FILE_TOAST',
|
id: 'WARREN_RM_TOAST',
|
||||||
description: `Successfully deleted ${fileName}`,
|
description: `Successfully deleted ${fileName}`,
|
||||||
});
|
});
|
||||||
return { success: true };
|
return { success: true };
|
||||||
@@ -211,20 +246,11 @@ export async function uploadToWarren(
|
|||||||
try {
|
try {
|
||||||
await promise;
|
await promise;
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Upload', {
|
|
||||||
id: 'UPLOAD_FILE_TOAST',
|
|
||||||
description: `Failed to upload`,
|
|
||||||
});
|
|
||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
await refreshNuxtData('current-directory');
|
await refreshNuxtData('current-directory');
|
||||||
|
|
||||||
toast.success('Upload', {
|
|
||||||
id: 'UPLOAD_FILE_TOAST',
|
|
||||||
description: `Successfully uploaded ${files.length} file${files.length !== 1 ? 's' : ''}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,7 +272,7 @@ export async function renameWarrenEntry(
|
|||||||
headers: getApiHeaders(),
|
headers: getApiHeaders(),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
warrenId,
|
warrenId,
|
||||||
path,
|
paths: [path],
|
||||||
targetPath,
|
targetPath,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -273,14 +299,27 @@ export async function fetchFile(
|
|||||||
warrenId: string,
|
warrenId: string,
|
||||||
path: string,
|
path: string,
|
||||||
fileName: string
|
fileName: string
|
||||||
|
): Promise<{ success: true; data: Blob } | { success: false }> {
|
||||||
|
return fetchFiles(warrenId, path, [fileName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchFiles(
|
||||||
|
warrenId: string,
|
||||||
|
path: string,
|
||||||
|
fileNames: string[]
|
||||||
): Promise<{ success: true; data: Blob } | { success: false }> {
|
): Promise<{ success: true; data: Blob } | { success: false }> {
|
||||||
if (!path.endsWith('/')) {
|
if (!path.endsWith('/')) {
|
||||||
path += '/';
|
path += '/';
|
||||||
}
|
}
|
||||||
path += fileName;
|
const paths = [];
|
||||||
|
for (const fileName of fileNames) {
|
||||||
|
paths.push(path + fileName);
|
||||||
|
}
|
||||||
|
|
||||||
const { data, error } = await useFetch<Blob>(
|
const { data, error } = await useFetch<Blob>(
|
||||||
getApiUrl(`warrens/files/cat?warrenId=${warrenId}&path=${path}`),
|
getApiUrl(
|
||||||
|
`warrens/files/cat?warrenId=${warrenId}&paths=${paths.join(':')}`
|
||||||
|
),
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: getApiHeaders(),
|
headers: getApiHeaders(),
|
||||||
@@ -314,7 +353,7 @@ export async function fetchFileStream(
|
|||||||
path += fileName;
|
path += fileName;
|
||||||
|
|
||||||
const { data, error } = await useFetch<ReadableStream<Uint8Array>>(
|
const { data, error } = await useFetch<ReadableStream<Uint8Array>>(
|
||||||
getApiUrl(`warrens/files/cat?warrenId=${warrenId}&path=${path}`),
|
getApiUrl(`warrens/files/cat?warrenId=${warrenId}&paths=${path}`),
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: getApiHeaders(),
|
headers: getApiHeaders(),
|
||||||
@@ -335,9 +374,9 @@ export async function fetchFileStream(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function moveFile(
|
export async function moveFiles(
|
||||||
warrenId: string,
|
warrenId: string,
|
||||||
currentPath: string,
|
currentPaths: string[],
|
||||||
targetPath: string
|
targetPath: string
|
||||||
): Promise<{ success: boolean }> {
|
): Promise<{ success: boolean }> {
|
||||||
const { status } = await useFetch(getApiUrl(`warrens/files/mv`), {
|
const { status } = await useFetch(getApiUrl(`warrens/files/mv`), {
|
||||||
@@ -345,7 +384,7 @@ export async function moveFile(
|
|||||||
headers: getApiHeaders(),
|
headers: getApiHeaders(),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
warrenId,
|
warrenId,
|
||||||
path: currentPath,
|
paths: currentPaths,
|
||||||
targetPath: targetPath,
|
targetPath: targetPath,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ export const userWarrenSchema = object({
|
|||||||
canReadFiles: boolean().required(),
|
canReadFiles: boolean().required(),
|
||||||
canModifyFiles: boolean().required(),
|
canModifyFiles: boolean().required(),
|
||||||
canDeleteFiles: boolean().required(),
|
canDeleteFiles: boolean().required(),
|
||||||
|
|
||||||
|
canListShares: boolean().required(),
|
||||||
|
canCreateShares: boolean().required(),
|
||||||
|
canModifyShares: boolean().required(),
|
||||||
|
canDeleteShares: boolean().required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createWarrenSchema = object({
|
export const createWarrenSchema = object({
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { fetchShareFile, getShare, listShareFiles } from '~/lib/api/shares';
|
import byteSize from 'byte-size';
|
||||||
|
import { toast } from 'vue-sonner';
|
||||||
|
import {
|
||||||
|
fetchShareFile,
|
||||||
|
getShare,
|
||||||
|
listShareFiles,
|
||||||
|
verifySharePassword,
|
||||||
|
} from '~/lib/api/shares';
|
||||||
import type { DirectoryEntry } from '~/shared/types';
|
import type { DirectoryEntry } from '~/shared/types';
|
||||||
import type { Share } from '~/shared/types/shares';
|
import type { Share } from '~/shared/types/shares';
|
||||||
|
import { useImageViewer, useTextEditor } from '~/stores/viewers';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'share',
|
layout: 'share',
|
||||||
@@ -10,11 +18,26 @@ definePageMeta({
|
|||||||
const warrenStore = useWarrenStore();
|
const warrenStore = useWarrenStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
|
const imageViewer = useImageViewer();
|
||||||
|
const textEditor = useTextEditor();
|
||||||
|
|
||||||
|
const entries = computed(() =>
|
||||||
|
warrenStore.current != null && warrenStore.current.dir != null
|
||||||
|
? warrenStore.current.dir.entries
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
const parent = computed(() =>
|
||||||
|
warrenStore.current != null && warrenStore.current.dir != null
|
||||||
|
? warrenStore.current.dir.parent
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
const share = await getShareFromQuery();
|
const share = await getShareFromQuery();
|
||||||
const entries = ref<DirectoryEntry[] | null>(null);
|
|
||||||
const parent = ref<DirectoryEntry | null>(null);
|
|
||||||
const password = ref<string>('');
|
const password = ref<string>('');
|
||||||
const loading = ref<boolean>(false);
|
const loading = ref<boolean>(false);
|
||||||
|
const passwordValid = ref<boolean>(
|
||||||
|
share == null ? false : !share.data.password
|
||||||
|
);
|
||||||
|
|
||||||
if (share != null) {
|
if (share != null) {
|
||||||
warrenStore.setCurrentWarren(share.data.warrenId, '/');
|
warrenStore.setCurrentWarren(share.data.warrenId, '/');
|
||||||
@@ -44,7 +67,39 @@ async function getShareFromQuery(): Promise<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function submitPassword() {
|
async function submitPassword() {
|
||||||
loadFiles();
|
if (share == null || loading.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!passwordValid.value) {
|
||||||
|
loading.value = true;
|
||||||
|
const result = await verifySharePassword(share.data.id, password.value);
|
||||||
|
loading.value = false;
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
passwordValid.value = true;
|
||||||
|
let cookie = `X-Share-Password=${password.value}; Path=/; SameSite=Lax; Secure;`;
|
||||||
|
|
||||||
|
if (share.data.expiresAt != null) {
|
||||||
|
const dayjs = useDayjs();
|
||||||
|
|
||||||
|
const diff = dayjs(share.data.expiresAt).diff(dayjs()) / 1000;
|
||||||
|
cookie += `Max-Age=${diff};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.cookie = cookie;
|
||||||
|
} else {
|
||||||
|
toast.error('Share', {
|
||||||
|
id: 'SHARE_PASSWORD_TOAST',
|
||||||
|
description: 'Invalid password',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (share.file.fileType === 'directory') {
|
||||||
|
loadFiles();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadFiles() {
|
async function loadFiles() {
|
||||||
@@ -52,10 +107,6 @@ async function loadFiles() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (share.file.fileType !== 'directory') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
const result = await listShareFiles(
|
const result = await listShareFiles(
|
||||||
@@ -65,30 +116,31 @@ async function loadFiles() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
let cookie = `X-Share-Password=${password.value}; Path=/; SameSite=Lax; Secure;`;
|
warrenStore.setCurrentWarrenEntries(result.files, result.parent);
|
||||||
|
|
||||||
if (share.data.expiresAt != null) {
|
|
||||||
const dayjs = useDayjs();
|
|
||||||
|
|
||||||
const diff = dayjs(share.data.expiresAt).diff(dayjs()) / 1000;
|
|
||||||
cookie += `Max-Age=${diff};`;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.cookie = cookie;
|
|
||||||
|
|
||||||
entries.value = result.files;
|
|
||||||
parent.value = result.parent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onEntryClicked(entry: DirectoryEntry) {
|
async function onEntryClicked(entry: DirectoryEntry, event: MouseEvent) {
|
||||||
if (warrenStore.current == null) {
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (
|
||||||
|
warrenStore.current == null ||
|
||||||
|
share == null ||
|
||||||
|
(share.data.password && !passwordValid.value)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entryPath = joinPaths(warrenStore.current.path, entry.name);
|
if (warrenStore.handleSelectionClick(entry, event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryPath =
|
||||||
|
entry !== share.file
|
||||||
|
? joinPaths(warrenStore.current.path, entry.name)
|
||||||
|
: warrenStore.current.path;
|
||||||
|
|
||||||
if (entry.fileType === 'directory') {
|
if (entry.fileType === 'directory') {
|
||||||
warrenStore.setCurrentWarrenPath(entryPath);
|
warrenStore.setCurrentWarrenPath(entryPath);
|
||||||
@@ -109,8 +161,29 @@ async function onEntryClicked(entry: DirectoryEntry) {
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const url = URL.createObjectURL(result.data);
|
const url = URL.createObjectURL(result.data);
|
||||||
warrenStore.imageViewer.src = url;
|
imageViewer.open(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.mimeType.startsWith('text/')) {
|
||||||
|
const result = await fetchShareFile(
|
||||||
|
share!.data.id,
|
||||||
|
entryPath,
|
||||||
|
password.value.length > 0 ? password.value : null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
textEditor.open(
|
||||||
|
warrenStore.current.warrenId,
|
||||||
|
warrenStore.current.path,
|
||||||
|
entry,
|
||||||
|
result.data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,10 +198,15 @@ function onDowloadClicked() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadFile(
|
const downloadName =
|
||||||
share.file.name,
|
share.file.fileType === 'directory'
|
||||||
getApiUrl(`warrens/files/cat_share?shareId=${share.data.id}&path=/`)
|
? `${share.file.name}.zip`
|
||||||
|
: share.file.name;
|
||||||
|
const downloadApiUrl = getApiUrl(
|
||||||
|
`warrens/files/cat_share?shareId=${share.data.id}&paths=/`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
downloadFile(downloadName, downloadApiUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEntryDownload(entry: DirectoryEntry) {
|
function onEntryDownload(entry: DirectoryEntry) {
|
||||||
@@ -136,12 +214,29 @@ function onEntryDownload(entry: DirectoryEntry) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadFile(
|
let downloadName: string;
|
||||||
entry.name,
|
let downloadApiUrl: string;
|
||||||
getApiUrl(
|
|
||||||
`warrens/files/cat_share?shareId=${share.data.id}&path=${joinPaths(warrenStore.current.path, entry.name)}`
|
const selectionSize = warrenStore.selection.size;
|
||||||
)
|
|
||||||
);
|
if (selectionSize === 0 || !warrenStore.isSelected(entry)) {
|
||||||
|
downloadName =
|
||||||
|
entry.fileType === 'directory' ? `${entry.name}.zip` : entry.name;
|
||||||
|
downloadApiUrl = getApiUrl(
|
||||||
|
`warrens/files/cat_share?shareId=${share.data.id}&paths=${joinPaths(warrenStore.current.path, entry.name)}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
downloadName = 'download.zip';
|
||||||
|
const paths = Array.from(warrenStore.selection.values()).map((entry) =>
|
||||||
|
joinPaths(warrenStore.current!.path, entry.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
downloadApiUrl = getApiUrl(
|
||||||
|
`warrens/files/cat_share?shareId=${share.data.id}&paths=${paths.join(':')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadFile(downloadName, downloadApiUrl);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -153,16 +248,34 @@ function onEntryDownload(entry: DirectoryEntry) {
|
|||||||
<div
|
<div
|
||||||
:class="[
|
:class="[
|
||||||
'w-full rounded-lg border transition-all',
|
'w-full rounded-lg border transition-all',
|
||||||
entries == null ? 'max-w-lg' : 'max-w-screen-xl',
|
passwordValid && share.file.fileType === 'directory'
|
||||||
|
? 'h-[min(98vh,600px)] max-w-screen-xl'
|
||||||
|
: 'max-w-2xl',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<div
|
<div class="flex flex-row items-center justify-between gap-4 p-6">
|
||||||
class="flex flex-row items-center justify-between gap-4 px-6 pt-6"
|
<button
|
||||||
>
|
:disabled="share.data.password && !passwordValid"
|
||||||
<div class="flex w-full flex-row">
|
:class="[
|
||||||
<div class="flex grow flex-col gap-1.5">
|
'flex min-w-0 grow flex-row items-center gap-2 text-left',
|
||||||
<h3 class="leading-none font-semibold">Share</h3>
|
(!share.data.password || passwordValid) &&
|
||||||
<p class="text-muted-foreground text-sm">
|
'cursor-pointer',
|
||||||
|
]"
|
||||||
|
@click="(e) => onEntryClicked(share!.file, e)"
|
||||||
|
>
|
||||||
|
<DirectoryEntryIcon :entry="share.file" />
|
||||||
|
|
||||||
|
<div class="flex flex-col overflow-hidden">
|
||||||
|
<h3
|
||||||
|
:title="share.file.name"
|
||||||
|
class="truncate leading-none font-semibold"
|
||||||
|
>
|
||||||
|
{{ share.file.name }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-muted-foreground text-sm text-nowrap">
|
||||||
|
{{ byteSize(share.file.size) }}
|
||||||
|
</p>
|
||||||
|
<p class="text-muted-foreground text-sm text-nowrap">
|
||||||
Created
|
Created
|
||||||
{{
|
{{
|
||||||
$dayjs(share.data.createdAt).format(
|
$dayjs(share.data.createdAt).format(
|
||||||
@@ -171,19 +284,12 @@ function onEntryDownload(entry: DirectoryEntry) {
|
|||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row items-center justify-end gap-4">
|
</button>
|
||||||
<p>{{ share.file.name }}</p>
|
|
||||||
<DirectoryEntryIcon
|
|
||||||
:entry="{ ...share.file, name: '/' }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-row items-end">
|
<div class="flex flex-row items-end">
|
||||||
<Button
|
<Button
|
||||||
:class="
|
:class="
|
||||||
share.file.fileType !== 'file' &&
|
share.data.password && !passwordValid && 'hidden'
|
||||||
entries == null &&
|
|
||||||
'hidden'
|
|
||||||
"
|
"
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -193,12 +299,19 @@ function onEntryDownload(entry: DirectoryEntry) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex w-full flex-col p-6">
|
<div
|
||||||
|
v-if="
|
||||||
|
share.file.fileType === 'directory' ||
|
||||||
|
(share.data.password && !passwordValid)
|
||||||
|
"
|
||||||
|
class="flex w-full flex-col px-6 pb-6"
|
||||||
|
>
|
||||||
<DirectoryList
|
<DirectoryList
|
||||||
v-if="entries != null"
|
v-if="entries != null"
|
||||||
:entries
|
:entries
|
||||||
:parent
|
:parent
|
||||||
:disable-entries="loading"
|
:disable-entries="loading"
|
||||||
|
:entries-draggable="false"
|
||||||
@entry-click="onEntryClicked"
|
@entry-click="onEntryClicked"
|
||||||
@entry-download="onEntryDownload"
|
@entry-download="onEntryDownload"
|
||||||
@back="onBack"
|
@back="onBack"
|
||||||
|
|||||||
@@ -3,14 +3,19 @@ import { useDropZone } from '@vueuse/core';
|
|||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import DirectoryListContextMenu from '~/components/DirectoryListContextMenu.vue';
|
import DirectoryListContextMenu from '~/components/DirectoryListContextMenu.vue';
|
||||||
import RenameEntryDialog from '~/components/actions/RenameEntryDialog.vue';
|
import RenameEntryDialog from '~/components/actions/RenameEntryDialog.vue';
|
||||||
import { fetchFile, getWarrenDirectory } from '~/lib/api/warrens';
|
import { fetchFile, getWarrenDirectory, warrenRm } from '~/lib/api/warrens';
|
||||||
import type { DirectoryEntry } from '~/shared/types';
|
import type { DirectoryEntry } from '~/shared/types';
|
||||||
|
import { useImageViewer, useTextEditor } from '~/stores/viewers';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ['authenticated'],
|
middleware: ['authenticated'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const warrenStore = useWarrenStore();
|
const warrenStore = useWarrenStore();
|
||||||
|
|
||||||
|
const imageViewer = useImageViewer();
|
||||||
|
const textEditor = useTextEditor();
|
||||||
|
|
||||||
const loadingIndicator = useLoadingIndicator();
|
const loadingIndicator = useLoadingIndicator();
|
||||||
const uploadStore = useUploadStore();
|
const uploadStore = useUploadStore();
|
||||||
const warrenPath = computed(() => useWarrenPath());
|
const warrenPath = computed(() => useWarrenPath());
|
||||||
@@ -28,10 +33,10 @@ if (warrenStore.current == null) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const dirData = useAsyncData(
|
useAsyncData(
|
||||||
'current-directory',
|
'current-directory',
|
||||||
async () => {
|
async () => {
|
||||||
if (warrenStore.current == null) {
|
if (warrenPath.value == null) {
|
||||||
return {
|
return {
|
||||||
files: [],
|
files: [],
|
||||||
parent: null,
|
parent: null,
|
||||||
@@ -42,17 +47,17 @@ const dirData = useAsyncData(
|
|||||||
warrenStore.loading = true;
|
warrenStore.loading = true;
|
||||||
|
|
||||||
const { files, parent } = await getWarrenDirectory(
|
const { files, parent } = await getWarrenDirectory(
|
||||||
warrenStore.current.warrenId,
|
warrenPath.value.warrenId,
|
||||||
warrenStore.current.path
|
warrenPath.value.path
|
||||||
);
|
);
|
||||||
|
|
||||||
warrenStore.loading = false;
|
warrenStore.loading = false;
|
||||||
loadingIndicator.finish();
|
loadingIndicator.finish();
|
||||||
|
|
||||||
return { files, parent };
|
warrenStore.setCurrentWarrenEntries(files, parent);
|
||||||
},
|
},
|
||||||
{ watch: [warrenPath] }
|
{ watch: [warrenPath] }
|
||||||
).data;
|
);
|
||||||
|
|
||||||
function onDrop(files: File[] | null, e: DragEvent) {
|
function onDrop(files: File[] | null, e: DragEvent) {
|
||||||
if (files == null) {
|
if (files == null) {
|
||||||
@@ -79,11 +84,17 @@ function onDrop(files: File[] | null, e: DragEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onEntryClicked(entry: DirectoryEntry) {
|
async function onEntryClicked(entry: DirectoryEntry, event: MouseEvent) {
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
if (warrenStore.loading || warrenStore.current == null) {
|
if (warrenStore.loading || warrenStore.current == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (warrenStore.handleSelectionClick(entry, event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (entry.fileType === 'directory') {
|
if (entry.fileType === 'directory') {
|
||||||
warrenStore.addToCurrentWarrenPath(entry.name);
|
warrenStore.addToCurrentWarrenPath(entry.name);
|
||||||
return;
|
return;
|
||||||
@@ -99,10 +110,32 @@ async function onEntryClicked(entry: DirectoryEntry) {
|
|||||||
warrenStore.current.path,
|
warrenStore.current.path,
|
||||||
entry.name
|
entry.name
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const url = URL.createObjectURL(result.data);
|
const url = URL.createObjectURL(result.data);
|
||||||
warrenStore.imageViewer.src = url;
|
imageViewer.open(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.mimeType.startsWith('text/')) {
|
||||||
|
const result = await fetchFile(
|
||||||
|
warrenStore.current.warrenId,
|
||||||
|
warrenStore.current.path,
|
||||||
|
entry.name
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
textEditor.open(
|
||||||
|
warrenStore.current.warrenId,
|
||||||
|
warrenStore.current.path,
|
||||||
|
entry,
|
||||||
|
result.data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,19 +144,41 @@ function onEntryDownload(entry: DirectoryEntry) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.fileType !== 'file') {
|
let downloadName: string;
|
||||||
toast.warning('Download', {
|
let downloadApiUrl: string;
|
||||||
description: 'Directory downloads are not supported yet',
|
|
||||||
});
|
const targets = getTargetsFromSelection(entry, warrenStore.selection);
|
||||||
|
if (targets.length === 1) {
|
||||||
|
downloadName =
|
||||||
|
entry.fileType === 'directory'
|
||||||
|
? `${targets[0].name}.zip`
|
||||||
|
: targets[0].name;
|
||||||
|
downloadApiUrl = getApiUrl(
|
||||||
|
`warrens/files/cat?warrenId=${warrenStore.current.warrenId}&paths=${joinPaths(warrenStore.current.path, targets[0].name)}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
downloadName = 'download.zip';
|
||||||
|
const paths = targets
|
||||||
|
.map((entry) => joinPaths(warrenStore.current!.path, entry.name))
|
||||||
|
.join(':');
|
||||||
|
|
||||||
|
downloadApiUrl = getApiUrl(
|
||||||
|
`warrens/files/cat?warrenId=${warrenStore.current.warrenId}&paths=${paths}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadFile(downloadName, downloadApiUrl);
|
||||||
|
}
|
||||||
|
async function onEntryDelete(entry: DirectoryEntry, force: boolean) {
|
||||||
|
if (warrenStore.current == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadFile(
|
const targets = getTargetsFromSelection(entry, warrenStore.selection).map(
|
||||||
entry.name,
|
(entry) => joinPaths(warrenStore.current!.path, entry.name)
|
||||||
getApiUrl(
|
|
||||||
`warrens/files/cat?warrenId=${warrenStore.current.warrenId}&path=${joinPaths(warrenStore.current.path, entry.name)}`
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await warrenRm(warrenStore.current.warrenId, targets, force);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onBack() {
|
function onBack() {
|
||||||
@@ -135,15 +190,19 @@ function onBack() {
|
|||||||
<div ref="dropZoneRef" class="grow">
|
<div ref="dropZoneRef" class="grow">
|
||||||
<DirectoryListContextMenu class="w-full grow">
|
<DirectoryListContextMenu class="w-full grow">
|
||||||
<DirectoryList
|
<DirectoryList
|
||||||
v-if="dirData != null"
|
v-if="
|
||||||
|
warrenStore.current != null &&
|
||||||
|
warrenStore.current.dir != null
|
||||||
|
"
|
||||||
:is-over-drop-zone="
|
:is-over-drop-zone="
|
||||||
dropZone.isOverDropZone.value &&
|
dropZone.isOverDropZone.value &&
|
||||||
dropZone.files.value != null
|
dropZone.files.value != null
|
||||||
"
|
"
|
||||||
:entries="dirData.files"
|
:entries="warrenStore.current.dir.entries"
|
||||||
:parent="dirData.parent"
|
:parent="warrenStore.current.dir.parent"
|
||||||
@entry-click="onEntryClicked"
|
@entry-click="onEntryClicked"
|
||||||
@entry-download="onEntryDownload"
|
@entry-download="onEntryDownload"
|
||||||
|
@entry-delete="onEntryDelete"
|
||||||
@back="onBack"
|
@back="onBack"
|
||||||
/>
|
/>
|
||||||
</DirectoryListContextMenu>
|
</DirectoryListContextMenu>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export type DirectoryEntry = {
|
|||||||
name: string;
|
name: string;
|
||||||
fileType: FileType;
|
fileType: FileType;
|
||||||
mimeType: string | null;
|
mimeType: string | null;
|
||||||
|
size: number;
|
||||||
/// Timestamp in seconds
|
/// Timestamp in seconds
|
||||||
createdAt: number | null;
|
createdAt: number | null;
|
||||||
isParent: boolean;
|
isParent: boolean;
|
||||||
|
|||||||
1
frontend/shared/types/selection.ts
Normal file
1
frontend/shared/types/selection.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type SelectionMode = 'set' | 'add' | 'subtract';
|
||||||
@@ -6,10 +6,16 @@ import { getParentPath } from '~/utils/files';
|
|||||||
export const useWarrenStore = defineStore('warrens', {
|
export const useWarrenStore = defineStore('warrens', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
warrens: {} as Record<string, WarrenData>,
|
warrens: {} as Record<string, WarrenData>,
|
||||||
imageViewer: {
|
current: null as {
|
||||||
src: null as string | null,
|
warrenId: string;
|
||||||
},
|
path: string;
|
||||||
current: null as { warrenId: string; path: string } | null,
|
dir: {
|
||||||
|
parent: DirectoryEntry | null;
|
||||||
|
entries: DirectoryEntry[];
|
||||||
|
} | null;
|
||||||
|
} | null,
|
||||||
|
selection: new Map() as Map<string, DirectoryEntry>,
|
||||||
|
selectionRangeAnchor: null as DirectoryEntry | null,
|
||||||
loading: false,
|
loading: false,
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
@@ -17,6 +23,23 @@ export const useWarrenStore = defineStore('warrens', {
|
|||||||
this.current = {
|
this.current = {
|
||||||
warrenId,
|
warrenId,
|
||||||
path,
|
path,
|
||||||
|
dir: null,
|
||||||
|
};
|
||||||
|
this.clearSelection();
|
||||||
|
},
|
||||||
|
setCurrentWarrenEntries(
|
||||||
|
entries: DirectoryEntry[],
|
||||||
|
parent: DirectoryEntry | null
|
||||||
|
) {
|
||||||
|
if (this.current == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.current.dir = {
|
||||||
|
entries: entries.toSorted((a, b) =>
|
||||||
|
a.name.localeCompare(b.name)
|
||||||
|
),
|
||||||
|
parent,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
addToCurrentWarrenPath(path: string) {
|
addToCurrentWarrenPath(path: string) {
|
||||||
@@ -28,14 +51,14 @@ export const useWarrenStore = defineStore('warrens', {
|
|||||||
path = '/' + path;
|
path = '/' + path;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.current.path += path;
|
this.setCurrentWarrenPath(this.current.path + path);
|
||||||
},
|
},
|
||||||
backCurrentPath(): boolean {
|
backCurrentPath(): boolean {
|
||||||
if (this.current == null || this.current.path === '/') {
|
if (this.current == null || this.current.path === '/') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.current.path = getParentPath(this.current.path);
|
this.setCurrentWarrenPath(getParentPath(this.current.path));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
@@ -44,19 +67,154 @@ export const useWarrenStore = defineStore('warrens', {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const previous = this.current.path;
|
||||||
|
|
||||||
if (!path.startsWith('/')) {
|
if (!path.startsWith('/')) {
|
||||||
path = '/' + path;
|
path = '/' + path;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.current.path = path;
|
this.current.path = path;
|
||||||
|
|
||||||
|
if (previous !== path) {
|
||||||
|
this.clearSelection();
|
||||||
|
this.current.dir = null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
clearCurrentWarren() {
|
clearCurrentWarren() {
|
||||||
this.current = null;
|
this.current = null;
|
||||||
},
|
},
|
||||||
|
addToSelection(entry: DirectoryEntry) {
|
||||||
|
this.selection.set(entry.name, entry);
|
||||||
|
},
|
||||||
|
setSelection(entries: DirectoryEntry[]) {
|
||||||
|
this.selection = entries.reduce(
|
||||||
|
(acc, entry) => acc.set(entry.name, entry),
|
||||||
|
new Map()
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
this.selectionRangeAnchor != null &&
|
||||||
|
!this.selection.has(this.selectionRangeAnchor.name)
|
||||||
|
) {
|
||||||
|
this.selectionRangeAnchor = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addMultipleToSelection(entries: DirectoryEntry[]) {
|
||||||
|
for (const entry of entries) {
|
||||||
|
this.selection.set(entry.name, entry);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeMultipleFromSelection(entries: DirectoryEntry[]) {
|
||||||
|
for (const entry of entries) {
|
||||||
|
this.selection.delete(entry.name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setSelectedRangeAnchor(entry: DirectoryEntry) {
|
||||||
|
this.selectionRangeAnchor = entry;
|
||||||
|
},
|
||||||
|
removeFromSelection(entry: DirectoryEntry): boolean {
|
||||||
|
return this.selection.delete(entry.name);
|
||||||
|
},
|
||||||
|
toggleSelection(entry: DirectoryEntry): boolean {
|
||||||
|
if (this.selection.has(entry.name)) {
|
||||||
|
this.removeFromSelection(entry);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
this.addToSelection(entry);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isSelected(entry: DirectoryEntry): boolean {
|
||||||
|
if (this.current == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.selection.has(entry.name);
|
||||||
|
},
|
||||||
|
handleSelectionClick(
|
||||||
|
selectedEntry: DirectoryEntry,
|
||||||
|
event: MouseEvent
|
||||||
|
): boolean {
|
||||||
|
if (!event.ctrlKey && !event.shiftKey) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.current == null || this.current.dir == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.ctrlKey ||
|
||||||
|
(event.shiftKey && this.selection.size === 0)
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
this.toggleSelection(selectedEntry) ||
|
||||||
|
this.selectionRangeAnchor == null
|
||||||
|
) {
|
||||||
|
this.setSelectedRangeAnchor(selectedEntry);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!event.shiftKey) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const anchor = this.selectionRangeAnchor;
|
||||||
|
|
||||||
|
if (anchor == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = this.current.dir.entries;
|
||||||
|
|
||||||
|
const anchorIndex = entries.findIndex(
|
||||||
|
(entry) => entry.name === anchor.name
|
||||||
|
);
|
||||||
|
|
||||||
|
const clickedIndex = entries.findIndex(
|
||||||
|
(e) => e.name === selectedEntry.name
|
||||||
|
);
|
||||||
|
|
||||||
|
const targetSelection = [];
|
||||||
|
|
||||||
|
if (clickedIndex > anchorIndex) {
|
||||||
|
for (let i = anchorIndex; i <= clickedIndex; i++) {
|
||||||
|
targetSelection.push(entries[i]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let i = clickedIndex; i <= anchorIndex; i++) {
|
||||||
|
targetSelection.push(entries[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setSelection(targetSelection);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
clearSelection() {
|
||||||
|
this.selection.clear();
|
||||||
|
this.selectionRangeAnchor = null;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useCreateDirectoryDialog = defineStore('create_directory_dialog', {
|
export const useCreateDirectoryDialog = defineStore('create-directory-dialog', {
|
||||||
|
state: () => ({
|
||||||
|
open: false,
|
||||||
|
value: '',
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
openDialog() {
|
||||||
|
this.open = true;
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
this.open = false;
|
||||||
|
this.value = '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useCreateFileDialog = defineStore('create-file-dialog', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
open: false,
|
open: false,
|
||||||
value: '',
|
value: '',
|
||||||
|
|||||||
55
frontend/stores/selectionRect.ts
Normal file
55
frontend/stores/selectionRect.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import type { SelectionMode } from '~/shared/types/selection';
|
||||||
|
|
||||||
|
export const useSelectionRect = defineStore('selection-rect', {
|
||||||
|
state: () => ({
|
||||||
|
enabled: false as boolean,
|
||||||
|
a: {
|
||||||
|
x: 0 as number,
|
||||||
|
y: 0 as number,
|
||||||
|
},
|
||||||
|
b: {
|
||||||
|
x: 0 as number,
|
||||||
|
y: 0 as number,
|
||||||
|
},
|
||||||
|
mode: 'set' as SelectionMode,
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
set(
|
||||||
|
a: { x: number; y: number },
|
||||||
|
b: { x: number; y: number },
|
||||||
|
mode: SelectionMode
|
||||||
|
) {
|
||||||
|
this.a.x = a.x;
|
||||||
|
this.a.y = a.y;
|
||||||
|
|
||||||
|
this.b.x = b.x;
|
||||||
|
this.b.y = b.y;
|
||||||
|
|
||||||
|
this.enabled = true;
|
||||||
|
this.mode = mode;
|
||||||
|
},
|
||||||
|
updateB(x: number, y: number) {
|
||||||
|
this.b.x = x;
|
||||||
|
this.b.y = y;
|
||||||
|
},
|
||||||
|
disable() {
|
||||||
|
this.enabled = false;
|
||||||
|
this.a = {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
};
|
||||||
|
this.b = {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
isMinSize(minPixels: number = 10) {
|
||||||
|
return (
|
||||||
|
Math.max(
|
||||||
|
Math.abs(this.a.x - this.b.x),
|
||||||
|
Math.abs(this.a.y - this.b.y)
|
||||||
|
) >= minPixels
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
13
frontend/stores/viewers/image.ts
Normal file
13
frontend/stores/viewers/image.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export default defineStore('image-viewer', {
|
||||||
|
state: () => ({
|
||||||
|
src: null as string | null,
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
open(src: string) {
|
||||||
|
this.src = src;
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
this.src = null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
4
frontend/stores/viewers/index.ts
Normal file
4
frontend/stores/viewers/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import useImageViewer from './image';
|
||||||
|
import useTextEditor from './text';
|
||||||
|
|
||||||
|
export { useImageViewer, useTextEditor };
|
||||||
71
frontend/stores/viewers/text.ts
Normal file
71
frontend/stores/viewers/text.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { uploadToWarren } from '~/lib/api/warrens';
|
||||||
|
import type { DirectoryEntry } from '~/shared/types';
|
||||||
|
|
||||||
|
export default defineStore('text-editor', {
|
||||||
|
state: () => ({
|
||||||
|
data: null as {
|
||||||
|
warrenId: string;
|
||||||
|
parentPath: string;
|
||||||
|
entry: DirectoryEntry;
|
||||||
|
content: string;
|
||||||
|
editedContent: string;
|
||||||
|
} | null,
|
||||||
|
saving: false as boolean,
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
async open(
|
||||||
|
warrenId: string,
|
||||||
|
parentPath: string,
|
||||||
|
entry: DirectoryEntry,
|
||||||
|
content: Blob
|
||||||
|
) {
|
||||||
|
const contentString = await content.text();
|
||||||
|
|
||||||
|
this.data = {
|
||||||
|
warrenId,
|
||||||
|
parentPath,
|
||||||
|
entry,
|
||||||
|
content: contentString,
|
||||||
|
editedContent: contentString,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async save(): Promise<boolean> {
|
||||||
|
if (this.saving || this.data == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saving = true;
|
||||||
|
|
||||||
|
const dt = new DataTransfer();
|
||||||
|
dt.items.add(
|
||||||
|
new File([this.data.editedContent], this.data.entry.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await uploadToWarren(
|
||||||
|
this.data.warrenId,
|
||||||
|
this.data.parentPath,
|
||||||
|
dt.files,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
this.$patch({
|
||||||
|
data: {
|
||||||
|
content: this.data.editedContent,
|
||||||
|
},
|
||||||
|
saving: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.success;
|
||||||
|
},
|
||||||
|
discardEdits() {
|
||||||
|
if (this.data == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.data.editedContent = this.data.content;
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
this.data = null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { copyFile, moveFile } from '~/lib/api/warrens';
|
import { copyFile, moveFiles } from '~/lib/api/warrens';
|
||||||
import type { DirectoryEntry } from '~/shared/types';
|
import type { DirectoryEntry } from '~/shared/types';
|
||||||
|
|
||||||
export function joinPaths(path: string, ...other: string[]): string {
|
export function joinPaths(path: string, ...other: string[]): string {
|
||||||
@@ -25,7 +25,11 @@ export function onDirectoryEntryDrop(
|
|||||||
return async (e: DragEvent) => {
|
return async (e: DragEvent) => {
|
||||||
const warrenStore = useWarrenStore();
|
const warrenStore = useWarrenStore();
|
||||||
|
|
||||||
if (e.dataTransfer == null || warrenStore.current == null) {
|
if (
|
||||||
|
e.dataTransfer == null ||
|
||||||
|
warrenStore.current == null ||
|
||||||
|
warrenStore.current.dir == null
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,23 +43,29 @@ export function onDirectoryEntryDrop(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentPath = joinPaths(warrenStore.current.path, fileName);
|
const draggedEntry = warrenStore.current.dir.entries.find(
|
||||||
|
(e) => e.name === fileName
|
||||||
|
);
|
||||||
|
if (draggedEntry == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetPaths = getTargetsFromSelection(
|
||||||
|
draggedEntry,
|
||||||
|
warrenStore.selection
|
||||||
|
).map((currentEntry) =>
|
||||||
|
joinPaths(warrenStore.current!.path, currentEntry.name)
|
||||||
|
);
|
||||||
|
|
||||||
let targetPath: string;
|
let targetPath: string;
|
||||||
|
|
||||||
if (isParent) {
|
if (isParent) {
|
||||||
targetPath = joinPaths(
|
targetPath = getParentPath(warrenStore.current.path);
|
||||||
getParentPath(warrenStore.current.path),
|
|
||||||
fileName
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
targetPath = joinPaths(
|
targetPath = joinPaths(warrenStore.current.path, entry.name);
|
||||||
warrenStore.current.path,
|
|
||||||
entry.name,
|
|
||||||
fileName
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await moveFile(warrenStore.current.warrenId, currentPath, targetPath);
|
await moveFiles(warrenStore.current.warrenId, targetPaths, targetPath);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
11
frontend/utils/rect.ts
Normal file
11
frontend/utils/rect.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export function intersectRect(
|
||||||
|
a: { x: number; y: number; width: number; height: number },
|
||||||
|
b: { x: number; y: number; width: number; height: number }
|
||||||
|
): boolean {
|
||||||
|
return !(
|
||||||
|
b.x > a.x + a.width ||
|
||||||
|
b.x + b.width < a.x ||
|
||||||
|
b.y > a.y + a.height ||
|
||||||
|
b.y + b.height < a.y
|
||||||
|
);
|
||||||
|
}
|
||||||
17
frontend/utils/selection.ts
Normal file
17
frontend/utils/selection.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { DirectoryEntry } from '~/shared/types';
|
||||||
|
|
||||||
|
/** Converts a selection and the entry that triggered an action into the target entries
|
||||||
|
* @param targetEntry - The entry that triggered an action
|
||||||
|
* @param selection - The selected entries
|
||||||
|
* @returns If there are no selected elements or the target was not included only the target is returned. Otherwise the selection is returned
|
||||||
|
*/
|
||||||
|
export function getTargetsFromSelection(
|
||||||
|
targetEntry: DirectoryEntry,
|
||||||
|
selection: Map<string, DirectoryEntry>
|
||||||
|
): DirectoryEntry[] {
|
||||||
|
if (selection.size === 0 || !selection.has(targetEntry.name)) {
|
||||||
|
return [targetEntry];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(selection.values());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user