diff --git a/.gitmodules b/.gitmodules index 56818778..3fe77e51 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "orgauth"] path = orgauth url = https://github.com/bburdette/orgauth.git +[submodule "websocket"] + path = websocket + url = git@github.com:bburdette/websocket.git diff --git a/Cargo.lock b/Cargo.lock index 586b3aa2..9adfad63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,9 +59,9 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.11.1" +version = "3.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44cceded2fb55f3c4b67068fa64962e2ca59614edc5b03167de9ff82ae803da0" +checksum = "48e2faa3e7418ed780cca54829d32782a4008a077230f67457caa063415e99c2" dependencies = [ "actix-codec", "actix-rt", @@ -75,7 +75,7 @@ dependencies = [ "derive_more 2.0.1", "encoding_rs", "flate2", - "foldhash", + "foldhash 0.2.0", "futures-core", "h2", "http 0.2.12", @@ -87,8 +87,8 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rand 0.9.2", - "sha1", + "rand 0.10.2", + "sha1 0.11.0", "smallvec", "tokio", "tokio-util", @@ -160,9 +160,9 @@ dependencies = [ [[package]] name = "actix-router" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" +checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7" dependencies = [ "bytestring", "cfg-if", @@ -240,7 +240,7 @@ dependencies = [ "futures-core", "http 0.2.12", "http 1.3.1", - "impl-more", + "impl-more 0.1.9", "pin-project-lite", "tokio", "tokio-rustls 0.23.4", @@ -261,9 +261,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.11.0" +version = "4.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea" +checksum = "df09e2d9239703dd64056359c920c7f3fba6535ec61a0059e0f44e095ffe02b4" dependencies = [ "actix-codec", "actix-http", @@ -280,10 +280,10 @@ dependencies = [ "cookie 0.16.2", "derive_more 2.0.1", "encoding_rs", - "foldhash", + "foldhash 0.2.0", "futures-core", "futures-util", - "impl-more", + "impl-more 0.3.2", "itoa", "language-tags", "log", @@ -296,7 +296,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.5.10", + "socket2 0.6.0", "time", "tracing", "url", @@ -315,12 +315,19 @@ dependencies = [ ] [[package]] -name = "addr2line" -version = "0.24.2" +name = "actix-ws" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "decf53c3cdd63dd6f289980b430238f9a2f6d19f8bce8e418272e08d3da43f0f" dependencies = [ - "gimli", + "actix-codec", + "actix-http", + "actix-web", + "bytestring", + "futures-core", + "futures-sink", + "tokio", + "tokio-util", ] [[package]] @@ -335,7 +342,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "generic-array", ] @@ -347,7 +354,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -778,21 +785,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - [[package]] name = "barrel" version = "0.7.0" @@ -850,6 +842,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block-padding" version = "0.3.3" @@ -947,6 +948,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chrono" version = "0.4.41" @@ -967,7 +979,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "inout", ] @@ -1040,7 +1052,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b77c319abfd5219629c45c34c89ba945ed3c5e49fcde9d16b6c3885f118a730" dependencies = [ - "const-oid", + "const-oid 0.9.6", "der", "spki", "x509-cert", @@ -1083,6 +1095,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "convert_case" version = "0.4.0" @@ -1166,6 +1184,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -1192,6 +1219,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "ctr" version = "0.9.2" @@ -1248,7 +1284,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "der_derive", "flagset", "pem-rfc7468", @@ -1338,11 +1374,22 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.6", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1577,6 +1624,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1747,11 +1800,23 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasi 0.14.4+wasi-0.2.4", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", +] + [[package]] name = "ghash" version = "0.5.1" @@ -1762,12 +1827,6 @@ dependencies = [ "polyval", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "girlboss" version = "1.0.0-alpha.4" @@ -1812,7 +1871,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -1872,7 +1931,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -1949,6 +2008,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "0.14.32" @@ -2202,6 +2270,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" +[[package]] +name = "impl-more" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134d2c4324d61664107020b79019cf6a6aec153f0b79bc9619ee9e794a5fb021" + [[package]] name = "indexmap" version = "2.11.0" @@ -2242,17 +2316,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", -] - [[package]] name = "ipnet" version = "2.11.0" @@ -2567,15 +2630,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - [[package]] name = "oid-registry" version = "0.8.1" @@ -2656,7 +2710,7 @@ dependencies = [ "pkcs5", "rand 0.9.2", "rc2", - "sha1", + "sha1 0.10.6", "sha2", "thiserror 2.0.16", "x509-parser", @@ -2703,7 +2757,7 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "digest", + "digest 0.10.7", "hmac", ] @@ -2784,9 +2838,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "695b3df3d3cc1015f12d70235e35b6b79befc5fa7a9b95b951eab1dd07c9efc2" dependencies = [ "cms", - "const-oid", + "const-oid 0.9.6", "der", - "digest", + "digest 0.10.7", "spki", "x509-cert", "zeroize", @@ -2850,7 +2904,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -2995,6 +3049,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.5.6" @@ -3029,6 +3089,17 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -3082,6 +3153,12 @@ dependencies = [ "getrandom 0.3.3", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rc2" version = "0.8.1" @@ -3277,12 +3354,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -3604,8 +3675,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -3615,8 +3697,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] @@ -3948,29 +4030,26 @@ checksum = "b130bd8a58c163224b44e217b4239ca7b927d82bf6cc2fea1fc561d15056e3f7" [[package]] name = "tokio" -version = "1.47.1" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", "socket2 0.6.0", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -4028,6 +4107,7 @@ dependencies = [ "bytes", "futures-core", "futures-sink", + "futures-util", "pin-project-lite", "tokio", ] @@ -4139,9 +4219,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.18.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicase" @@ -4188,7 +4268,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "subtle", ] @@ -4780,7 +4860,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" dependencies = [ - "const-oid", + "const-oid 0.9.6", "der", "spki", ] @@ -4945,6 +5025,7 @@ dependencies = [ "actix-rt", "actix-session", "actix-web", + "actix-ws", "async-stream", "awc", "barrel", diff --git a/elm/elm.json b/elm/elm.json index 8c3cadf8..dd0979fa 100644 --- a/elm/elm.json +++ b/elm/elm.json @@ -3,7 +3,8 @@ "source-directories": [ "src", "../orgauth/elm-common/src/", - "../orgauth/src/" + "../orgauth/src/", + "../websocket/src/" ], "elm-version": "0.19.1", "dependencies": { diff --git a/elm/src/DataUtil.elm b/elm/src/DataUtil.elm index 0627e006..4d149fe0 100644 --- a/elm/src/DataUtil.elm +++ b/elm/src/DataUtil.elm @@ -18,6 +18,7 @@ type alias FileUrlInfo = { location : String , filelocation : String , tauri : Bool + , websockets : Bool } diff --git a/elm/src/Main.elm b/elm/src/Main.elm index 4385cb61..674ca4be 100644 --- a/elm/src/Main.elm +++ b/elm/src/Main.elm @@ -53,6 +53,7 @@ import ShowMessage import SlideShow import SpecialNotes as SN import SpecialNotesGui as SNG +import TDict import TSet import TagAThing import TagFiles @@ -65,6 +66,7 @@ import Url exposing (Url) import UserSettings import Util exposing (andMap) import View +import WebSocket import WindowKeys import ZkCommon exposing (StylePalette) @@ -93,6 +95,7 @@ type Msg | TauriAdminReplyData JD.Value | TauriPublicReplyData JD.Value | TauriTauriReplyData JD.Value + | ReceiveSocketMsg JD.Value | LoadUrl String | InternalUrl Url | TASelection JD.Value @@ -174,6 +177,7 @@ decodeFlags = |> andMap (JD.field "login" (JD.maybe DataUtil.decodeLoginData)) |> andMap (JD.field "adminsettings" OD.adminSettingsDecoder) |> andMap (JD.field "tauri" JD.bool) + |> andMap (JD.field "websockets" JD.bool) |> andMap (JD.field "mobile" JD.bool) @@ -189,6 +193,7 @@ type alias Flags = , login : Maybe DataUtil.LoginData , adminsettings : OD.AdminSettings , tauri : Bool + , websockets : Bool , mobile : Bool } @@ -208,10 +213,33 @@ type LocalValAction = LocalValAction { for : String , name : String - , action : Maybe String -> ( Model, Cmd Msg ) + , action : Model -> Maybe String -> ( Model, Cmd Msg ) } +type SocketState + = SsOpenRequested + | SsIsOpen + | SsIsClosed WebSocket.SocketClosure + | SsError + + +opensocket : String -> Cmd Msg +opensocket location = + sendSocketCommand + (WebSocket.encodeCmd <| + WebSocket.Connect + { name = "private" + , address = + location + -- http -> ws, https -> wss + |> String.replace "http" "ws" + |> (\s -> s ++ "/privatews") + , protocol = "" + } + ) + + type alias Model = { state : State , size : Util.Size @@ -233,8 +261,9 @@ type alias Model = , ziClosures : Dict Int (Result Http.Error ( Time.Posix, Data.PrivateReply ) -> Msg) , mobile : Bool , spmodel : SP.Model - , localValAction : Maybe LocalValAction + , localValAction : Dict String LocalValAction , zknSearchResult : Data.ZkListNoteSearchResult + , socketState : Maybe SocketState } @@ -869,6 +898,9 @@ showMessage msg = TauriTauriReplyData _ -> "TauriTauriReplyData" + ReceiveSocketMsg _ -> + "ReceiveSocketMsg" + SelectDialogMsg _ -> "SelectDialogMsg" @@ -1381,6 +1413,17 @@ sendZIMsg fui msg = if fui.tauri then sendZIMsgTauri <| PrivateClosureRequest Nothing msg + else if fui.websockets then + sendSocketCommand + (WebSocket.encodeCmd <| + WebSocket.Send + { name = "private" + , content = + JE.encode 0 + (Data.privateClosureRequestEncoder (PrivateClosureRequest Nothing msg)) + } + ) + else HE.postJsonTask { url = fui.location ++ "/private" @@ -1591,94 +1634,128 @@ view model = _ -> routeTitle model.savedRoute.route , body = - [ case model.state of - DisplayMessage dm _ -> - Html.map DisplayMessageMsg <| - GD.layout - (Just { width = min 600 model.size.width, height = min 500 model.size.height }) - dm - - MessageNLink dm _ -> - Html.map MessageNLinkMsg <| - GD.layout - (Just { width = min 600 model.size.width, height = min 500 model.size.height }) - dm - - SelectDialog sdm _ -> - if model.mobile then - E.layout [] <| - E.map SelectDialogMsg <| - GD.dialogView Nothing sdm + (case model.socketState of + Nothing -> + [] - else - Html.map SelectDialogMsg <| - GD.layout - (Just { width = min 600 model.size.width, height = min 500 model.size.height }) - sdm - - ChangePasswordDialog cdm _ -> - Html.map ChangePasswordDialogMsg <| - GD.layout - (Just { width = min 600 model.size.width, height = min 200 model.size.height }) - cdm - - ChangeEmailDialog cdm _ -> - Html.map ChangeEmailDialogMsg <| - GD.layout - (Just { width = min 600 model.size.width, height = min 200 model.size.height }) - cdm - - ChangeRemoteUrlDialog cdm _ -> - Html.map ChangeRemoteUrlDialogMsg <| - GD.layout - (Just { width = min 600 model.size.width, height = min 200 model.size.height }) - cdm - - JobsDialog dm _ -> - if model.mobile then - E.layout [] <| - E.map JobsDialogMsg <| - GD.dialogView Nothing { dm | model = model.jobs } + Just ss -> + case ss of + SsOpenRequested -> + [ Html.text "connecting websocket" ] - else - Html.map JobsDialogMsg <| - GD.layout - (Just { width = min 600 model.size.width, height = min 500 model.size.height }) - { dm | model = model.jobs } + SsIsOpen -> + [] - MdInlineXform gdm _ -> - if model.mobile then - E.layout [] <| - E.map MdInlineXformMsg <| - GD.dialogView Nothing gdm + SsIsClosed WebSocket.PolicyViolation -> + [] - else - Html.map MdInlineXformMsg <| - GD.layout - (Just { width = min 600 model.size.width, height = min 500 model.size.height }) - gdm + SsIsClosed sc -> + [ Html.div [] + [ Html.div [] + [ Html.text <| + "websocket closed with code: " + ++ (String.fromInt <| WebSocket.socketClosureCode sc) + ++ " string: " + ++ WebSocket.showSocketClosure sc + ] + , Html.div [] + [ Html.text + "reconnecting..." + ] + ] + ] - RequestsDialog dm _ -> - if model.mobile then - E.layout [] <| - E.map RequestsDialogMsg <| - GD.dialogView Nothing { dm | model = model.trackedRequests } + SsError -> + [ Html.text <| "websocket error" ] + ) + ++ [ case model.state of + DisplayMessage dm _ -> + Html.map DisplayMessageMsg <| + GD.layout + (Just { width = min 600 model.size.width, height = min 500 model.size.height }) + dm + + MessageNLink dm _ -> + Html.map MessageNLinkMsg <| + GD.layout + (Just { width = min 600 model.size.width, height = min 500 model.size.height }) + dm + + SelectDialog sdm _ -> + if model.mobile then + E.layout [] <| + E.map SelectDialogMsg <| + GD.dialogView Nothing sdm - else - Html.map RequestsDialogMsg <| - GD.layout - (Just { width = min 600 model.size.width, height = min 500 model.size.height }) - -- use the live-updated model - { dm | model = model.trackedRequests } + else + Html.map SelectDialogMsg <| + GD.layout + (Just { width = min 600 model.size.width, height = min 500 model.size.height }) + sdm + + ChangePasswordDialog cdm _ -> + Html.map ChangePasswordDialogMsg <| + GD.layout + (Just { width = min 600 model.size.width, height = min 200 model.size.height }) + cdm + + ChangeEmailDialog cdm _ -> + Html.map ChangeEmailDialogMsg <| + GD.layout + (Just { width = min 600 model.size.width, height = min 200 model.size.height }) + cdm + + ChangeRemoteUrlDialog cdm _ -> + Html.map ChangeRemoteUrlDialogMsg <| + GD.layout + (Just { width = min 600 model.size.width, height = min 200 model.size.height }) + cdm + + JobsDialog dm _ -> + if model.mobile then + E.layout [] <| + E.map JobsDialogMsg <| + GD.dialogView Nothing { dm | model = model.jobs } - _ -> - E.layout - ([ EF.size model.stylePalette.fontSize, E.width E.fill ] - ++ dndif - ) - <| - viewState model.size model.state model - ] + else + Html.map JobsDialogMsg <| + GD.layout + (Just { width = min 600 model.size.width, height = min 500 model.size.height }) + { dm | model = model.jobs } + + MdInlineXform gdm _ -> + if model.mobile then + E.layout [] <| + E.map MdInlineXformMsg <| + GD.dialogView Nothing gdm + + else + Html.map MdInlineXformMsg <| + GD.layout + (Just { width = min 600 model.size.width, height = min 500 model.size.height }) + gdm + + RequestsDialog dm _ -> + if model.mobile then + E.layout [] <| + E.map RequestsDialogMsg <| + GD.dialogView Nothing { dm | model = model.trackedRequests } + + else + Html.map RequestsDialogMsg <| + GD.layout + (Just { width = min 600 model.size.width, height = min 500 model.size.height }) + -- use the live-updated model + { dm | model = model.trackedRequests } + + _ -> + E.layout + ([ EF.size model.stylePalette.fontSize, E.width E.fill ] + ++ dndif + ) + <| + viewState model.size model.state model + ] } @@ -2201,10 +2278,10 @@ actualupdate msg model = ( nmd, cmd ) ( ReceiveLocalVal lv, _ ) -> - case model.localValAction of + case Dict.get lv.name model.localValAction of Just (LocalValAction lva) -> if lv.for == lva.for && lv.name == lva.name then - lva.action lv.value + lva.action { model | localValAction = Dict.remove lv.name model.localValAction } lv.value else ( model, Cmd.none ) @@ -2236,6 +2313,69 @@ actualupdate msg model = , Cmd.none ) + ( ReceiveSocketMsg jd, _ ) -> + case JD.decodeValue WebSocket.decodeMsg jd of + Ok (WebSocket.OnError wsm) -> + ( { model | socketState = Just <| SsError } + , Cmd.none + ) + + Ok (WebSocket.OnClose wsm) -> + ( { model | socketState = Just <| SsIsClosed wsm.code } + , case wsm.code of + WebSocket.PolicyViolation -> + -- don't keep trying, bad login. + sendSocketCommand + (WebSocket.encodeCmd <| + WebSocket.Close + { name = "private" + } + ) + + _ -> + opensocket model.fui.location + ) + + Ok (WebSocket.OnOpen wsm) -> + ( { model | socketState = Just SsIsOpen } + , Cmd.none + ) + + Ok (WebSocket.OnData wsm) -> + if wsm.name == "private" then + case JD.decodeString (makeTDDecoder Data.privateClosureReplyDecoder) wsm.data of + Ok td -> + case td.data.closureId of + Just id -> + case Dict.get id model.ziClosures of + Just closure -> + let + cmsg = + closure (Ok ( td.utc, td.data.reply )) + in + actualupdate cmsg model + + Nothing -> + actualupdate (ZkReplyData (Ok ( td.utc, td.data.reply ))) model + + Nothing -> + actualupdate (ZkReplyData (Ok ( td.utc, td.data.reply ))) model + + Err e -> + ( displayMessageDialog model <| JD.errorToString e ++ "\n" ++ JE.encode 2 jd + , Cmd.none + ) + + else + ( displayMessageDialog model <| "unknown websocket connection - \"" ++ wsm.name ++ "\"" + , Cmd.none + ) + + Err e -> + ( displayMessageDialog model <| JD.errorToString e ++ "\n" ++ JE.encode 2 jd + , Cmd.none + ) + ( TauriUserReplyData jd, _ ) -> case JD.decodeValue OD.userResponseDecoder jd of Ok d -> @@ -2722,7 +2862,7 @@ actualupdate msg model = Data.PbyZkNoteAndLinks znl -> let action = - \mbstate -> + \amodel mbstate -> let znas = { znal = znl, mbstate = mbstate } @@ -2743,21 +2883,21 @@ actualupdate msg model = ngets = makePubNoteCacheGets model znl.zknote.content in - ( { model | state = vstate } + ( { amodel | state = vstate } , Cmd.batch ngets ) lid = SNG.localDataId znl.zknote.id in - ( { model | localValAction = Just <| LocalValAction { for = "lva", name = lid, action = action } } + ( { model | localValAction = Dict.insert lid (LocalValAction { for = "lva", name = lid, action = action }) model.localValAction } , LS.getLocalVal { for = "lva", name = lid } ) Data.PbyZkNoteAndLinksWhat znlw -> let action = - \mbstate -> + \amodel mbstate -> let znas : DataUtil.ZkNoteAndStateWhat znas = @@ -2765,12 +2905,12 @@ actualupdate msg model = , what = znlw.what } in - onZkNoteStatePbWhat model pt znas + onZkNoteStatePbWhat amodel pt znas lid = SNG.localDataId znlw.znl.zknote.id in - ( { model | localValAction = Just <| LocalValAction { for = "lva", name = lid, action = action } } + ( { model | localValAction = Dict.insert lid (LocalValAction { for = "lva", name = lid, action = action }) model.localValAction } , LS.getLocalVal { for = "lva", name = lid } ) @@ -3255,7 +3395,7 @@ actualupdate msg model = Data.PvyZkNoteAndLinksWhat znew -> let action = - \mbstate -> + \amodel mbstate -> let znas : DataUtil.ZkNoteAndStateWhat znas = @@ -3263,12 +3403,12 @@ actualupdate msg model = , what = znew.what } in - onZkNoteStateEditWhat model pt znas + onZkNoteStateEditWhat amodel pt znas lid = SNG.localDataId znew.znl.zknote.id in - ( { model | localValAction = Just <| LocalValAction { for = "lva", name = lid, action = action } } + ( { model | localValAction = Dict.insert lid (LocalValAction { for = "lva", name = lid, action = action }) model.localValAction } , LS.getLocalVal { for = "lva", name = lid } ) @@ -5187,6 +5327,11 @@ initToRoute model route = model.navkey (routeUrl (stateRoute initialstate).route) , Just c + , if model.fui.websockets then + Just <| opensocket model.fui.location + + else + Nothing ] ) @@ -5223,6 +5368,7 @@ init flags url key zone fontsize = { location = flags.location , filelocation = flags.filelocation , tauri = flags.tauri + , websockets = flags.websockets } , navkey = key , seed = seed @@ -5241,12 +5387,18 @@ init flags url key zone fontsize = , ziClosures = Dict.empty , mobile = flags.mobile , spmodel = SP.initModel - , localValAction = Nothing + , localValAction = Dict.empty , zknSearchResult = { notes = [] , offset = 0 , what = "" } + , socketState = + if flags.websockets then + Just SsOpenRequested + + else + Nothing } geterrornote = @@ -5350,6 +5502,7 @@ main = , receiveUITauriResponse TauriUserReplyData , receivePITauriResponse TauriPublicReplyData , receiveTITauriResponse TauriTauriReplyData + , receiveSocketMsg ReceiveSocketMsg ] ++ rdysubs , onUrlRequest = urlRequest @@ -5413,3 +5566,9 @@ port sendKeyCommand : JE.Value -> Cmd msg skcommand : WindowKeys.WindowKeyCmd -> Cmd Msg skcommand = WindowKeys.send sendKeyCommand + + +port receiveSocketMsg : (JD.Value -> msg) -> Sub msg + + +port sendSocketCommand : JE.Value -> Cmd msg diff --git a/onsave/src/main.rs b/onsave/src/main.rs index 45c93053..340f1370 100644 --- a/onsave/src/main.rs +++ b/onsave/src/main.rs @@ -147,7 +147,6 @@ async fn err_main() -> Result<(), Box> { loop { let r: Result<(), Box> = async { - println!("here!!"); let conn = match (amqp_uid.clone(), amqp_pwd.clone()) { (Some(uid), Some(pwd)) => { // Hack in the uid and pwd to the url. @@ -429,8 +428,6 @@ pub async fn yeet_service( match res { Ok(res) => { - println!("res: {:?}", res); - if !res.status().is_success() { error!("error saving yeet error note: {}, {:?}", zkn.id, res); } diff --git a/orgauth b/orgauth index 6cfff9d4..2492c7d0 160000 --- a/orgauth +++ b/orgauth @@ -1 +1 @@ -Subproject commit 6cfff9d4d3b35cb1aab1197c85b4603fdb6f110c +Subproject commit 2492c7d05dba082dbdfc047adfe0ac2a2db9f498 diff --git a/server-lib/Cargo.toml b/server-lib/Cargo.toml index e2e60356..26a0a8f5 100644 --- a/server-lib/Cargo.toml +++ b/server-lib/Cargo.toml @@ -9,7 +9,7 @@ zkprotocol = { path = "../zkprotocol" } orgauth = { path = "../orgauth/rustlib/" } env_logger = "0.11.8" # env_logger = { version = "0.11.5", features = ["anstream"] } -rusqlite = { version = "0.37.0", features = ["bundled"] } +rusqlite = { version = "0.37.0", features = ["bundled", "trace"] } log = "0.4.28" actix-web = "4.11.0" actix-files = "0.6.7" @@ -50,3 +50,4 @@ girlboss = {version = "1.0.0-alpha.4", features = ["actix-rt"]} nom = "8.0.0" # lapin = { version = "3.7.2", default-features = false, features = [ "rustls--ring", "default-runtime" ] } lapin = { version = "2.5.5", default-features = true } +actix-ws = "0.4.0" diff --git a/server-lib/src/error.rs b/server-lib/src/error.rs index 3d459e27..da158bdf 100644 --- a/server-lib/src/error.rs +++ b/server-lib/src/error.rs @@ -1,5 +1,6 @@ use actix_session; use actix_web::error as awe; +use actix_web::ResponseError; use cookie; use girlboss; use regex; @@ -49,6 +50,8 @@ pub fn annotate_string(s: String, source: Error) -> Error { annotate(Error::String(s), source) } +impl ResponseError for Error {} + impl fmt::Display for AnnotatedE { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} \n source: {}", self.error, self.source) diff --git a/server-lib/src/interfaces.rs b/server-lib/src/interfaces.rs index 250e0620..dbf81785 100644 --- a/server-lib/src/interfaces.rs +++ b/server-lib/src/interfaces.rs @@ -8,6 +8,7 @@ use crate::search; use crate::sqldata; use crate::sqldata::local_server_id; use crate::sqldata::zknotes_callbacks; +use crate::sqldata::ConnFig; use crate::sqldata::LapinInfo; use crate::state::new_jobid; use crate::state::State; @@ -40,11 +41,16 @@ use zkprotocol::private::PrivateReply; use zkprotocol::private::PrivateRequest; use zkprotocol::public::{PublicReply, PublicRequest}; use zkprotocol::search::{ZkListNoteSearchResult, ZkNoteSearch}; + pub fn login_data_for_token( session: Session, config: &Config, ) -> Result, Box> { - let mut conn = sqldata::connection_open(config.orgauth_config.db.as_path())?; + let mut conn = sqldata::connection_open( + "login_data_for_token", + ConnFig::ReadWrite, + config.orgauth_config.db.as_path(), + )?; conn.busy_timeout(Duration::from_millis(500))?; let mut cb = sqldata::zknotes_callbacks(); @@ -54,7 +60,7 @@ pub fn login_data_for_token( match orgauth::dbfun::read_user_with_token_pageload( &mut conn, &session, - token, + &token, config.orgauth_config.regen_login_tokens, config.orgauth_config.login_token_expiration_ms, ) { @@ -81,7 +87,7 @@ pub fn login_data_for_token( pub async fn connect_and_make_lapin_info( state: &State, - token: Option, + token: &Option, ) -> Option { // TODO: maybe attempt reconnect only every X seconds, so as not to // slow processing during rmq outage. @@ -177,6 +183,8 @@ pub async fn zk_interface_loggedin_streaming( PrivateStreamingRequests::SearchZkNotes => { let search: ZkNoteSearch = serde_json::from_value(msgdata.clone())?; let conn = Arc::new(sqldata::connection_open( + "PrivateStreamingRequests::SearchZkNotes", + ConnFig::ReadOnly, config.orgauth_config.db.as_path(), )?); let znsstream = @@ -187,6 +195,8 @@ pub async fn zk_interface_loggedin_streaming( PrivateStreamingRequests::GetArchiveZkLinks => { let rq: GetArchiveZkLinks = serde_json::from_value(msgdata.clone())?; let conn = Arc::new(sqldata::connection_open( + "psr:GetArchiveZkLinks", + ConnFig::ReadOnly, config.orgauth_config.db.as_path(), )?); let bstream = sqldata::read_archivezklinks_stream(conn, uid, rq.createddate_after, None) @@ -196,6 +206,8 @@ pub async fn zk_interface_loggedin_streaming( PrivateStreamingRequests::GetZkLinksSince => { let rq: GetZkLinksSince = serde_json::from_value(msgdata.clone())?; let conn = Arc::new(sqldata::connection_open( + "psr:GetZkLinksSince", + ConnFig::ReadOnly, config.orgauth_config.db.as_path(), )?); let bstream = sqldata::read_zklinks_since_stream(conn, uid, rq.createddate_after, None) @@ -204,6 +216,8 @@ pub async fn zk_interface_loggedin_streaming( } PrivateStreamingRequests::Sync => { let conn = Arc::new(sqldata::connection_open( + "psr:Sync", + ConnFig::ReadWrite, config.orgauth_config.db.as_path(), )?); let rq: SyncSince = serde_json::from_value(msgdata.clone())?; @@ -235,8 +249,8 @@ pub async fn zk_interface_loggedin_streaming( pub async fn zk_interface_loggedin( state: &State, - conn: &Connection, - token: Option, + conn: Connection, + token: &Option, uid: UserId, msg: &PrivateRequest, ) -> Result { @@ -311,23 +325,53 @@ pub async fn zk_interface_loggedin( } } PrivateRequest::PvqPowerDelete(search) => { + // conn.close().map_err(|e| e.1)?; + // let conn = sqldata::connection_open( + // "PvqPowerDelete", + // ConnFig::ReadWrite, + // state.config.orgauth_config.db.as_path(), + // )?; let res = search::power_delete_zknotes(&conn, state.config.file_path.clone(), uid, &search)?; Ok(PrivateReply::PvyPowerDeleteComplete(res)) } PrivateRequest::PvqDeleteZkNote(id) => { + // conn.close().map_err(|e| e.1)?; + // let conn = sqldata::connection_open( + // "PvqDeleteZkNote", + // ConnFig::ReadWrite, + // state.config.orgauth_config.db.as_path(), + // )?; sqldata::delete_zknote(&conn, state.config.file_path.clone(), uid, &id)?; Ok(PrivateReply::PvyDeletedZkNote(id.clone())) } PrivateRequest::PvqSaveZkNote(sbe) => { + // conn.close().map_err(|e| e.1)?; + // let conn = sqldata::connection_open( + // "PvqSaveZkNote", + // ConnFig::ReadWrite, + // state.config.orgauth_config.db.as_path(), + // )?; let li = connect_and_make_lapin_info(state, token).await; let (_id, s) = sqldata::save_zknote(&conn, &li, &state.server, uid, &sbe, None).await?; Ok(PrivateReply::PvySavedZkNote(s)) } PrivateRequest::PvqSaveZkLinks(msg) => { - let _ = sqldata::save_zklinks(&state.config.orgauth_config.db.as_path(), uid, &msg.links)?; + // conn.close().map_err(|e| e.1)?; + // let conn = sqldata::connection_open( + // "PvqSaveZkLinks", + // ConnFig::ReadWrite, + // state.config.orgauth_config.db.as_path(), + // )?; + let _ = sqldata::save_zklinks(&conn, uid, &msg.links)?; Ok(PrivateReply::PvySavedZkLinks) } PrivateRequest::PvqSaveZkNoteAndLinks(sznpl) => { + // conn.close().map_err(|e| e.1)?; + // let conn = sqldata::connection_open( + // "PvqSaveZkNoteAndLinks", + // ConnFig::ReadWrite, + // state.config.orgauth_config.db.as_path(), + // )?; let li = connect_and_make_lapin_info(state, token).await; let (_, szkn) = sqldata::save_zknote(&conn, &li, &state.server, uid, &sznpl.note, None).await?; @@ -336,23 +380,45 @@ pub async fn zk_interface_loggedin( Ok(PrivateReply::PvySavedZkNoteAndLinks(szkn)) } PrivateRequest::PvqSaveImportZkNotes(gzl) => { + // conn.close().map_err(|e| e.1)?; + // let conn = sqldata::connection_open( + // "PvqPowerDelete", + // ConnFig::ReadWrite, + // state.config.orgauth_config.db.as_path(), + // )?; let li = connect_and_make_lapin_info(state, token).await; sqldata::save_importzknotes(&conn, &li, &state.server, uid, gzl).await?; Ok(PrivateReply::PvySavedImportZkNotes) } PrivateRequest::PvqSetHomeNote(hn) => { + // conn.close().map_err(|e| e.1)?; + // let conn = sqldata::connection_open( + // "PvqPowerDelete", + // ConnFig::ReadWrite, + // state.config.orgauth_config.db.as_path(), + // )?; sqldata::set_homenote(&conn, uid, hn)?; Ok(PrivateReply::PvyHomeNoteSet(hn.clone())) } PrivateRequest::PvqSyncRemote => { + // conn.close().map_err(|e| e.1)?; + // let conn = sqldata::connection_open( + // "PvqPowerDelete", + // ConnFig::ReadWrite, + // state.config.orgauth_config.db.as_path(), + // )?; let dbpath: PathBuf = state.config.orgauth_config.db.to_path_buf(); let file_path: PathBuf = state.config.file_path.to_path_buf(); let uid: UserId = uid; let jid = new_jobid(state, uid); let lgb = state.girlboss.clone(); let server = state.server.clone(); - let li = connect_and_make_lapin_info(state, token.clone()).await; + let li = connect_and_make_lapin_info(state, token).await; let lapin_channelx = li.map(|li| li.channel).clone(); + let token = token.clone(); + + // trying to reduce sqlite blocking. + conn.close().map_err(|(_conn, err)| err)?; std::thread::spawn(move || { let rt = actix_rt::System::new(); @@ -410,7 +476,7 @@ pub async fn zk_interface_loggedin( jid, server, lapin_channelx, - token, + token.clone(), )); rt.run() .map_err(|e| { @@ -461,7 +527,8 @@ pub async fn zk_interface_loggedin( write!(gbm, "starting file sync"); let r = async { - let conn = sqldata::connection_open(&dbpath.as_path())?; + let conn = + sqldata::connection_open("startit", ConnFig::ReadWrite, &dbpath.as_path())?; let _dv = sync::sync_files_down( &conn, &lapin_info, @@ -558,7 +625,11 @@ pub fn public_interface( ) -> Result { match msg { PublicRequest::PbrGetZkNoteAndLinks(gzne) => { - let conn = sqldata::connection_open(config.orgauth_config.db.as_path())?; + let conn = sqldata::connection_open( + "PublicRequest::PbrGetZkNoteAndLinks", + ConnFig::ReadOnly, + config.orgauth_config.db.as_path(), + )?; let (_, note) = sqldata::read_zknote(&conn, &config.file_path, None, &gzne.zknote)?; info!( "public#getzknote: {:?} - {} - {:?}", @@ -578,7 +649,11 @@ pub fn public_interface( "user#getzneifchanged: {:?} - {}", gzic.zknote, gzic.changeddate ); - let conn = sqldata::connection_open(config.orgauth_config.db.as_path())?; + let conn = sqldata::connection_open( + "PublicRequest::PbrGetZknIfChanged", + ConnFig::ReadOnly, + config.orgauth_config.db.as_path(), + )?; let ozkne = sqldata::read_zneifchanged(&conn, &config.file_path, None, &gzic)?; match ozkne { @@ -587,7 +662,11 @@ pub fn public_interface( } } PublicRequest::PbrGetZkNotePubId(pubid) => { - let conn = sqldata::connection_open(config.orgauth_config.db.as_path())?; + let conn = sqldata::connection_open( + "PublicRequest::PbrGetZkNotePubId", + ConnFig::ReadOnly, + config.orgauth_config.db.as_path(), + )?; let note = sqldata::read_zknotepubid(&conn, &config.file_path, None, pubid.as_str())?; info!( "public#getzknotepubid: {} - {} - {:?}", diff --git a/server-lib/src/lib.rs b/server-lib/src/lib.rs index b4fe23a6..3f890fbd 100644 --- a/server-lib/src/lib.rs +++ b/server-lib/src/lib.rs @@ -9,7 +9,7 @@ mod sqltest; pub mod state; mod sync; mod synctest; -use crate::{error as zkerr, state::State}; +use crate::{error as zkerr, sqldata::ConnFig, state::State}; use actix_cors::Cors; use actix_files::NamedFile; use actix_multipart::Multipart; @@ -19,16 +19,18 @@ use actix_session::{ use actix_web::{ cookie::{self, Key}, dev::Server, - web, App, HttpRequest, HttpResponse, HttpServer, Result, + rt, web, App, HttpRequest, HttpResponse, HttpServer, Result, }; +use actix_ws::AggregatedMessage; use chrono; use clap::Arg; use config::Config; +use futures_util::StreamExt as _; use futures_util::TryStreamExt as _; use girlboss::Girlboss; use interfaces::connect_and_make_lapin_info; use lapin; -use log::{error, info}; +use log::{error, info, warn}; pub use orgauth; use orgauth::{data::UserId, util}; pub use orgauth::{ @@ -36,17 +38,18 @@ pub use orgauth::{ endpoints::ActixTokener, }; pub use rusqlite; -use rusqlite::Connection; +use rusqlite::{trace::config_log, Connection}; +use serde::{Deserialize, Serialize}; use serde_json; use simple_error::simple_error; use sqldata::{get_single_value, local_server_id, LapinInfo}; -use std::fs::File; use std::io::{stdin, Write}; -use std::path::Path; use std::path::PathBuf; use std::str::FromStr; use std::{env, sync::RwLock}; use std::{error::Error, sync::Arc}; +use std::{ffi::c_int, path::Path}; +use std::{fs::File, time::SystemTime}; use timer; use tokio_util::io::StreamReader; use tracing_actix_web::TracingLogger; @@ -56,7 +59,9 @@ pub use zkprotocol::content as zc; pub use zkprotocol::messages::PrivateStreamingMessage; pub use zkprotocol::search as zs; use zkprotocol::{ - private::{PrivateError, PrivateReply, PrivateRequest}, + private::{ + PrivateClosureReply, PrivateClosureRequest, PrivateError, PrivateReply, PrivateRequest, + }, public::{PublicError, PublicReply, PublicRequest}, upload::UploadReply, }; @@ -178,7 +183,11 @@ async fn user( // req.connection_info() // ); match async { - let conn = sqldata::connection_open(data.config.orgauth_config.db.as_path())?; + let conn = sqldata::connection_open( + "user", + ConnFig::ReadWrite, + data.config.orgauth_config.db.as_path(), + )?; interfaces::user_interface( &conn, @@ -219,7 +228,7 @@ async fn admin( ) { Ok(sr) => HttpResponse::Ok().json(sr), Err(e) => { - error!("'user' err: {:?}", e); + error!("'admin' err: {:?}", e); let se = AdminResponse::ArpServerError(e.to_string()); HttpResponse::Ok().json(se) } @@ -235,7 +244,7 @@ fn session_user( None => Err(zkerr::Error::NotLoggedIn)?, Some(token) => orgauth::dbfun::read_user_by_token_api( &conn, - token, + &token, state.config.orgauth_config.login_token_expiration_ms, state.config.orgauth_config.regen_login_tokens, ) @@ -254,7 +263,11 @@ pub fn get_tauri_uid(conn: &Connection) -> Result, zkerr::Error> } async fn file(session: Session, state: web::Data, req: HttpRequest) -> HttpResponse { - let conn = match sqldata::connection_open(state.config.orgauth_config.db.as_path()) { + let conn = match sqldata::connection_open( + "file", + ConnFig::ReadOnly, + state.config.orgauth_config.db.as_path(), + ) { Ok(c) => c, Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)), }; @@ -323,7 +336,7 @@ async fn receive_files( mut payload: Multipart, req: HttpRequest, ) -> HttpResponse { - let li = connect_and_make_lapin_info(config.get_ref(), get_cookie_id(&req)).await; + let li = connect_and_make_lapin_info(config.get_ref(), &get_cookie_id(&req)).await; match make_file_notes(session, &config, &li, &mut payload).await { Ok(r) => HttpResponse::Ok().json(r), Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)), @@ -336,7 +349,11 @@ async fn make_file_notes( lapin_info: &Option, payload: &mut Multipart, ) -> Result> { - let conn = sqldata::connection_open(state.config.orgauth_config.db.as_path())?; + let conn = sqldata::connection_open( + "make_file_notes", + ConnFig::ReadWrite, + state.config.orgauth_config.db.as_path(), + )?; let uid = if state.config.tauri_mode { get_tauri_uid(&conn)?.ok_or(zkerr::Error::NotLoggedIn)? } else { @@ -428,6 +445,128 @@ fn get_cookie_id(req: &HttpRequest) -> Option { req.cookie("id").map(|c| c.value().to_string()) } +#[derive(Serialize, Deserialize)] +pub struct PrivateTimedData { + utcmillis: u128, + data: PrivateClosureReply, +} + +async fn private_ws( + session: Session, + data: web::Data, + req: HttpRequest, + stream: web::Payload, +) -> Result { + info!("private_ws"); + let mut state = data.clone(); + + let token = get_cookie_id(&req); + + let (res, mut wssession, stream) = actix_ws::handle(&req, stream)?; + + let mut stream = stream + .aggregate_continuations() + // aggregate continuation frames up to 1MiB + .max_continuation_size(2_usize.pow(20)); + + let token_uuid = match session.get::("token")? { + Some(tu) => tu, + None => { + warn!("closing socket"); + wssession + .close(Some(actix_ws::CloseReason { + code: actix_ws::CloseCode::Policy, + description: Some("not logged in".to_string()), + })) + .await + .map_err(|e| zkerr::Error::String(e.to_string()))?; + + return Ok(res); // have to return this special 'res' for the above to work. + } + }; + + rt::spawn(async move { + while let Some(msg) = stream.next().await { + match async { + match msg { + Ok(AggregatedMessage::Text(t)) => { + let pcr: PrivateClosureRequest = serde_json::from_str(&t)?; + match ( + zk_interface_check(&token_uuid, &mut state, &token, pcr.request).await, + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .map(|n| n.as_millis()), + ) { + (Ok(sr), Ok(t)) => { + let pcreply = PrivateTimedData { + utcmillis: t, + data: PrivateClosureReply { + closure_id: pcr.closure_id, + reply: sr, + }, + }; + + wssession.text(serde_json::to_string(&pcreply)?).await? + } + (_, Err(e)) => { + error!("'private' err: {:?}", e); + wssession + .text(serde_json::to_string(&PrivateReply::PvyServerError( + PrivateError::PveString(e.to_string()), + ))?) + .await?; + } + (Err(e), _) => { + error!("'private' err: {:?}", e); + wssession + .text(serde_json::to_string(&PrivateReply::PvyServerError( + PrivateError::PveString(e.to_string()), + ))?) + .await?; + } + } + } + Ok(AggregatedMessage::Binary(b)) => { + wssession.binary(b).await?; + } + Ok(AggregatedMessage::Ping(msg)) => { + wssession.pong(&msg).await?; + } + Ok(AggregatedMessage::Pong(msg)) => { + wssession.ping(&msg).await?; // ping? + } + Ok(AggregatedMessage::Close(_msg)) => { + // wssession.close(msg); + return Ok::<(), Box>(()); + } + + Err(e) => { + error!("error {e}"); + } + }; + Ok(()) + } + .await + { + Ok(()) => (), + Err(e) => { + error!("'user' err: {:?}", e); + let se = UserResponse::UrpServerError(e.to_string()); + match serde_json::to_string(&se) { + Ok(s) => { + // we're ignoring it if reporting the error fails too. + let _ = wssession.text(s).await; + } + Err(_) => (), + } + } + } + } + }); + + Ok(res) +} + async fn private( session: Session, data: web::Data, @@ -437,8 +576,19 @@ async fn private( let mut state = data.clone(); let token = get_cookie_id(&req); + let token_uuid = match session.get::("token") { + Ok(None) => { + return HttpResponse::Ok().json(PrivateReply::PvyServerError(PrivateError::PveNotLoggedIn)) + } + Ok(Some(token_uuid)) => token_uuid, + Err(e) => { + return HttpResponse::Ok().json(PrivateReply::PvyServerError(PrivateError::PveString( + e.to_string(), + ))) + } + }; - match zk_interface_check(&session, &mut state, token, item.into_inner()).await { + match zk_interface_check(&token_uuid, &mut state, &token, item.into_inner()).await { Ok(sr) => HttpResponse::Ok().json(sr), Err(e) => { error!("'private' err: {:?}", e); @@ -466,33 +616,29 @@ async fn private_streaming( } async fn zk_interface_check( - session: &Session, + token_uuid: &Uuid, state: &State, - token: Option, + token: &Option, msg: PrivateRequest, ) -> Result { - match session.get::("token")? { - None => Ok(PrivateReply::PvyServerError(PrivateError::PveNotLoggedIn)), - Some(token_uuid) => { - let conn = sqldata::connection_open(state.config.orgauth_config.db.as_path())?; - match orgauth::dbfun::read_user_by_token_api( - &conn, - token_uuid, - state.config.orgauth_config.login_token_expiration_ms, - state.config.orgauth_config.regen_login_tokens, - ) { - Err(e) => { - info!("read_user_by_token_api error2: {:?}, {:?}", token_uuid, e); - - Ok(PrivateReply::PvyServerError(PrivateError::PveLoginError( - e.to_string(), - ))) - } - Ok(userdata) => { - // finally! processing messages as logged in user. - interfaces::zk_interface_loggedin(state, &conn, token, userdata.id, &msg).await - } - } + let conn = sqldata::connection_open( + "zk_interface_check", + ConnFig::ReadWrite, + state.config.orgauth_config.db.as_path(), + )?; + info!("pre read_user_by_token_api"); + match orgauth::dbfun::read_user_by_token_api( + &conn, + token_uuid, + state.config.orgauth_config.login_token_expiration_ms, + state.config.orgauth_config.regen_login_tokens, + ) { + Err(e) => Ok(PrivateReply::PvyServerError(PrivateError::PveLoginError( + e.to_string(), + ))), + Ok(userdata) => { + // finally! processing messages as logged in user. + interfaces::zk_interface_loggedin(state, conn, token, userdata.id, &msg).await } } } @@ -506,21 +652,20 @@ async fn zk_interface_check_streaming( None => Ok(HttpResponse::Ok().json(PrivateReply::PvyServerError(PrivateError::PveNotLoggedIn))), Some(token) => { - let conn = sqldata::connection_open(config.orgauth_config.db.as_path())?; + let conn = sqldata::connection_open( + "zk_interface_check_streaming", + ConnFig::ReadWrite, + config.orgauth_config.db.as_path(), + )?; match orgauth::dbfun::read_user_by_token_api( &conn, - token, + &token, config.orgauth_config.login_token_expiration_ms, config.orgauth_config.regen_login_tokens, ) { - Err(e) => { - info!("read_user_by_token_api error2: {:?}, {:?}", token, e); - Ok( - HttpResponse::Ok().json(PrivateReply::PvyServerError(PrivateError::PveLoginError( - format!("{:?}", e), - ))), - ) - } + Err(e) => Ok(HttpResponse::Ok().json(PrivateReply::PvyServerError( + PrivateError::PveLoginError(format!("{:?}", e)), + ))), Ok(userdata) => { // finally! processing messages as logged in user. interfaces::zk_interface_loggedin_streaming(&config, userdata.id, &msg).await @@ -536,11 +681,11 @@ async fn private_upstreaming( body: web::Payload, req: HttpRequest, ) -> HttpResponse { - let li = connect_and_make_lapin_info(data.get_ref(), get_cookie_id(&req)).await; + let li = connect_and_make_lapin_info(data.get_ref(), &get_cookie_id(&req)).await; match zk_interface_check_upstreaming(&session, &data.config, &li, body).await { Ok(hr) => hr, Err(e) => { - error!("'private' err: {:?}", e); + error!("'private_upstreaming' err: {:?}", e); let se = PrivateReply::PvyServerError(PrivateError::PveString(e.to_string())); HttpResponse::Ok().json(se) } @@ -548,7 +693,7 @@ async fn private_upstreaming( } fn convert_bodyerr(err: actix_web::error::PayloadError) -> std::io::Error { - error!("convert_err {:?}", err); + error!("convert_bodyerr {:?}", err); todo!() } @@ -562,25 +707,23 @@ async fn zk_interface_check_upstreaming( None => Ok(HttpResponse::Ok().json(PrivateReply::PvyServerError(PrivateError::PveNotLoggedIn))), Some(token) => { - let conn = sqldata::connection_open(config.orgauth_config.db.as_path())?; + let conn = sqldata::connection_open( + "zk_interface_check_upstreaming", + ConnFig::ReadWrite, + config.orgauth_config.db.as_path(), + )?; let server = sqldata::local_server_id(&conn)?; match orgauth::dbfun::read_user_by_token_api( &conn, - token, + &token, config.orgauth_config.login_token_expiration_ms, config.orgauth_config.regen_login_tokens, ) { - Err(e) => { - info!("read_user_by_token_api error2: {:?}, {:?}", token, e); - - Ok( - HttpResponse::Ok().json(PrivateReply::PvyServerError(PrivateError::PveLoginError( - format!("{:?}", e), - ))), - ) - } + Err(e) => Ok(HttpResponse::Ok().json(PrivateReply::PvyServerError( + PrivateError::PveLoginError(format!("{:?}", e)), + ))), Ok(user) => { // finally! processing messages as logged in user. let rstream = @@ -600,6 +743,7 @@ async fn zk_interface_check_upstreaming( None, &mut sqldata::zknotes_callbacks(), &mut br, + None, ) .await?; @@ -665,11 +809,24 @@ async fn new_email(data: web::Data, req: HttpRequest) -> HttpResponse { orgauth::endpoints::new_email(&data.config.orgauth_config, req) } +fn rulog(cint: c_int, meh: &str) { + info!("rulog: {}; {}", cint, meh); +} + +const SQLITE_LOGGING: bool = false; + #[actix_web::main] pub async fn err_main( oconfig: Option, logfile: Option, ) -> Result<(), Box> { + // sqlite logging + if SQLITE_LOGGING { + unsafe { + config_log(Some(rulog))?; + } + } + match logfile { Some(lf) => { let target = Box::new(File::create(lf).expect("Can't create file")); @@ -785,7 +942,11 @@ pub async fn err_main( // promoting a user to admin? if let Some(uid) = matches.value_of("promote_to_admin") { - let conn = sqldata::connection_open(config.orgauth_config.db.as_path())?; + let conn = sqldata::connection_open( + "promote to admin", + ConnFig::ReadWrite, + config.orgauth_config.db.as_path(), + )?; let mut user = orgauth::dbfun::read_user_by_name(&conn, uid)?; user.admin = true; orgauth::dbfun::update_user(&conn, &user)?; @@ -802,7 +963,11 @@ pub async fn err_main( stdin().read_line(&mut pwd)?; let mut cb = sqldata::zknotes_callbacks(); - let conn = sqldata::connection_open(config.orgauth_config.db.as_path())?; + let conn = sqldata::connection_open( + "creating admin", + ConnFig::ReadWrite, + config.orgauth_config.db.as_path(), + )?; // make new registration i let rd = orgauth::data::RegistrationData { uid: username.to_string(), @@ -893,7 +1058,11 @@ pub async fn init_server(mut config: Config) -> Result> { }; { - let conn = sqldata::connection_open(config.orgauth_config.db.as_path())?; + let conn = sqldata::connection_open( + "init_server", + ConnFig::ReadWrite, + config.orgauth_config.db.as_path(), + )?; let tr = conn.unchecked_transaction()?; sqldata::update_filetable(&conn, &config.file_path)?; @@ -959,6 +1128,7 @@ pub async fn init_server(mut config: Config) -> Result> { .service(web::resource("/upload").route(web::post().to(receive_files))) .service(web::resource("/public").route(web::post().to(public))) .service(web::resource("/private").route(web::post().to(private))) + .service(web::resource("/privatews").route(web::get().to(private_ws))) .service(web::resource("/stream").route(web::post().to(private_streaming))) .service(web::resource("/upstream").route(web::post().to(private_upstreaming))) .service(web::resource("/user").route(web::post().to(user))) diff --git a/server-lib/src/migrations.rs b/server-lib/src/migrations.rs index 7180d7b6..db4066f6 100644 --- a/server-lib/src/migrations.rs +++ b/server-lib/src/migrations.rs @@ -3199,3 +3199,31 @@ pub fn udpate43(dbfile: &Path) -> Result<(), zkerr::Error> { tr.commit()?; Ok(()) } + +pub fn udpate44(dbfile: &Path) -> Result<(), zkerr::Error> { + // db connection without foreign key checking. + let conn = Connection::open(dbfile)?; + let tr = conn.unchecked_transaction()?; + + conn.execute( + "CREATE INDEX \"idx_zkarchzknote\" ON \"zkarch\" (\"zknote\")", + params![], + )?; + + tr.commit()?; + Ok(()) +} + +pub fn udpate45(dbfile: &Path) -> Result<(), zkerr::Error> { + // db connection without foreign key checking. + let conn = Connection::open(dbfile)?; + let tr = conn.unchecked_transaction()?; + + conn.execute( + "CREATE INDEX \"idx_zkarchuuid\" ON \"zkarch\" (\"uuid\")", + params![], + )?; + + tr.commit()?; + Ok(()) +} diff --git a/server-lib/src/search.rs b/server-lib/src/search.rs index 6eec4d23..4c7ed1af 100644 --- a/server-lib/src/search.rs +++ b/server-lib/src/search.rs @@ -238,9 +238,9 @@ pub fn search_zknotes_stream( try_stream! { let (sql, args) = build_sql(&conn, user, &search, exclude_notes)?; - let mut stmt = conn.prepare(sql.as_str())?; let mut rows = stmt.query(rusqlite::params_from_iter(args.iter()))?; + yield SyncMessage::from(ZkSearchResultHeader { what: search.what, resulttype: search.resulttype, diff --git a/server-lib/src/sqldata.rs b/server-lib/src/sqldata.rs index 715e2746..40a5bebd 100644 --- a/server-lib/src/sqldata.rs +++ b/server-lib/src/sqldata.rs @@ -11,8 +11,8 @@ use orgauth::data::{RegistrationData, UserId}; use orgauth::dbfun::user_id; use orgauth::endpoints::Callbacks; use orgauth::util::now; -use rusqlite::Row; use rusqlite::{params, Connection}; +use rusqlite::{OpenFlags, Row}; use serde_derive::{Deserialize, Serialize}; use simple_error::bail; use std::iter::FromIterator; @@ -169,8 +169,37 @@ pub fn set_homenote( Ok(()) } -pub fn connection_open(dbfile: &Path) -> Result { - let conn = Connection::open(dbfile)?; +fn sqlog(meh: &str) { + info!("sqlog: {}", meh); +} + +const SQLITE_TRACING: bool = false; + +pub enum ConnFig { + ReadWrite, + ReadOnly, +} + +pub fn connection_open( + _connid: &str, + conn_fig: ConnFig, + dbfile: &Path, +) -> Result { + let mut conn = match conn_fig { + ConnFig::ReadOnly => { + let c = Connection::open_with_flags( + dbfile, + OpenFlags::SQLITE_OPEN_READ_ONLY + | OpenFlags::SQLITE_OPEN_NO_MUTEX + | OpenFlags::SQLITE_OPEN_URI, // OpenFlags::SQLITE_OPEN_READ_ONLY + // | OpenFlags::SQLITE_OPEN_CREATE + // | OpenFlags::SQLITE_OPEN_URI + // | OpenFlags::SQLITE_OPEN_NO_MUTEX, + )?; + c + } + ConnFig::ReadWrite => Connection::open(dbfile)?, + }; // conn.busy_timeout(Duration::from_millis(500))?; conn.busy_handler(Some(|count| { @@ -180,6 +209,11 @@ pub fn connection_open(dbfile: &Path) -> Result { true }))?; + if SQLITE_TRACING { + #[allow(deprecated)] + conn.trace(Some(sqlog)); + } + conn.execute("PRAGMA foreign_keys = true;", params![])?; // give this a go with sync! @@ -240,7 +274,7 @@ pub fn update_single_value( pub fn dbinit(dbfile: &Path, token_expiration_ms: Option) -> Result { let exists = dbfile.exists(); - let conn = connection_open(dbfile)?; + let conn = connection_open("dbinit", ConnFig::ReadWrite, dbfile)?; if !exists { info!("initialdb"); @@ -474,6 +508,16 @@ pub fn dbinit(dbfile: &Path, token_expiration_ms: Option) -> Result, - token: Option, + token: &Option, ) -> Option { match (conn, token) { (Some(conn), Some(token)) => match make_lapin_channels(conn).await { - Ok(lc) => Some(LapinInfo { channel: lc, token }), + Ok(lc) => Some(LapinInfo { + channel: lc, + token: token.clone(), + }), Err(e) => { error!("{e}"); None @@ -1741,12 +1788,10 @@ pub fn delete_zknote( } pub fn save_zklinks( - dbfile: &Path, + conn: &Connection, uid: UserId, zklinks: &Vec, ) -> Result<(), zkerr::Error> { - let conn = connection_open(dbfile)?; - for zklink in zklinks.iter() { // TODO: integrate into sql instead of separate queries. let to = note_id_for_zknoteid(&conn, &zklink.to)?; @@ -1834,19 +1879,17 @@ pub fn save_savelzlinks( if link.delete == Some(true) { // create archive record. let now = now()?; - let ac = conn.execute( + let _ac = conn.execute( "insert into zklinkarchive (fromid, toid, user, linkzknote, createdate, deletedate) select fromid, toid, user, linkzknote, createdate, ?1 from zklink where fromid = ?2 and toid = ?3 and user = ?4", params![now, from, to, uid.to_i64()], )?; - println!("inserted {} archive lzlink", ac); // delete the link. - let dc = conn.execute( + let _dc = conn.execute( "delete from zklink where fromid = ?1 and toid = ?2 and user = ?3", params![from, to, uid.to_i64()], )?; - println!("deleted {} lzlink", dc); } else { save_zklink(&conn, from, to, uid, Some(linkzknote))?; } diff --git a/server-lib/src/sqltest.rs b/server-lib/src/sqltest.rs index 618bd16f..7628eefe 100644 --- a/server-lib/src/sqltest.rs +++ b/server-lib/src/sqltest.rs @@ -40,7 +40,7 @@ mod tests { dbinit(dbp, None)?; - let conn = connection_open(dbp)?; + let conn = connection_open("dbp", dbp)?; let server = local_server_id(&conn)?; @@ -754,10 +754,10 @@ mod tests { } println!("25"); - // should be 4 notes tagged with et'user' - 3 users and 1 'system'. + // should be 3 notes tagged with e>'user' - 3 users let u1pubnote2_exact_search = ZkNoteSearch { tagsearch: vec![TagSearch::SearchTerm { - mods: vec![SearchMod::ExactMatch, SearchMod::Tag], + mods: vec![SearchMod::ExactMatch, SearchMod::TagTo], term: "user".to_string(), }], offset: 0, @@ -771,10 +771,10 @@ mod tests { match search_zknotes(&conn, filesdir, uid1, &u1pubnote2_exact_search)? { SearchResult::SrListNote(zklr) => { - if zklr.notes.len() == 4 { + if zklr.notes.len() == 3 { () } else { - panic!("test failed") + panic!("test failed, len was {}", zklr.notes.len()) } } _ => panic!("test failed"), @@ -784,7 +784,7 @@ mod tests { // should be 9 notes for 'user1' let u1pubnote2_exact_search = ZkNoteSearch { tagsearch: vec![TagSearch::SearchTerm { - mods: vec![SearchMod::ExactMatch, SearchMod::Tag, SearchMod::User], + mods: vec![SearchMod::ExactMatch, SearchMod::TagTo, SearchMod::User], term: "user1".to_string(), }], offset: 0, diff --git a/server-lib/src/sync.rs b/server-lib/src/sync.rs index e85d2e6a..12756b07 100644 --- a/server-lib/src/sync.rs +++ b/server-lib/src/sync.rs @@ -4,7 +4,7 @@ use crate::search::build_sql; use crate::search::{search_zknotes, search_zknotes_stream, sync_users, system_user, SearchResult}; use crate::sqldata::{ self, local_server_id, note_id_for_uuid, save_zklink, save_zknote, server_id, user_note_id, - LapinInfo, NoteDates, + ConnFig, LapinInfo, NoteDates, }; use crate::util::now; use actix_multipart_rfc7578 as multipart; @@ -17,7 +17,7 @@ use futures::future; use futures::Stream; use futures_util::TryStreamExt; use futures_util::{StreamExt, TryFutureExt}; -use log::{error, info}; +use log::{error, info, warn}; use orgauth; use orgauth::data::{User, UserId}; use orgauth::dbfun::user_id; @@ -183,6 +183,15 @@ pub fn temp_tables(conn: &Connection) -> Result { params![], )?; + conn.execute( + format!( + "CREATE UNIQUE INDEX \"{}unq\" ON \"{}\" (\"id\")", + notetemp, notetemp + ) + .as_str(), + params![], + )?; + conn.execute( format!( "create temporary table {} (\"id\" integer primary key not null)", @@ -192,6 +201,15 @@ pub fn temp_tables(conn: &Connection) -> Result { params![], )?; + conn.execute( + format!( + "CREATE UNIQUE INDEX \"{}unq\" ON \"{}\" (\"id\")", + archivenotetemp, archivenotetemp + ) + .as_str(), + params![], + )?; + conn.execute( format!( "create temporary table {} ( @@ -223,6 +241,15 @@ pub fn temp_tables(conn: &Connection) -> Result { params![], )?; + conn.execute( + format!( + "CREATE UNIQUE INDEX \"{}unq\" ON \"{}\" (\"id\")", + archivelinktemp, archivelinktemp + ) + .as_str(), + params![], + )?; + Ok(TempTableNames { notetemp, archivenotetemp, @@ -240,7 +267,11 @@ pub async fn sync( callbacks: &mut Callbacks, monitor: &dyn JobMonitor, ) -> Result> { - let conn = Arc::new(sqldata::connection_open(dbpath)?); + let conn = Arc::new(sqldata::connection_open( + "sync", + ConnFig::ReadWrite, + dbpath, + )?); let user = orgauth::dbfun::read_user_by_id(&conn, uid)?; // TODO pass this in from calling ftn? let extra_login_data = sqldata::read_extra_login_data(&conn, user.id)?; // let mut callbacks = &mut zknotes_callbacks(); @@ -362,7 +393,7 @@ pub async fn download_file( .map_err(|e| zkerr::Error::String(e.to_string()))?; if res.status() == 404 { - println!("404 for {}!", title); + info!("404 for {}!", title); return Ok(DownloadResult::DownloadFailed); } @@ -643,9 +674,13 @@ pub async fn sync_files_up( let mut line = String::new(); - let mut sm = read_sync_message(&mut line, &mut sr) - .map_err(|e| zkerr::Error::String(format!("read sync error: {:?}", e))) - .await?; + let mut resvec = Vec::new(); + + let mut sm = match read_sync_message(&mut line, &mut sr).await { + Err(zkerr::Error::EmptyStream) => return Ok(resvec), + Err(e) => return Err(zkerr::Error::String(format!("read sync error: {:?}", e))), + Ok(sm) => sm, + }; if let SyncMessage::ZkSearchResultHeader(ref _zsrh) = sm { } else { @@ -658,11 +693,12 @@ pub async fn sync_files_up( ); } - sm = read_sync_message(&mut line, &mut sr) - .map_err(|e| zkerr::Error::String(format!("read sync error: {:?}", e))) - .await?; + sm = match read_sync_message(&mut line, &mut sr).await { + Err(zkerr::Error::EmptyStream) => return Ok(resvec), + Err(e) => return Err(zkerr::Error::String(format!("read sync error: {:?}", e))), + Ok(sm) => sm, + }; - let mut resvec = Vec::new(); while let SyncMessage::ZkNoteId(ref id) = sm { let nid = note_id_for_uuid(&conn, &Uuid::from_str(id.as_str())?)?; @@ -741,6 +777,7 @@ pub async fn sync_from_remote( Some(archivelinktemp), callbacks, &mut sr, + Some(monitor), ) .await?; Ok(reply) @@ -773,6 +810,7 @@ pub async fn sync_from_stream( archivelinktemp: Option<&str>, callbacks: &mut Callbacks, br: &mut StreamReader, + monitor: Option<&dyn JobMonitor>, ) -> Result> where S: Stream> + Unpin, @@ -864,9 +902,15 @@ where sm = read_sync_message(&mut line, br).await?; - let serverhash = HashMap::::new(); + let mut serverhash = HashMap::::new(); + + let mut notecount: u64 = 0; while let SyncMessage::ZkNote(ref note, ref mbf) = sm { + notecount = notecount + 1; + if notecount % 100 == 0 { + monitor.map(|m| write!(m, "sync downloaded {} notes", notecount)); + } let uid = UserId::Uid( *userhash .get(note.user.to_i64()) @@ -923,22 +967,28 @@ where } }; - let server_id = match serverhash.get(¬e.server).ok_or_else(|| { - match server_id(&conn, note.server.as_str()) { - Ok(id) => Ok(id), - Err(_e) => conn - .execute( + let server_id: i64 = match serverhash.get(¬e.server) { + Some(id) => id.clone(), + None => match server_id(&conn, note.server.as_str()) { + Ok(id) => { + serverhash.insert(note.server.clone(), id); + id.clone() + } + Err(_e) => { + match conn.execute( "insert into server (uuid, createdate) values (?1, ?2)", params![note.server, now], - ) - .map(|_| conn.last_insert_rowid()) - .map_err(|e| zkerr::Error::from(e)), - } - }) { - Ok(id) => Ok(id.clone()), - Err(Ok(id)) => Ok(id), - Err(Err(e)) => Err(e), - }?; + ) { + Ok(_) => { + let id = conn.last_insert_rowid(); + serverhash.insert(note.server.clone(), id); + id.clone() + } + Err(e) => Err(zkerr::Error::from(e))?, + } + } + }, + }; let ex = conn.execute( "insert into zknote (title, content, user, pubid, editable, showtitle, deleted, uuid, file, server, createdate, changeddate) @@ -1028,10 +1078,15 @@ where sm = read_sync_message(&mut line, br).await?; } + info!("synced {} zknotes", notecount); + monitor.map(|m| write!(m, "sync downloaded {} notes total", notecount)); + // ---------------------------------------------------------------------------------- // archive notes // ---------------------------------------------------------------------------------- // After the current notes are the archivenotes. + // + notecount = 0; if let SyncMessage::ZkSearchResultHeader(ref _zsrh) = sm { } else { return Err( @@ -1045,6 +1100,10 @@ where sm = read_sync_message(&mut line, br).await?; while let SyncMessage::ZkNote(ref note, ref _mbf) = sm { + notecount = notecount + 1; + if notecount % 100 == 0 { + monitor.map(|m| write!(m, "sync downloaded {} archive notes", notecount)); + } // TODO: make file source record (?) let uid = UserId::Uid( @@ -1123,6 +1182,9 @@ where sm = read_sync_message(&mut line, br).await?; } + info!("synced {} archive zknotes", notecount); + monitor.map(|m| write!(m, "sync downloaded {} archive notes total", notecount)); + if let SyncMessage::ArchiveZkLinkHeader = sm { } else { return Err( @@ -1168,7 +1230,7 @@ where // actually there is no unique index on zklinkarchive so can this even happen? if e.code == rusqlite::ErrorCode::ConstraintViolation { // if duplicate record, just ignore and go on. - println!("duplicate archivelink: {:?}", l); + warn!("duplicate archivelink: {:?}", l); None } else { return Err(rusqlite::Error::SqliteFailure(e, s).into()); @@ -1190,7 +1252,7 @@ where // actually there is no unique index on zklinkarchive so can this even happen? if e.code == rusqlite::ErrorCode::ConstraintViolation { // if duplicate record, just ignore and go on. - println!("duplicate archivelink: {:?}", l); + warn!("duplicate archivelink: {:?}", l); None } else { return Err(rusqlite::Error::SqliteFailure(e, s).into()); @@ -1430,11 +1492,10 @@ pub async fn sync_to_remote( .cookie(cookie) .timeout(Duration::from_secs(60 * 60)) .send_body(awc::body::BodyStream::new(ss)) - .map_err(|e| zkerr::Error::String(e.to_string())) + .map_err(|e| zkerr::Error::String(format!("sync_to_remote send_body err {}", e.to_string()))) .await?; // TODO: send back Sync results... how many records and etc. - Ok(PrivateReply::PvySyncComplete) } diff --git a/server-lib/src/synctest.rs b/server-lib/src/synctest.rs index 68addffa..e642128a 100644 --- a/server-lib/src/synctest.rs +++ b/server-lib/src/synctest.rs @@ -480,7 +480,7 @@ mod tests { fs::copy(dbp_server, Path::new("server.db.saved"))?; } } - let server_conn = connection_open(dbp_server)?; + let server_conn = connection_open("dbp_server", dbp_server)?; let server_ts = setup_db(&server_conn, &mut cb, "".to_string(), None, None, "server").await?; println!("0.1"); @@ -506,7 +506,7 @@ mod tests { fs::copy(dbp_client, Path::new("client.db.saved"))?; } } - let client_conn = connection_open(dbp_client)?; + let client_conn = connection_open("dbp_client", dbp_client)?; let client_ts = setup_db( &client_conn, &mut cb, diff --git a/server/default.toml b/server/default.toml index 053dedf1..a53a6605 100644 --- a/server/default.toml +++ b/server/default.toml @@ -21,4 +21,4 @@ invite_token_expiration_ms = 604800000 open_registration = true send_emails = false non_admin_invite = true -remote_registration = false +remote_registration = true diff --git a/server/static/index.html b/server/static/index.html index 124cf129..cb5fd680 100644 --- a/server/static/index.html +++ b/server/static/index.html @@ -3,46 +3,47 @@ - + -
+
diff --git a/websocket b/websocket new file mode 160000 index 00000000..3bab6300 --- /dev/null +++ b/websocket @@ -0,0 +1 @@ +Subproject commit 3bab63000d0630e482af3c494b693de9265cf1b1