diff --git a/docs/docusaurus/static/img/APIs/FTP.png b/docs/docusaurus/static/img/APIs/FTP.png new file mode 100644 index 00000000000..44d419eed50 Binary files /dev/null and b/docs/docusaurus/static/img/APIs/FTP.png differ diff --git a/docs/docusaurus/static/img/APIs/small/FTP.png b/docs/docusaurus/static/img/APIs/small/FTP.png new file mode 100644 index 00000000000..7b3255d99ce Binary files /dev/null and b/docs/docusaurus/static/img/APIs/small/FTP.png differ diff --git a/src/addins/ftp/Cargo.lock b/src/addins/ftp/Cargo.lock new file mode 100644 index 00000000000..9473a841a77 --- /dev/null +++ b/src/addins/ftp/Cargo.lock @@ -0,0 +1,791 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addin1c" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef34e8b7ff4c43e87491a4cc30a4779a9f67c50db43378a36362c7a56246e05b" +dependencies = [ + "smallvec", + "utf16_lit", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy-regex" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60c7310b93682b36b98fa7ea4de998d3463ccbebd94d935d6b48ba5b6ffa7126" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba01db5ef81e17eb10a5e0f2109d1b3a3e29bac3070fdbd7d156bf7dbd206a1" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn", +] + +[[package]] +name = "libc" +version = "0.2.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" + +[[package]] +name = "linux-raw-sys" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" + +[[package]] +name = "log" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" + +[[package]] +name = "openssl" +version = "0.10.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "opi_mysql" +version = "0.1.0" +dependencies = [ + "addin1c", + "native-tls", + "serde", + "serde_json", + "socks", + "suppaftp", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustix" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.134" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socks" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" +dependencies = [ + "byteorder", + "libc", + "winapi", +] + +[[package]] +name = "suppaftp" +version = "6.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d869e942cc5f349ad91645925a9e6b570f62c4c170ad1c7b92b867bd16bd54" +dependencies = [ + "chrono", + "futures-lite", + "lazy-regex", + "log", + "native-tls", + "thiserror", +] + +[[package]] +name = "syn" +version = "2.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ae51629bf965c5c098cc9e87908a3df5301051a9e087d6f9bef5c9771ed126" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "utf16_lit" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14706d2a800ee8ff38c1d3edb873cd616971ea59eb7c0d046bb44ef59b06a1ae" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] diff --git a/src/addins/ftp/Cargo.toml b/src/addins/ftp/Cargo.toml new file mode 100644 index 00000000000..1a375c67df3 --- /dev/null +++ b/src/addins/ftp/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "opi_mysql" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[profile.release] +lto = "fat" # Enable Link Time Optimization +codegen-units = 1 # Reduce number of codegen units to increase optimizations. +panic = "abort" # Abort on panic +strip = true # Automatically strip symbols from the binary. +opt-level = "z" + +[dependencies] +addin1c = "0.5.0" +suppaftp = { version = "6.3.0", features = ["native-tls", "secure"] } +serde_json = "1.0" +serde = { version = "1.0.217", features = ["derive"] } +native-tls = "0.2.14" +socks = "0.3.4" \ No newline at end of file diff --git a/src/addins/ftp/dependencies.log b/src/addins/ftp/dependencies.log new file mode 100644 index 00000000000..211e0974241 --- /dev/null +++ b/src/addins/ftp/dependencies.log @@ -0,0 +1,15 @@ +"MAIN ---" + linux-vdso.so.1 (0x00007ffec07be000) + libssl.so.3 => /lib64/libssl.so.3 (0x000075673a000000) + libcrypto.so.3 => /lib64/libcrypto.so.3 (0x0000756739800000) + libm.so.6 => /lib64/libm.so.6 (0x0000756739400000) + libpthread.so.0 => /lib64/libpthread.so.0 (0x0000756739000000) + libc.so.6 => /lib64/libc.so.6 (0x0000756738c00000) + libdl.so.2 => /lib64/libdl.so.2 (0x0000756738800000) + /lib64/ld-linux-x86-64.so.2 (0x000075673a600000) + libz.so.1 => /lib64/libz.so.1 (0x0000756738400000) +GLIBC_2.2.5 +GLIBC_2.3 +GLIBC_2.3.4 +GLIBC_2.14 +GLIBC_2.17 diff --git a/src/addins/ftp/release.bat b/src/addins/ftp/release.bat new file mode 100644 index 00000000000..6ae71a3ea46 --- /dev/null +++ b/src/addins/ftp/release.bat @@ -0,0 +1,9 @@ +@echo off + +:: Установить переменную +set CARGO_NAME=opi_mysql +set LIB_NAME=OPI_MySQL +set ADDIN_DIR=%~dp0 +set FOLV=true + +call ../build.bat \ No newline at end of file diff --git a/src/addins/ftp/src/component/mod.rs b/src/addins/ftp/src/component/mod.rs new file mode 100644 index 00000000000..9e2dbee32d4 --- /dev/null +++ b/src/addins/ftp/src/component/mod.rs @@ -0,0 +1,364 @@ +mod ftp_client; + +use addin1c::{name, Variant}; +use crate::core::getset; +use serde_json::json; +use suppaftp::{FtpStream, NativeTlsConnector, NativeTlsFtpStream}; +use suppaftp::native_tls::TlsConnector; +use suppaftp::types::Mode; +use serde::Deserialize; +use crate::component::ftp_client::FtpClient; +use std::net::TcpStream; +use std::sync::{Arc, Mutex}; +use socks::{Socks4Stream, Socks5Stream}; + +// МЕТОДЫ КОМПОНЕНТЫ ------------------------------------------------------------------------------- + +// Синонимы +pub const METHODS: &[&[u16]] = &[ + name!("Connect"), + name!("UpdateSettings"), + name!("UpdateProxy"), + name!("SetTLS") +]; + +// Число параметров функций компоненты +pub fn get_params_amount(num: usize) -> usize { + match num { + 0 => 0, + 1 => 1, + 2 => 1, + 3 => 3, + _ => 0, + } +} + +// Соответствие функций Rust функциям компоненты +// Вызовы должны быть обернуты в Box::new +pub fn cal_func(obj: &mut AddIn, num: usize, params: &mut [Variant]) -> Box { + + match num { + + 0 => Box::new(obj.initialize()), + 1 => { + let json_string = params[0].get_string().unwrap_or("".to_string()); + Box::new(obj.update_settings(&json_string)) + }, + 2 => { + let json_string = params[0].get_string().unwrap_or("".to_string()); + Box::new(obj.update_proxy(&json_string)) + }, + 3 => { + + let use_tls = params[0].get_bool().unwrap_or(false); + let accept_invalid_certs = params[1].get_bool().unwrap_or(false); + let ca_cert_path = params[2].get_string().unwrap_or("".to_string()); + + Box::new(obj.set_tls(use_tls, accept_invalid_certs, &ca_cert_path)) + + } + _ => Box::new(false), // Неверный номер команды + } + +} + +// ------------------------------------------------------------------------------------------------- + +// ПОЛЯ КОМПОНЕНТЫ --------------------------------------------------------------------------------- + +// Синонимы +pub const PROPS: &[&[u16]] = &[]; + +#[derive(Deserialize, Debug)] +struct FtpProxySettings { + server: String, + port: u16, + login: Option, + password: Option, + proxy_type: Option, // "http", "socks4", "socks5" +} + +#[derive(Deserialize, Debug)] +struct FtpSettings { + domain: Option, + port: Option, + login: Option, + password: Option, + passive: Option, + timeout: Option, +} + +pub struct AddIn { + domain: String, + port: u16, + login: Option, + password: Option, + passive: bool, + timeout: u64, + // TLS + use_tls: bool, + accept_invalid_certs: bool, + ca_cert_path: String, + client: Option>>, + // Proxy + proxy_server: Option, + proxy_port: Option, + proxy_login: Option, + proxy_password: Option, + proxy_type: Option, +} + +impl AddIn { + + pub fn new() -> Self { + AddIn { + client: None, + domain: String::new(), + port: 21, + login: None, + password: None, + passive: true, + timeout: 120, + use_tls: false, + accept_invalid_certs: false, + ca_cert_path: String::new(), + // Инициализация полей прокси + proxy_server: None, + proxy_port: None, + proxy_login: None, + proxy_password: None, + proxy_type: None, + } + } + + pub fn update_settings(&mut self, json_data: &str) -> String { + + let json_struct: FtpSettings = match serde_json::from_str(json_data){ + Ok(s) => s, + Err(e) => return Self::process_error(&e.to_string()), + }; + + if let Some(domain) = json_struct.domain { + self.domain = domain; + } + + if let Some(port) = json_struct.port { + self.port = port; + } + + if let Some(passive) = json_struct.passive { + self.passive = passive; + } + + if let Some(timeout) = json_struct.timeout { + self.timeout = timeout; + } + + self.login = json_struct.login.or(self.login.clone()); + self.password = json_struct.password.or(self.password.clone()); + + json!({"result": true}).to_string() + + } + + pub fn update_proxy(&mut self, json_data: &str) -> String { + + let json_struct: FtpProxySettings = match serde_json::from_str(json_data){ + Ok(s) => s, + Err(e) => return Self::process_error(&e.to_string()), + }; + + self.proxy_server = Some(json_struct.server); + self.proxy_port = Some(json_struct.port); + self.proxy_login = json_struct.login; + self.proxy_password = json_struct.password; + self.proxy_type = json_struct.proxy_type; + + json!({"result": true}).to_string() + + } + + pub fn initialize(&mut self) -> String { + + if self.domain.is_empty() { + return Self::process_error("Address must be initialized"); + } + + let tcp_stream = match self.create_tcp_connection() { + Ok(stream) => stream, + Err(e) => return e, + }; + + let client = match self.configure_ftp_client(tcp_stream) { + Ok(client) => client, + Err(e) => return e, + }; + + let login: Option<&str> = self.login.as_deref(); + let password: Option<&str> = self.password.as_deref(); + + self.client = match client.login(login, password) { + Ok(auth) => Some(Arc::new(Mutex::new(auth))), + Err(e) => return Self::process_error(&e.to_string()), + }; + + json!({"result": true}).to_string() + } + + fn configure_ftp_client( + &mut self, + tcp_stream: TcpStream, + ) -> Result { + + let mode = if self.passive { Mode::Passive } else { Mode::Active }; + + if self.use_tls { + + let tls_connector = self.get_tls_connector()?; + + let ftp_stream = NativeTlsFtpStream::connect_with_stream(tcp_stream) + .map_err(|e| Self::process_error(&e.to_string()))?; + + let mut secure_stream = ftp_stream + .into_secure(NativeTlsConnector::from(tls_connector), &self.domain) + .map_err(|e| Self::process_error(&e.to_string()))?; + + secure_stream.set_mode(mode); + secure_stream.set_passive_nat_workaround(true); + Ok(FtpClient::Secure(secure_stream)) + + } else { + + let mut ftp_stream = FtpStream::connect_with_stream(tcp_stream) + .map_err(|e| Self::process_error(&e.to_string()))?; + + ftp_stream.set_mode(mode); + ftp_stream.set_passive_nat_workaround(true); + Ok(FtpClient::Insecure(ftp_stream)) + } + } + + fn create_tcp_connection(&self) -> Result { + + if let (Some(proxy_server), Some(proxy_port), Some(proxy_type)) = + (&self.proxy_server, &self.proxy_port, &self.proxy_type) + { + + let target_addr = (self.domain.as_str(), self.port); + let proxy_addr = format!("{}:{}", proxy_server, proxy_port); + + match proxy_type.to_lowercase().as_str() { + "socks5" => self.connect_via_socks5(&proxy_addr, target_addr), + "socks4" => self.connect_via_socks4(&proxy_addr, target_addr), + _ => Err(Self::process_error("Unsupported proxy type")), + } + } else { + self.connect_direct() + } + } + + fn connect_via_socks5(&self, proxy_addr: &str, target_addr: (&str, u16)) -> Result { + + let stream = if let (Some(user), Some(pass)) = (&self.proxy_login, &self.proxy_password) { + Socks5Stream::connect_with_password(proxy_addr, target_addr, user, pass) + } else { + Socks5Stream::connect(proxy_addr, target_addr) + }; + + stream.map(|s| s.into_inner()) + .map_err(|e| Self::process_error(&format!("SOCKS5 error: {}", e))) + } + + fn connect_via_socks4(&self, proxy_addr: &str, target_addr: (&str, u16)) -> Result { + + let stream = if let Some(user) = &self.proxy_login { + Socks4Stream::connect(proxy_addr, target_addr, user) + } else { + Socks4Stream::connect(proxy_addr, target_addr, "") + }; + + stream.map(|s| s.into_inner()) + .map_err(|e| Self::process_error(&format!("SOCKS4 error: {}", e))) + } + + fn connect_direct(&self) -> Result { + let addr = format!("{}:{}", &self.domain, &self.port); + TcpStream::connect(&addr).map_err(|e| Self::process_error(&format!("Direct connection error: {}", e))) + } + + pub fn close_connection(&mut self) -> String { + if let Some(client) = self.client.take() { + match client.lock() { + Ok(mut locked_client) => { + match &mut *locked_client { + FtpClient::Secure(stream) => _ = stream.quit(), + FtpClient::Insecure(stream) => _ = stream.quit(), + } + } + Err(e) => return Self::process_error(&format!("Failed to lock client: {}", e)), + } + } + json!({"result": true}).to_string() + } + + pub fn set_tls(&mut self, use_tls: bool, accept_invalid_certs: bool, ca_cert_path: &str) -> String { + + if self.client.is_some(){ + return Self::process_error("TLS settings can only be set before the connection is established"); + }; + + self.accept_invalid_certs = accept_invalid_certs; + self.ca_cert_path = ca_cert_path.to_string(); + self.use_tls = use_tls; + + json!({"result": true}).to_string() + } + + fn get_tls_connector(&mut self) -> Result { + + let mut tls_builder = TlsConnector::builder(); + + tls_builder.danger_accept_invalid_certs(self.accept_invalid_certs); + + if !self.ca_cert_path.is_empty() { + + let cert_data = std::fs::read(&self.ca_cert_path) + .map_err(|e| Self::process_error(&e.to_string()))?; + + let cert = native_tls::Certificate::from_pem(&cert_data) + .map_err(|e| Self::process_error(&e.to_string()))?; + + tls_builder.add_root_certificate(cert); + } + + match tls_builder.build(){ + Ok(connector) => Ok(connector), + Err(e) => Err(Self::process_error(&e.to_string())), + } + } + + fn process_error(e: &str) -> String{ + json!({ + "result": false, + "error": e + }).to_string() + } + + pub fn get_field_ptr(&self, index: usize) -> *const dyn getset::ValueType { + match index { + _ => panic!("Index out of bounds"), + } + } + pub fn get_field_ptr_mut(&mut self, index: usize) -> *mut dyn getset::ValueType { self.get_field_ptr(index) as *mut _ } +} + + +// ------------------------------------------------------------------------------------------------- + +// УНИЧТОЖЕНИЕ ОБЪЕКТА ----------------------------------------------------------------------------- + +// Обработка удаления объекта +impl Drop for AddIn { + fn drop(&mut self) {} +} + diff --git a/src/addins/ftp/src/core/getset.rs b/src/addins/ftp/src/core/getset.rs new file mode 100644 index 00000000000..dfb6f65bc80 --- /dev/null +++ b/src/addins/ftp/src/core/getset.rs @@ -0,0 +1,78 @@ +use addin1c::{Variant, Tm}; + + +pub trait ValueType { + fn get_value(&self, val: &mut Variant) -> bool; + fn set_value(&mut self, val: &Variant); +} + +// Реализация для i32 +impl ValueType for i32 { + fn get_value(&self, val: &mut Variant) -> bool { + val.set_i32(*self); + true + } + + fn set_value(&mut self, val: &Variant) { + *self = val.get_i32().unwrap_or(0); + } +} + +// Реализация для f64 +impl ValueType for f64 { + fn get_value(&self, val: &mut Variant) -> bool { + val.set_f64(*self); + true + } + + fn set_value(&mut self, val: &Variant) { + *self = val.get_f64().unwrap_or(0.0); + } +} + +// Реализация для bool +impl ValueType for bool { + fn get_value(&self, val: &mut Variant) -> bool { + val.set_bool(*self); + true + } + + fn set_value(&mut self, val: &Variant) { + *self = val.get_bool().unwrap_or(false); + } +} + +// Реализация для tm +impl ValueType for Tm { + fn get_value(&self, val: &mut Variant) -> bool { + val.set_date(*self); + true + } + + fn set_value(&mut self, val: &Variant) { + *self = val.get_date().unwrap_or(Tm::default()); + } +} + +// Реализация для String +impl ValueType for String { + fn get_value(&self, val: &mut Variant) -> bool { + let s: Vec = self.encode_utf16().collect(); + val.set_str1c(s.as_slice()).is_ok() + } + + fn set_value(&mut self, val: &Variant) { + *self = val.get_string().unwrap_or("".to_string()); + } +} + +// Реализация для Vec +impl ValueType for Vec { + fn get_value(&self, val: &mut Variant) -> bool { + val.set_blob(self.as_slice()).is_ok() + } + + fn set_value(&mut self, val: &Variant) { + *self = val.get_blob().unwrap_or(&[]).to_vec() + } +} \ No newline at end of file diff --git a/src/addins/ftp/src/core/mod.rs b/src/addins/ftp/src/core/mod.rs new file mode 100644 index 00000000000..9f8e10c39f0 --- /dev/null +++ b/src/addins/ftp/src/core/mod.rs @@ -0,0 +1,53 @@ +pub mod getset; + +use addin1c::{name, RawAddin, Variant}; + +use crate::component::METHODS; +use crate::component::PROPS; +use crate::component::get_params_amount; +use crate::component::cal_func; +use crate::component::AddIn; + +// Определение класса +impl RawAddin for AddIn { + + fn register_extension_as(&mut self) -> &'static [u16] { + name!("Main") + } + fn get_n_props(&mut self) -> usize { + PROPS.len() + } + fn find_prop(&mut self, name: &[u16]) -> Option { + PROPS.iter().position(|&x| x == name) + } + fn get_prop_name(&mut self, num: usize, _alias: usize) -> Option<&'static [u16]> { PROPS.get(num).copied() } + fn get_prop_val(&mut self, num: usize, val: &mut Variant) -> bool {let field: &dyn getset::ValueType = &self[num]; field.get_value(val) } + fn set_prop_val(&mut self, num: usize, val: &Variant) -> bool {let field: &mut dyn getset::ValueType = &mut self[num]; field.set_value(val); true } + fn is_prop_readable(&mut self, _num: usize) -> bool { true } + fn is_prop_writable(&mut self, _num: usize) -> bool { true } + fn get_n_methods(&mut self) -> usize { METHODS.len() } + fn find_method(&mut self, name: &[u16]) -> Option { METHODS.iter().position(|&x| x == name) } + fn get_method_name(&mut self, num: usize, _alias: usize) -> Option<&'static [u16]> { METHODS.get(num).copied() } + fn get_n_params(&mut self, num: usize) -> usize { get_params_amount(num) } + fn get_param_def_value(&mut self, _method_num: usize, _param_num: usize, _value: Variant, ) -> bool { true } + fn has_ret_val(&mut self, _num: usize) -> bool { true } + fn call_as_proc(&mut self, _num: usize, _params: &mut [Variant]) -> bool { false } + fn call_as_func(&mut self, num: usize, params: &mut [Variant], ret_value: &mut Variant, ) -> bool { cal_func(self, num, params).get_value(ret_value) } + +} + +impl std::ops::Index for AddIn { + type Output = dyn getset::ValueType; + + fn index(&self, index: usize) -> &Self::Output { + unsafe { &*self.get_field_ptr(index) } + } +} + +impl std::ops::IndexMut for AddIn { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + unsafe { &mut *self.get_field_ptr_mut(index) } + } +} + + diff --git a/src/addins/ftp/src/lib.rs b/src/addins/ftp/src/lib.rs new file mode 100644 index 00000000000..6b7c2f62a10 --- /dev/null +++ b/src/addins/ftp/src/lib.rs @@ -0,0 +1,49 @@ +pub mod component; +mod core; + + +use std::{ + ffi::{c_int, c_long, c_void}, + sync::atomic::{AtomicI32, Ordering}, +}; + +use component::AddIn; +use addin1c::{create_component, destroy_component, name, AttachType}; + +pub static mut PLATFORM_CAPABILITIES: AtomicI32 = AtomicI32::new(-1); + +#[allow(non_snake_case)] +#[no_mangle] +pub unsafe extern "C" fn GetClassObject(_name: *const u16, component: *mut *mut c_void) -> c_long { + + let addin = AddIn::new(); + create_component(component, addin) + +} + +#[allow(non_snake_case)] +#[no_mangle] +pub unsafe extern "C" fn DestroyObject(component: *mut *mut c_void) -> c_long { + destroy_component(component) +} + +#[allow(non_snake_case)] +#[no_mangle] +pub extern "C" fn GetClassNames() -> *const u16 { + // small strings for performance + name!("Main").as_ptr() +} + +#[allow(non_snake_case)] +#[no_mangle] +#[allow(static_mut_refs)] +pub unsafe extern "C" fn SetPlatformCapabilities(capabilities: c_int) -> c_int { + PLATFORM_CAPABILITIES.store(capabilities, Ordering::Relaxed); + 3 +} + +#[allow(non_snake_case)] +#[no_mangle] +pub extern "C" fn GetAttachType() -> AttachType { + AttachType::Any +} diff --git a/src/ru/OPI/src/CommonModules/OPI_FTP/Module.bsl b/src/ru/OPI/src/CommonModules/OPI_FTP/Module.bsl new file mode 100644 index 00000000000..ef6f012b70b --- /dev/null +++ b/src/ru/OPI/src/CommonModules/OPI_FTP/Module.bsl @@ -0,0 +1,50 @@ +// OneScript: ./OInt/core/Modules/OPI_FTP.os +// Lib: FTP +// CLI: ftp +// Keywords: ftp, ftps + +// MIT License + +// Copyright (c) 2023-2025 Anton Tsitavets + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// https://github.com/Bayselonarrend/OpenIntegrations + +// BSLLS:Typo-off +// BSLLS:LatinAndCyrillicSymbolInWord-off +// BSLLS:IncorrectLineBreak-off +// BSLLS:NumberOfOptionalParams-off +// BSLLS:UsingServiceTag-off +// BSLLS:LineLength-off +// BSLLS:UsingSynchronousCalls-off +// BSLLS:MagicNumber-off + +//@skip-check module-structure-top-region +//@skip-check module-structure-method-in-regions +//@skip-check wrong-string-literal-content +//@skip-check method-too-many-params + +#Область ПрограммныйИнтерфейс + +#Область ОсновныеМетоды + +#КонецОбласти + +#КонецОбласти diff --git a/src/ru/OPI/src/CommonModules/OPI_FTP/OPI_FTP.mdo b/src/ru/OPI/src/CommonModules/OPI_FTP/OPI_FTP.mdo new file mode 100644 index 00000000000..c4c4919c31e --- /dev/null +++ b/src/ru/OPI/src/CommonModules/OPI_FTP/OPI_FTP.mdo @@ -0,0 +1,11 @@ + + + OPI_FTP + + ru + FTP (ОПИ) + + true + true + true + diff --git a/src/ru/OPI/src/CommonTemplates/OPI_FTP/OPI_FTP.mdo b/src/ru/OPI/src/CommonTemplates/OPI_FTP/OPI_FTP.mdo new file mode 100644 index 00000000000..6f26d9a3b29 --- /dev/null +++ b/src/ru/OPI/src/CommonTemplates/OPI_FTP/OPI_FTP.mdo @@ -0,0 +1,9 @@ + + + OPI_FTP + + ru + FTP (OPI) + + AddIn +