cheogram-www
created pr with
cheogram-www/tauri-media-handling.1
commented
The following changes since commit 62ea2af732f009407e7d2de6588cd10657f4e89b:
npm run tauri init (2026-10-28 10:28:04 -0400)
are available in the Git repository at:
https://tangled.org/mrry.band/cheogram-www tauri-media-handling
for you to fetch changes up to d1d3a238ce8271bab76def99868cfaf5402e1a54:
Tauri file system media store (2026-10-28 11:11:55 -0400)
----------------------------------------------------------------
Eric Roberts (1):
Tauri file system media store
package.json | 1 +
src-tauri/Cargo.lock | 231 +++++++-----
src-tauri/Cargo.toml | 9 +
src-tauri/src/lib.rs | 50 ++-
src-tauri/src/media.rs | 460 ++++++++++++++++++++++
src-tauri/src/media/store.rs | 588 +++++++++++++++++++++++++++++
src-tauri/tauri.conf.json | 1 +
src/App.tsx | 2 +-
src/components/MessageAttachments.test.tsx | 25 ++
src/components/MessageAttachments.tsx | 10 +-
src/media-store-tauri.test.ts | 89 +++++
src/media-store-tauri.ts | 49 +++
src/media-store.ts | 7 +
13 files changed, 1421 insertions(+), 101 deletions(-)
create mode 100644 src-tauri/src/media.rs
create mode 100644 src-tauri/src/media/store.rs
create mode 100644 src/media-store-tauri.test.ts
create mode 100644 src/media-store-tauri.ts
create mode 100644 src/media-store.ts
added cheogram-www/tauri-media-handling.2
1: d1d3a23 ! 1: 5c9c8ae Tauri file system media store
added cheogram-www/tauri-media-handling.3
1: 5c9c8ae < -: ------- Tauri file system media store
-: ------- > 1: 615caaf Review comments
added cheogram-www/tauri-media-handling.4
1: 615caaf < -: ------- Review comments
-: ------- > 1: 58e4016 Tauri file system media store
added cheogram-www/tauri-media-handling.5
1: 58e4016 = 1: b0c5136 Tauri file system media store
added cheogram-www/tauri-media-handling.6
1: b0c5136 ! 1: f6bda1a Tauri file system media store
added cheogram-www/tauri-media-handling.7
1: f6bda1a ! 1: efe5296 Tauri file system media store
cmds
checkout latest patchset:
ssh pr.pico.sh pull cheogram-www:tauri-media-handling | git am -3checkout specific patchset revision:
ssh pr.pico.sh pull cheogram-www:tauri-media-handling [rev] | git am -3add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh cheogram-www:tauri-media-handlingadd comment to patch request:
ssh pr.pico.sh comment cheogram-www:tauri-media-handling "lgtm!"
Patchset
cheogram-www/tauri-media-handling.7
Tauri file system media store
Eric Roberts
2026-10-16T18:10:16ZSemantic diff summary
81 added,
7 modified,
0 signature changed,
0 removed
across 11 analyzed files
(4 files skipped: unsupported file type)
src-tauri/src/media.rs
-
function_itemparse_pathadded -
function_itemerror_responseadded -
function_itemwait_responseadded -
function_itemhashadded -
struct_itemFixtureadded -
function_itempublishadded -
function_itemempty_and_interleaved_uploads_have_independent_hashesadded -
function_itemwrite_mediaadded -
function_itemfinish_mediaadded -
function_itemhas_mediaadded -
function_itemfinish_consumes_resourceadded -
function_itembegin_mediaadded -
function_itemopen_navigationadded -
function_itemprotocoladded -
function_itemhash_urladded -
function_itemvalidates_paths_and_publicationadded -
function_itempending_request_loads_after_publicationadded -
struct_itemUploadResourceadded -
function_itemremove_mediaadded -
function_itemnewadded -
function_itembeginadded
src-tauri/src/media/store.rs
-
function_itempublishadded -
function_itembeginadded -
struct_itemMediaCacheadded -
function_itemreadadded -
struct_itemFixtureadded -
function_itemopening_copies_aliases_with_mime_extensionsadded -
function_itemopening_jpeg_uses_jpg_instead_of_jfifadded -
function_itemserves_aliases_and_mime_after_restartadded -
function_itemprepare_openadded -
function_itemnewadded -
function_itemduplicates_and_deletion_work_for_both_algorithmsadded -
function_itemmedia_pathadded -
function_itemexistsadded -
function_itemhasadded -
function_itemhashadded -
function_itemhash_urladded -
function_itemreopening_reuses_the_path_and_restores_cached_contentsadded -
function_itemvalidates_disk_aliasesadded -
struct_itemResolvedMediaadded -
function_itemmime_pathadded -
function_itemmimeadded -
function_itemwrite_mimeadded -
function_itemresponseadded -
function_itemopening_generic_binary_detects_content_or_uses_binadded -
function_itemremoveadded -
function_itemremove_fileadded -
function_itemresolveadded -
function_iteminvalid_mime_sidecars_return_io_errorsadded -
function_itemmissing_or_evicted_files_and_old_files_without_mime_are_missesadded -
function_itemwrite_aliasadded -
function_itemhexadded -
function_itemalias_pathadded -
function_itemopening_uses_the_original_filename_or_falls_back_to_mimeadded
src-tauri/src/media/upload_slot.rs
-
struct_itemUploadSlotadded -
function_itemwriteadded -
function_itemfinishadded -
function_itemupload_stores_plain_text_mime_and_returns_a_urladded -
function_itemwrite_failure_cleans_up_temporary_file_on_dropadded -
function_itempublication_failures_clean_up_temporary_filesadded -
function_itemclose_and_resource_drop_remove_partial_uploadsadded -
function_itemnewadded -
function_itemhashadded -
function_itemhash_urladded
+1
-0
package.json
#
| ... | ... | @@ -20,6 +20,7 @@ | |
| 20 | 20 | "@privacyresearch/libsignal-protocol-typescript": "^0.0.16", | |
| 21 | 21 | "@sentry/browser": "^10.52.0", | |
| 22 | 22 | "@sqlite.org/sqlite-wasm": "^3.54.1-build1", | |
| 23 | + | "@tauri-apps/api": "^2.12.4", | |
| 23 | 24 | "@types/react": "<= 18.2.0", | |
| 24 | 25 | "@types/react-dom": "<= 18.2.0", | |
| 25 | 26 | "borogove": "^0.0.1", |
+670
-121
src-tauri/Cargo.lock
#
| ... | ... | @@ -65,15 +65,134 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 65 | 65 | checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" | |
| 66 | 66 | ||
| 67 | 67 | [[package]] | |
| 68 | - | name = "cheogram-www" | |
| 69 | - | version = "0.0.1" | |
| 68 | + | name = "async-broadcast" | |
| 69 | + | version = "0.7.2" | |
| 70 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 71 | + | checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" | |
| 70 | 72 | dependencies = [ | |
| 71 | - | "log", | |
| 72 | - | "serde", | |
| 73 | - | "serde_json", | |
| 74 | - | "tauri", | |
| 75 | - | "tauri-build", | |
| 76 | - | "tauri-plugin-log", | |
| 73 | + | "event-listener", | |
| 74 | + | "event-listener-strategy", | |
| 75 | + | "futures-core", | |
| 76 | + | "pin-project-lite", | |
| 77 | + | ] | |
| 78 | + | ||
| 79 | + | [[package]] | |
| 80 | + | name = "async-channel" | |
| 81 | + | version = "2.5.0" | |
| 82 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 83 | + | checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" | |
| 84 | + | dependencies = [ | |
| 85 | + | "concurrent-queue", | |
| 86 | + | "event-listener-strategy", | |
| 87 | + | "futures-core", | |
| 88 | + | "pin-project-lite", | |
| 89 | + | ] | |
| 90 | + | ||
| 91 | + | [[package]] | |
| 92 | + | name = "async-executor" | |
| 93 | + | version = "1.14.0" | |
| 94 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 95 | + | checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" | |
| 96 | + | dependencies = [ | |
| 97 | + | "async-task", | |
| 98 | + | "concurrent-queue", | |
| 99 | + | "fastrand", | |
| 100 | + | "futures-lite", | |
| 101 | + | "pin-project-lite", | |
| 102 | + | "slab", | |
| 103 | + | ] | |
| 104 | + | ||
| 105 | + | [[package]] | |
| 106 | + | name = "async-io" | |
| 107 | + | version = "2.6.0" | |
| 108 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 109 | + | checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" | |
| 110 | + | dependencies = [ | |
| 111 | + | "autocfg", | |
| 112 | + | "cfg-if", | |
| 113 | + | "concurrent-queue", | |
| 114 | + | "futures-io", | |
| 115 | + | "futures-lite", | |
| 116 | + | "parking", | |
| 117 | + | "polling", | |
| 118 | + | "rustix", | |
| 119 | + | "slab", | |
| 120 | + | "windows-sys 0.61.2", | |
| 121 | + | ] | |
| 122 | + | ||
| 123 | + | [[package]] | |
| 124 | + | name = "async-lock" | |
| 125 | + | version = "3.4.2" | |
| 126 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 127 | + | checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" | |
| 128 | + | dependencies = [ | |
| 129 | + | "event-listener", | |
| 130 | + | "event-listener-strategy", | |
| 131 | + | "pin-project-lite", | |
| 132 | + | ] | |
| 133 | + | ||
| 134 | + | [[package]] | |
| 135 | + | name = "async-process" | |
| 136 | + | version = "2.5.0" | |
| 137 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 138 | + | checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" | |
| 139 | + | dependencies = [ | |
| 140 | + | "async-channel", | |
| 141 | + | "async-io", | |
| 142 | + | "async-lock", | |
| 143 | + | "async-signal", | |
| 144 | + | "async-task", | |
| 145 | + | "blocking", | |
| 146 | + | "cfg-if", | |
| 147 | + | "event-listener", | |
| 148 | + | "futures-lite", | |
| 149 | + | "rustix", | |
| 150 | + | ] | |
| 151 | + | ||
| 152 | + | [[package]] | |
| 153 | + | name = "async-recursion" | |
| 154 | + | version = "1.2.0" | |
| 155 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 156 | + | checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451" | |
| 157 | + | dependencies = [ | |
| 158 | + | "proc-macro2", | |
| 159 | + | "quote", | |
| 160 | + | "syn 3.0.9", | |
| 161 | + | ] | |
| 162 | + | ||
| 163 | + | [[package]] | |
| 164 | + | name = "async-signal" | |
| 165 | + | version = "0.2.14" | |
| 166 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 167 | + | checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" | |
| 168 | + | dependencies = [ | |
| 169 | + | "async-io", | |
| 170 | + | "async-lock", | |
| 171 | + | "atomic-waker", | |
| 172 | + | "cfg-if", | |
| 173 | + | "futures-core", | |
| 174 | + | "futures-io", | |
| 175 | + | "rustix", | |
| 176 | + | "signal-hook-registry", | |
| 177 | + | "slab", | |
| 178 | + | "windows-sys 0.61.2", | |
| 179 | + | ] | |
| 180 | + | ||
| 181 | + | [[package]] | |
| 182 | + | name = "async-task" | |
| 183 | + | version = "4.7.1" | |
| 184 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 185 | + | checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" | |
| 186 | + | ||
| 187 | + | [[package]] | |
| 188 | + | name = "async-trait" | |
| 189 | + | version = "0.1.92" | |
| 190 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 191 | + | checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" | |
| 192 | + | dependencies = [ | |
| 193 | + | "proc-macro2", | |
| 194 | + | "quote", | |
| 195 | + | "syn 3.0.9", | |
| 77 | 196 | ] | |
| 78 | 197 | ||
| 79 | 198 | [[package]] |
| ... | ... | @@ -177,11 +296,24 @@ dependencies = [ | |
| 177 | 296 | "objc2", | |
| 178 | 297 | ] | |
| 179 | 298 | ||
| 299 | + | [[package]] | |
| 300 | + | name = "blocking" | |
| 301 | + | version = "1.7.0" | |
| 302 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 303 | + | checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" | |
| 304 | + | dependencies = [ | |
| 305 | + | "async-channel", | |
| 306 | + | "async-task", | |
| 307 | + | "futures-io", | |
| 308 | + | "futures-lite", | |
| 309 | + | "piper", | |
| 310 | + | ] | |
| 311 | + | ||
| 180 | 312 | [[package]] | |
| 181 | 313 | name = "brotli" | |
| 182 | - | version = "9.0.0" | |
| 314 | + | version = "9.1.1" | |
| 183 | 315 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 184 | - | checksum = "f8b851b75c23ca7873623d612fe49bd1989aeb03d08fb9432187eb253d3d4c6b" | |
| 316 | + | checksum = "627b7cfb0578f0eafce087a90fcd40add1231900b69e4d4de2d456260a4f5d81" | |
| 185 | 317 | dependencies = [ | |
| 186 | 318 | "alloc-no-stdlib", | |
| 187 | 319 | "alloc-stdlib", |
| ... | ... | @@ -190,9 +322,9 @@ dependencies = [ | |
| 190 | 322 | ||
| 191 | 323 | [[package]] | |
| 192 | 324 | name = "brotli-decompressor" | |
| 193 | - | version = "6.0.1" | |
| 325 | + | version = "6.2.0" | |
| 194 | 326 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 195 | - | checksum = "941cd9bd4ddab83cb46fa5a2d428f1c857b24ac78cb876cf7beb710840934bd7" | |
| 327 | + | checksum = "34c6783f42c21c952ad7112b354f0ba2f10760e9decea5b1b60523d6e270faaf" | |
| 196 | 328 | dependencies = [ | |
| 197 | 329 | "alloc-no-stdlib", | |
| 198 | 330 | "alloc-stdlib", |
| ... | ... | @@ -304,9 +436,9 @@ dependencies = [ | |
| 304 | 436 | ||
| 305 | 437 | [[package]] | |
| 306 | 438 | name = "cc" | |
| 307 | - | version = "1.8.0" | |
| 439 | + | version = "1.10.0" | |
| 308 | 440 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 309 | - | checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" | |
| 441 | + | checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0" | |
| 310 | 442 | dependencies = [ | |
| 311 | 443 | "find-msvc-tools", | |
| 312 | 444 | "shlex", |
| ... | ... | @@ -345,6 +477,29 @@ version = "1.0.5" | |
| 345 | 477 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 346 | 478 | checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" | |
| 347 | 479 | ||
| 480 | + | [[package]] | |
| 481 | + | name = "cheogram-www" | |
| 482 | + | version = "0.0.1" | |
| 483 | + | dependencies = [ | |
| 484 | + | "base64 0.22.1", | |
| 485 | + | "hex", | |
| 486 | + | "infer", | |
| 487 | + | "log", | |
| 488 | + | "mime_guess", | |
| 489 | + | "sanitize-filename", | |
| 490 | + | "serde", | |
| 491 | + | "serde_json", | |
| 492 | + | "sha1", | |
| 493 | + | "sha2", | |
| 494 | + | "tauri", | |
| 495 | + | "tauri-build", | |
| 496 | + | "tauri-plugin-log", | |
| 497 | + | "tauri-plugin-opener", | |
| 498 | + | "tempfile", | |
| 499 | + | "tokio", | |
| 500 | + | "uuid", | |
| 501 | + | ] | |
| 502 | + | ||
| 348 | 503 | [[package]] | |
| 349 | 504 | name = "chrono" | |
| 350 | 505 | version = "0.4.45" |
| ... | ... | @@ -367,6 +522,15 @@ dependencies = [ | |
| 367 | 522 | "memchr", | |
| 368 | 523 | ] | |
| 369 | 524 | ||
| 525 | + | [[package]] | |
| 526 | + | name = "concurrent-queue" | |
| 527 | + | version = "2.5.0" | |
| 528 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 529 | + | checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" | |
| 530 | + | dependencies = [ | |
| 531 | + | "crossbeam-utils", | |
| 532 | + | ] | |
| 533 | + | ||
| 370 | 534 | [[package]] | |
| 371 | 535 | name = "cookie" | |
| 372 | 536 | version = "0.18.2" |
| ... | ... | @@ -428,9 +592,9 @@ dependencies = [ | |
| 428 | 592 | ||
| 429 | 593 | [[package]] | |
| 430 | 594 | name = "crc32fast" | |
| 431 | - | version = "1.5.2" | |
| 595 | + | version = "1.5.3" | |
| 432 | 596 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 433 | - | checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" | |
| 597 | + | checksum = "0ba106e844045ddc02c20768ac4b0b7cb45f2cc2ecbd833ac9f5dacb183c2055" | |
| 434 | 598 | dependencies = [ | |
| 435 | 599 | "cfg-if", | |
| 436 | 600 | ] |
| ... | ... | @@ -743,6 +907,33 @@ version = "1.2.2" | |
| 743 | 907 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 744 | 908 | checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" | |
| 745 | 909 | ||
| 910 | + | [[package]] | |
| 911 | + | name = "endi" | |
| 912 | + | version = "1.1.1" | |
| 913 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 914 | + | checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" | |
| 915 | + | ||
| 916 | + | [[package]] | |
| 917 | + | name = "enumflags2" | |
| 918 | + | version = "0.7.12" | |
| 919 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 920 | + | checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" | |
| 921 | + | dependencies = [ | |
| 922 | + | "enumflags2_derive", | |
| 923 | + | "serde", | |
| 924 | + | ] | |
| 925 | + | ||
| 926 | + | [[package]] | |
| 927 | + | name = "enumflags2_derive" | |
| 928 | + | version = "0.7.12" | |
| 929 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 930 | + | checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" | |
| 931 | + | dependencies = [ | |
| 932 | + | "proc-macro2", | |
| 933 | + | "quote", | |
| 934 | + | "syn 2.0.119", | |
| 935 | + | ] | |
| 936 | + | ||
| 746 | 937 | [[package]] | |
| 747 | 938 | name = "env_filter" | |
| 748 | 939 | version = "0.1.4" |
| ... | ... | @@ -761,15 +952,45 @@ checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" | |
| 761 | 952 | ||
| 762 | 953 | [[package]] | |
| 763 | 954 | name = "erased-serde" | |
| 764 | - | version = "0.4.10" | |
| 955 | + | version = "0.4.11" | |
| 765 | 956 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 766 | - | checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" | |
| 957 | + | checksum = "1e59b668157f32d3459d4bd634861a06ffa2503a461ca5f0916b9f0252605e66" | |
| 767 | 958 | dependencies = [ | |
| 768 | 959 | "serde", | |
| 769 | 960 | "serde_core", | |
| 770 | 961 | "typeid", | |
| 771 | 962 | ] | |
| 772 | 963 | ||
| 964 | + | [[package]] | |
| 965 | + | name = "errno" | |
| 966 | + | version = "0.3.14" | |
| 967 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 968 | + | checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" | |
| 969 | + | dependencies = [ | |
| 970 | + | "libc", | |
| 971 | + | "windows-sys 0.61.2", | |
| 972 | + | ] | |
| 973 | + | ||
| 974 | + | [[package]] | |
| 975 | + | name = "event-listener" | |
| 976 | + | version = "5.4.2" | |
| 977 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 978 | + | checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" | |
| 979 | + | dependencies = [ | |
| 980 | + | "parking", | |
| 981 | + | "pin-project-lite", | |
| 982 | + | ] | |
| 983 | + | ||
| 984 | + | [[package]] | |
| 985 | + | name = "event-listener-strategy" | |
| 986 | + | version = "0.5.4" | |
| 987 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 988 | + | checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" | |
| 989 | + | dependencies = [ | |
| 990 | + | "event-listener", | |
| 991 | + | "pin-project-lite", | |
| 992 | + | ] | |
| 993 | + | ||
| 773 | 994 | [[package]] | |
| 774 | 995 | name = "fastrand" | |
| 775 | 996 | version = "2.5.0" |
| ... | ... | @@ -806,9 +1027,9 @@ dependencies = [ | |
| 806 | 1027 | ||
| 807 | 1028 | [[package]] | |
| 808 | 1029 | name = "find-msvc-tools" | |
| 809 | - | version = "0.1.14" | |
| 1030 | + | version = "0.1.15" | |
| 810 | 1031 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 811 | - | checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" | |
| 1032 | + | checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" | |
| 812 | 1033 | ||
| 813 | 1034 | [[package]] | |
| 814 | 1035 | name = "flate2" |
| ... | ... | @@ -901,6 +1122,19 @@ version = "0.3.34" | |
| 901 | 1122 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 902 | 1123 | checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" | |
| 903 | 1124 | ||
| 1125 | + | [[package]] | |
| 1126 | + | name = "futures-lite" | |
| 1127 | + | version = "2.6.1" | |
| 1128 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1129 | + | checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" | |
| 1130 | + | dependencies = [ | |
| 1131 | + | "fastrand", | |
| 1132 | + | "futures-core", | |
| 1133 | + | "futures-io", | |
| 1134 | + | "parking", | |
| 1135 | + | "pin-project-lite", | |
| 1136 | + | ] | |
| 1137 | + | ||
| 904 | 1138 | [[package]] | |
| 905 | 1139 | name = "futures-macro" | |
| 906 | 1140 | version = "0.3.34" |
| ... | ... | @@ -1134,7 +1368,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1134 | 1368 | checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" | |
| 1135 | 1369 | dependencies = [ | |
| 1136 | 1370 | "heck 0.4.1", | |
| 1137 | - | "proc-macro-crate 2.0.2", | |
| 1371 | + | "proc-macro-crate 2.0.0", | |
| 1138 | 1372 | "proc-macro-error", | |
| 1139 | 1373 | "proc-macro2", | |
| 1140 | 1374 | "quote", |
| ... | ... | @@ -1244,6 +1478,12 @@ version = "0.5.0" | |
| 1244 | 1478 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1245 | 1479 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" | |
| 1246 | 1480 | ||
| 1481 | + | [[package]] | |
| 1482 | + | name = "hermit-abi" | |
| 1483 | + | version = "0.5.3" | |
| 1484 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1485 | + | checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" | |
| 1486 | + | ||
| 1247 | 1487 | [[package]] | |
| 1248 | 1488 | name = "hex" | |
| 1249 | 1489 | version = "0.4.3" |
| ... | ... | @@ -1282,9 +1522,9 @@ dependencies = [ | |
| 1282 | 1522 | ||
| 1283 | 1523 | [[package]] | |
| 1284 | 1524 | name = "http-body-util" | |
| 1285 | - | version = "0.1.5" | |
| 1525 | + | version = "0.1.4" | |
| 1286 | 1526 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1287 | - | checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" | |
| 1527 | + | checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" | |
| 1288 | 1528 | dependencies = [ | |
| 1289 | 1529 | "bytes", | |
| 1290 | 1530 | "futures-core", |
| ... | ... | @@ -1301,12 +1541,13 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" | |
| 1301 | 1541 | ||
| 1302 | 1542 | [[package]] | |
| 1303 | 1543 | name = "hyper" | |
| 1304 | - | version = "1.12.0" | |
| 1544 | + | version = "1.10.1" | |
| 1305 | 1545 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1306 | - | checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" | |
| 1546 | + | checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" | |
| 1307 | 1547 | dependencies = [ | |
| 1308 | 1548 | "atomic-waker", | |
| 1309 | 1549 | "bytes", | |
| 1550 | + | "futures-channel", | |
| 1310 | 1551 | "futures-core", | |
| 1311 | 1552 | "http", | |
| 1312 | 1553 | "http-body", |
| ... | ... | @@ -1320,17 +1561,16 @@ dependencies = [ | |
| 1320 | 1561 | ||
| 1321 | 1562 | [[package]] | |
| 1322 | 1563 | name = "hyper-util" | |
| 1323 | - | version = "0.1.21" | |
| 1564 | + | version = "0.1.20" | |
| 1324 | 1565 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1325 | - | checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" | |
| 1566 | + | checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" | |
| 1326 | 1567 | dependencies = [ | |
| 1327 | - | "base64 0.23.1", | |
| 1568 | + | "base64 0.22.1", | |
| 1328 | 1569 | "bytes", | |
| 1329 | 1570 | "futures-channel", | |
| 1330 | 1571 | "futures-util", | |
| 1331 | 1572 | "http", | |
| 1332 | 1573 | "http-body", | |
| 1333 | - | "httparse", | |
| 1334 | 1574 | "hyper", | |
| 1335 | 1575 | "ipnet", | |
| 1336 | 1576 | "libc", |
| ... | ... | @@ -1368,19 +1608,19 @@ dependencies = [ | |
| 1368 | 1608 | ||
| 1369 | 1609 | [[package]] | |
| 1370 | 1610 | name = "ico" | |
| 1371 | - | version = "0.5.0" | |
| 1611 | + | version = "0.6.0" | |
| 1372 | 1612 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1373 | - | checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" | |
| 1613 | + | checksum = "d75e94d66081e4d8dcd098bd8dc5254800fea14a26389b298410158d834de167" | |
| 1374 | 1614 | dependencies = [ | |
| 1375 | 1615 | "byteorder", | |
| 1376 | - | "png 0.17.16", | |
| 1616 | + | "png", | |
| 1377 | 1617 | ] | |
| 1378 | 1618 | ||
| 1379 | 1619 | [[package]] | |
| 1380 | 1620 | name = "icu_collections" | |
| 1381 | - | version = "2.3.0" | |
| 1621 | + | version = "2.4.0" | |
| 1382 | 1622 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1383 | - | checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" | |
| 1623 | + | checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd" | |
| 1384 | 1624 | dependencies = [ | |
| 1385 | 1625 | "displaydoc", | |
| 1386 | 1626 | "potential_utf", |
| ... | ... | @@ -1392,9 +1632,9 @@ dependencies = [ | |
| 1392 | 1632 | ||
| 1393 | 1633 | [[package]] | |
| 1394 | 1634 | name = "icu_locale_core" | |
| 1395 | - | version = "2.3.0" | |
| 1635 | + | version = "2.4.0" | |
| 1396 | 1636 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1397 | - | checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" | |
| 1637 | + | checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0" | |
| 1398 | 1638 | dependencies = [ | |
| 1399 | 1639 | "displaydoc", | |
| 1400 | 1640 | "litemap", |
| ... | ... | @@ -1405,9 +1645,9 @@ dependencies = [ | |
| 1405 | 1645 | ||
| 1406 | 1646 | [[package]] | |
| 1407 | 1647 | name = "icu_normalizer" | |
| 1408 | - | version = "2.3.0" | |
| 1648 | + | version = "2.4.0" | |
| 1409 | 1649 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1410 | - | checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" | |
| 1650 | + | checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599" | |
| 1411 | 1651 | dependencies = [ | |
| 1412 | 1652 | "icu_collections", | |
| 1413 | 1653 | "icu_normalizer_data", |
| ... | ... | @@ -1419,15 +1659,15 @@ dependencies = [ | |
| 1419 | 1659 | ||
| 1420 | 1660 | [[package]] | |
| 1421 | 1661 | name = "icu_normalizer_data" | |
| 1422 | - | version = "2.3.0" | |
| 1662 | + | version = "2.4.0" | |
| 1423 | 1663 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1424 | - | checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" | |
| 1664 | + | checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc" | |
| 1425 | 1665 | ||
| 1426 | 1666 | [[package]] | |
| 1427 | 1667 | name = "icu_properties" | |
| 1428 | - | version = "2.3.0" | |
| 1668 | + | version = "2.4.0" | |
| 1429 | 1669 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1430 | - | checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" | |
| 1670 | + | checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f" | |
| 1431 | 1671 | dependencies = [ | |
| 1432 | 1672 | "displaydoc", | |
| 1433 | 1673 | "icu_collections", |
| ... | ... | @@ -1440,15 +1680,15 @@ dependencies = [ | |
| 1440 | 1680 | ||
| 1441 | 1681 | [[package]] | |
| 1442 | 1682 | name = "icu_properties_data" | |
| 1443 | - | version = "2.3.0" | |
| 1683 | + | version = "2.4.0" | |
| 1444 | 1684 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1445 | - | checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" | |
| 1685 | + | checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf" | |
| 1446 | 1686 | ||
| 1447 | 1687 | [[package]] | |
| 1448 | 1688 | name = "icu_provider" | |
| 1449 | - | version = "2.3.1" | |
| 1689 | + | version = "2.4.0" | |
| 1450 | 1690 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1451 | - | checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" | |
| 1691 | + | checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514" | |
| 1452 | 1692 | dependencies = [ | |
| 1453 | 1693 | "displaydoc", | |
| 1454 | 1694 | "icu_locale_core", |
| ... | ... | @@ -1520,9 +1760,28 @@ dependencies = [ | |
| 1520 | 1760 | ||
| 1521 | 1761 | [[package]] | |
| 1522 | 1762 | name = "ipnet" | |
| 1523 | - | version = "2.12.2" | |
| 1763 | + | version = "2.12.0" | |
| 1764 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1765 | + | checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" | |
| 1766 | + | ||
| 1767 | + | [[package]] | |
| 1768 | + | name = "is-docker" | |
| 1769 | + | version = "0.2.0" | |
| 1770 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1771 | + | checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" | |
| 1772 | + | dependencies = [ | |
| 1773 | + | "once_cell", | |
| 1774 | + | ] | |
| 1775 | + | ||
| 1776 | + | [[package]] | |
| 1777 | + | name = "is-wsl" | |
| 1778 | + | version = "0.4.0" | |
| 1524 | 1779 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1525 | - | checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" | |
| 1780 | + | checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" | |
| 1781 | + | dependencies = [ | |
| 1782 | + | "is-docker", | |
| 1783 | + | "once_cell", | |
| 1784 | + | ] | |
| 1526 | 1785 | ||
| 1527 | 1786 | [[package]] | |
| 1528 | 1787 | name = "itoa" |
| ... | ... | @@ -1720,9 +1979,9 @@ dependencies = [ | |
| 1720 | 1979 | ||
| 1721 | 1980 | [[package]] | |
| 1722 | 1981 | name = "libc" | |
| 1723 | - | version = "0.2.190" | |
| 1982 | + | version = "0.2.191" | |
| 1724 | 1983 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1725 | - | checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" | |
| 1984 | + | checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" | |
| 1726 | 1985 | ||
| 1727 | 1986 | [[package]] | |
| 1728 | 1987 | name = "libdbus-sys" |
| ... | ... | @@ -1752,6 +2011,12 @@ dependencies = [ | |
| 1752 | 2011 | "libc", | |
| 1753 | 2012 | ] | |
| 1754 | 2013 | ||
| 2014 | + | [[package]] | |
| 2015 | + | name = "linux-raw-sys" | |
| 2016 | + | version = "0.12.1" | |
| 2017 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2018 | + | checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" | |
| 2019 | + | ||
| 1755 | 2020 | [[package]] | |
| 1756 | 2021 | name = "litemap" | |
| 1757 | 2022 | version = "0.8.3" |
| ... | ... | @@ -1805,6 +2070,16 @@ version = "0.3.17" | |
| 1805 | 2070 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1806 | 2071 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" | |
| 1807 | 2072 | ||
| 2073 | + | [[package]] | |
| 2074 | + | name = "mime_guess" | |
| 2075 | + | version = "2.0.5" | |
| 2076 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2077 | + | checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" | |
| 2078 | + | dependencies = [ | |
| 2079 | + | "mime", | |
| 2080 | + | "unicase", | |
| 2081 | + | ] | |
| 2082 | + | ||
| 1808 | 2083 | [[package]] | |
| 1809 | 2084 | name = "miniz_oxide" | |
| 1810 | 2085 | version = "0.8.9" |
| ... | ... | @@ -1932,9 +2207,9 @@ dependencies = [ | |
| 1932 | 2207 | ||
| 1933 | 2208 | [[package]] | |
| 1934 | 2209 | name = "num_threads" | |
| 1935 | - | version = "0.1.7" | |
| 2210 | + | version = "0.1.8" | |
| 1936 | 2211 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1937 | - | checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" | |
| 2212 | + | checksum = "cabcc654086861f9e1bc5115258de62ef92c79b20f4615a1b0cc57c4e82027d4" | |
| 1938 | 2213 | dependencies = [ | |
| 1939 | 2214 | "libc", | |
| 1940 | 2215 | ] |
| ... | ... | @@ -2141,12 +2416,33 @@ version = "1.21.4" | |
| 2141 | 2416 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2142 | 2417 | checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" | |
| 2143 | 2418 | ||
| 2419 | + | [[package]] | |
| 2420 | + | name = "open" | |
| 2421 | + | version = "5.4.5" | |
| 2422 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2423 | + | checksum = "8d8c8b8e725ad26404d6896eb2f064294a0a7b4f1d9120210d85544684c4ca95" | |
| 2424 | + | dependencies = [ | |
| 2425 | + | "dunce", | |
| 2426 | + | "is-wsl", | |
| 2427 | + | "libc", | |
| 2428 | + | ] | |
| 2429 | + | ||
| 2144 | 2430 | [[package]] | |
| 2145 | 2431 | name = "option-ext" | |
| 2146 | 2432 | version = "0.2.0" | |
| 2147 | 2433 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2148 | 2434 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" | |
| 2149 | 2435 | ||
| 2436 | + | [[package]] | |
| 2437 | + | name = "ordered-stream" | |
| 2438 | + | version = "0.2.0" | |
| 2439 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2440 | + | checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" | |
| 2441 | + | dependencies = [ | |
| 2442 | + | "futures-core", | |
| 2443 | + | "pin-project-lite", | |
| 2444 | + | ] | |
| 2445 | + | ||
| 2150 | 2446 | [[package]] | |
| 2151 | 2447 | name = "pango" | |
| 2152 | 2448 | version = "0.18.3" |
| ... | ... | @@ -2172,6 +2468,12 @@ dependencies = [ | |
| 2172 | 2468 | "system-deps", | |
| 2173 | 2469 | ] | |
| 2174 | 2470 | ||
| 2471 | + | [[package]] | |
| 2472 | + | name = "parking" | |
| 2473 | + | version = "2.2.1" | |
| 2474 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2475 | + | checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" | |
| 2476 | + | ||
| 2175 | 2477 | [[package]] | |
| 2176 | 2478 | name = "parking_lot" | |
| 2177 | 2479 | version = "0.12.5" |
| ... | ... | @@ -2260,6 +2562,17 @@ version = "0.2.17" | |
| 2260 | 2562 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2261 | 2563 | checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" | |
| 2262 | 2564 | ||
| 2565 | + | [[package]] | |
| 2566 | + | name = "piper" | |
| 2567 | + | version = "0.2.5" | |
| 2568 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2569 | + | checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" | |
| 2570 | + | dependencies = [ | |
| 2571 | + | "atomic-waker", | |
| 2572 | + | "fastrand", | |
| 2573 | + | "futures-io", | |
| 2574 | + | ] | |
| 2575 | + | ||
| 2263 | 2576 | [[package]] | |
| 2264 | 2577 | name = "pkg-config" | |
| 2265 | 2578 | version = "0.3.34" |
| ... | ... | @@ -2268,9 +2581,9 @@ checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" | |
| 2268 | 2581 | ||
| 2269 | 2582 | [[package]] | |
| 2270 | 2583 | name = "plist" | |
| 2271 | - | version = "1.10.1" | |
| 2584 | + | version = "1.10.3" | |
| 2272 | 2585 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2273 | - | checksum = "2896bade328c13f7042a297ea5ac5b0951f6cf989dea5f32c2fd98da398195cb" | |
| 2586 | + | checksum = "327a4cb2ad49dc48adfdc9da89e22c64d488b508c67a044d71ba729e4521a285" | |
| 2274 | 2587 | dependencies = [ | |
| 2275 | 2588 | "base64 0.23.1", | |
| 2276 | 2589 | "indexmap 2.14.2", |
| ... | ... | @@ -2281,11 +2594,11 @@ dependencies = [ | |
| 2281 | 2594 | ||
| 2282 | 2595 | [[package]] | |
| 2283 | 2596 | name = "png" | |
| 2284 | - | version = "0.17.16" | |
| 2597 | + | version = "0.18.1" | |
| 2285 | 2598 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2286 | - | checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" | |
| 2599 | + | checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" | |
| 2287 | 2600 | dependencies = [ | |
| 2288 | - | "bitflags 1.3.2", | |
| 2601 | + | "bitflags 2.13.2", | |
| 2289 | 2602 | "crc32fast", | |
| 2290 | 2603 | "fdeflate", | |
| 2291 | 2604 | "flate2", |
| ... | ... | @@ -2293,16 +2606,17 @@ dependencies = [ | |
| 2293 | 2606 | ] | |
| 2294 | 2607 | ||
| 2295 | 2608 | [[package]] | |
| 2296 | - | name = "png" | |
| 2297 | - | version = "0.18.1" | |
| 2609 | + | name = "polling" | |
| 2610 | + | version = "3.11.0" | |
| 2298 | 2611 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2299 | - | checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" | |
| 2612 | + | checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" | |
| 2300 | 2613 | dependencies = [ | |
| 2301 | - | "bitflags 2.13.2", | |
| 2302 | - | "crc32fast", | |
| 2303 | - | "fdeflate", | |
| 2304 | - | "flate2", | |
| 2305 | - | "miniz_oxide 0.8.9", | |
| 2614 | + | "cfg-if", | |
| 2615 | + | "concurrent-queue", | |
| 2616 | + | "hermit-abi", | |
| 2617 | + | "pin-project-lite", | |
| 2618 | + | "rustix", | |
| 2619 | + | "windows-sys 0.61.2", | |
| 2306 | 2620 | ] | |
| 2307 | 2621 | ||
| 2308 | 2622 | [[package]] |
| ... | ... | @@ -2353,11 +2667,10 @@ dependencies = [ | |
| 2353 | 2667 | ||
| 2354 | 2668 | [[package]] | |
| 2355 | 2669 | name = "proc-macro-crate" | |
| 2356 | - | version = "2.0.2" | |
| 2670 | + | version = "2.0.0" | |
| 2357 | 2671 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2358 | - | checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" | |
| 2672 | + | checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" | |
| 2359 | 2673 | dependencies = [ | |
| 2360 | - | "toml_datetime 0.6.3", | |
| 2361 | 2674 | "toml_edit 0.20.2", | |
| 2362 | 2675 | ] | |
| 2363 | 2676 |
| ... | ... | @@ -2509,11 +2822,11 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" | |
| 2509 | 2822 | ||
| 2510 | 2823 | [[package]] | |
| 2511 | 2824 | name = "reqwest" | |
| 2512 | - | version = "0.13.5" | |
| 2825 | + | version = "0.13.4" | |
| 2513 | 2826 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2514 | - | checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" | |
| 2827 | + | checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" | |
| 2515 | 2828 | dependencies = [ | |
| 2516 | - | "base64 0.23.1", | |
| 2829 | + | "base64 0.22.1", | |
| 2517 | 2830 | "bytes", | |
| 2518 | 2831 | "futures-core", | |
| 2519 | 2832 | "futures-util", |
| ... | ... | @@ -2556,6 +2869,19 @@ dependencies = [ | |
| 2556 | 2869 | "semver", | |
| 2557 | 2870 | ] | |
| 2558 | 2871 | ||
| 2872 | + | [[package]] | |
| 2873 | + | name = "rustix" | |
| 2874 | + | version = "1.1.6" | |
| 2875 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2876 | + | checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa" | |
| 2877 | + | dependencies = [ | |
| 2878 | + | "bitflags 2.13.2", | |
| 2879 | + | "errno", | |
| 2880 | + | "libc", | |
| 2881 | + | "linux-raw-sys", | |
| 2882 | + | "windows-sys 0.61.2", | |
| 2883 | + | ] | |
| 2884 | + | ||
| 2559 | 2885 | [[package]] | |
| 2560 | 2886 | name = "rustversion" | |
| 2561 | 2887 | version = "1.0.23" |
| ... | ... | @@ -2571,6 +2897,15 @@ dependencies = [ | |
| 2571 | 2897 | "winapi-util", | |
| 2572 | 2898 | ] | |
| 2573 | 2899 | ||
| 2900 | + | [[package]] | |
| 2901 | + | name = "sanitize-filename" | |
| 2902 | + | version = "0.6.0" | |
| 2903 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2904 | + | checksum = "bc984f4f9ceb736a7bb755c3e3bd17dc56370af2600c9780dcc48c66453da34d" | |
| 2905 | + | dependencies = [ | |
| 2906 | + | "regex", | |
| 2907 | + | ] | |
| 2908 | + | ||
| 2574 | 2909 | [[package]] | |
| 2575 | 2910 | name = "schemars" | |
| 2576 | 2911 | version = "0.8.22" |
| ... | ... | @@ -2816,6 +3151,17 @@ dependencies = [ | |
| 2816 | 3151 | "stable_deref_trait", | |
| 2817 | 3152 | ] | |
| 2818 | 3153 | ||
| 3154 | + | [[package]] | |
| 3155 | + | name = "sha1" | |
| 3156 | + | version = "0.10.7" | |
| 3157 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3158 | + | checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" | |
| 3159 | + | dependencies = [ | |
| 3160 | + | "cfg-if", | |
| 3161 | + | "cpufeatures", | |
| 3162 | + | "digest", | |
| 3163 | + | ] | |
| 3164 | + | ||
| 2819 | 3165 | [[package]] | |
| 2820 | 3166 | name = "sha2" | |
| 2821 | 3167 | version = "0.10.9" |
| ... | ... | @@ -2833,6 +3179,16 @@ version = "2.0.1" | |
| 2833 | 3179 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2834 | 3180 | checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" | |
| 2835 | 3181 | ||
| 3182 | + | [[package]] | |
| 3183 | + | name = "signal-hook-registry" | |
| 3184 | + | version = "1.4.8" | |
| 3185 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3186 | + | checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" | |
| 3187 | + | dependencies = [ | |
| 3188 | + | "errno", | |
| 3189 | + | "libc", | |
| 3190 | + | ] | |
| 3191 | + | ||
| 2836 | 3192 | [[package]] | |
| 2837 | 3193 | name = "simd-adler32" | |
| 2838 | 3194 | version = "0.3.10" |
| ... | ... | @@ -2853,9 +3209,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" | |
| 2853 | 3209 | ||
| 2854 | 3210 | [[package]] | |
| 2855 | 3211 | name = "smallvec" | |
| 2856 | - | version = "1.16.3" | |
| 3212 | + | version = "1.16.4" | |
| 2857 | 3213 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2858 | - | checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" | |
| 3214 | + | checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8" | |
| 2859 | 3215 | ||
| 2860 | 3216 | [[package]] | |
| 2861 | 3217 | name = "socket2" |
| ... | ... | @@ -3029,15 +3385,15 @@ dependencies = [ | |
| 3029 | 3385 | "cfg-expr", | |
| 3030 | 3386 | "heck 0.5.0", | |
| 3031 | 3387 | "pkg-config", | |
| 3032 | - | "toml 0.8.2", | |
| 3388 | + | "toml 0.8.23", | |
| 3033 | 3389 | "version-compare", | |
| 3034 | 3390 | ] | |
| 3035 | 3391 | ||
| 3036 | 3392 | [[package]] | |
| 3037 | 3393 | name = "tao" | |
| 3038 | - | version = "0.37.1" | |
| 3394 | + | version = "0.37.2" | |
| 3039 | 3395 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3040 | - | checksum = "f37f381f4e048e6cdf038b5705f8cf14ad108279d46eb968140a7b291aba9400" | |
| 3396 | + | checksum = "d5777b06fe6435126ae8d816eba269e050e1e10d302cf5461e9c49eccabc3548" | |
| 3041 | 3397 | dependencies = [ | |
| 3042 | 3398 | "bitflags 2.13.2", | |
| 3043 | 3399 | "block2", |
| ... | ... | @@ -3092,9 +3448,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" | |
| 3092 | 3448 | ||
| 3093 | 3449 | [[package]] | |
| 3094 | 3450 | name = "tauri" | |
| 3095 | - | version = "2.12.4" | |
| 3451 | + | version = "2.12.5" | |
| 3096 | 3452 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3097 | - | checksum = "f316751027fc2644221d1a448564d519efb07067fda584879d9cda966391d212" | |
| 3453 | + | checksum = "5fb0244332cfb9cee7e2a568bf5da29a36ed8fdbd717f6f47a66b6b7b358c072" | |
| 3098 | 3454 | dependencies = [ | |
| 3099 | 3455 | "anyhow", | |
| 3100 | 3456 | "bytes", |
| ... | ... | @@ -3143,9 +3499,9 @@ dependencies = [ | |
| 3143 | 3499 | ||
| 3144 | 3500 | [[package]] | |
| 3145 | 3501 | name = "tauri-build" | |
| 3146 | - | version = "2.7.3" | |
| 3502 | + | version = "2.7.4" | |
| 3147 | 3503 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3148 | - | checksum = "641dd5fd297c99ab92717b0428cca5b9c6f960b2ac96ca131b3eebc9d70b8ddc" | |
| 3504 | + | checksum = "adddb0e648752aaac5da9c3c3c6a68e1e87cab55e3b757b18a9441ef8c3ac614" | |
| 3149 | 3505 | dependencies = [ | |
| 3150 | 3506 | "anyhow", | |
| 3151 | 3507 | "cargo_toml", |
| ... | ... | @@ -3166,16 +3522,16 @@ dependencies = [ | |
| 3166 | 3522 | ||
| 3167 | 3523 | [[package]] | |
| 3168 | 3524 | name = "tauri-codegen" | |
| 3169 | - | version = "2.7.3" | |
| 3525 | + | version = "2.7.4" | |
| 3170 | 3526 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3171 | - | checksum = "a6585c5b536c00f039d12ce70a9755172d5423bbcfd3969863baa70314c3e02f" | |
| 3527 | + | checksum = "fb37631cf71dab5e8ab55e1bdd701e18a52653d90b78e71d0e5598b1e0a61316" | |
| 3172 | 3528 | dependencies = [ | |
| 3173 | 3529 | "base64 0.23.1", | |
| 3174 | 3530 | "brotli", | |
| 3175 | 3531 | "ico", | |
| 3176 | 3532 | "json-patch", | |
| 3177 | 3533 | "plist", | |
| 3178 | - | "png 0.18.1", | |
| 3534 | + | "png", | |
| 3179 | 3535 | "proc-macro2", | |
| 3180 | 3536 | "quote", | |
| 3181 | 3537 | "semver", |
| ... | ... | @@ -3193,9 +3549,9 @@ dependencies = [ | |
| 3193 | 3549 | ||
| 3194 | 3550 | [[package]] | |
| 3195 | 3551 | name = "tauri-macros" | |
| 3196 | - | version = "2.7.3" | |
| 3552 | + | version = "2.7.4" | |
| 3197 | 3553 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3198 | - | checksum = "e2e83a0cfa1bae376e8fbbdc0b1692707aed50cd6072ce85cae4789af5d6c19f" | |
| 3554 | + | checksum = "0cc38db58854c10fb5f91a887ed8bf82d3239bb92b5d8db86eda09e96d9963d1" | |
| 3199 | 3555 | dependencies = [ | |
| 3200 | 3556 | "heck 0.5.0", | |
| 3201 | 3557 | "proc-macro2", |
| ... | ... | @@ -3242,6 +3598,28 @@ dependencies = [ | |
| 3242 | 3598 | "time", | |
| 3243 | 3599 | ] | |
| 3244 | 3600 | ||
| 3601 | + | [[package]] | |
| 3602 | + | name = "tauri-plugin-opener" | |
| 3603 | + | version = "2.7.2" | |
| 3604 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3605 | + | checksum = "53810be1b4fed22bb6ac3f4cab5a2b80d9391d1af436bda145607532bc0bb219" | |
| 3606 | + | dependencies = [ | |
| 3607 | + | "dunce", | |
| 3608 | + | "glob", | |
| 3609 | + | "objc2-app-kit", | |
| 3610 | + | "objc2-foundation", | |
| 3611 | + | "open", | |
| 3612 | + | "schemars 0.8.22", | |
| 3613 | + | "serde", | |
| 3614 | + | "serde_json", | |
| 3615 | + | "tauri", | |
| 3616 | + | "tauri-plugin", | |
| 3617 | + | "thiserror 2.0.21", | |
| 3618 | + | "url", | |
| 3619 | + | "windows", | |
| 3620 | + | "zbus", | |
| 3621 | + | ] | |
| 3622 | + | ||
| 3245 | 3623 | [[package]] | |
| 3246 | 3624 | name = "tauri-runtime" | |
| 3247 | 3625 | version = "2.12.2" |
| ... | ... | @@ -3269,9 +3647,9 @@ dependencies = [ | |
| 3269 | 3647 | ||
| 3270 | 3648 | [[package]] | |
| 3271 | 3649 | name = "tauri-runtime-wry" | |
| 3272 | - | version = "2.12.2" | |
| 3650 | + | version = "2.12.3" | |
| 3273 | 3651 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3274 | - | checksum = "8d61892587aea2a85245bcaec98c6fe46268874c0a0deae5535fb71ca7b93eae" | |
| 3652 | + | checksum = "e1aa9d8b7a120f879f44fdc1053f7b7dbf416bf0bbc6a72b714dec46b782b52f" | |
| 3275 | 3653 | dependencies = [ | |
| 3276 | 3654 | "gtk", | |
| 3277 | 3655 | "http", |
| ... | ... | @@ -3333,15 +3710,28 @@ dependencies = [ | |
| 3333 | 3710 | ||
| 3334 | 3711 | [[package]] | |
| 3335 | 3712 | name = "tauri-winres" | |
| 3336 | - | version = "0.3.6" | |
| 3713 | + | version = "0.3.7" | |
| 3337 | 3714 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3338 | - | checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" | |
| 3715 | + | checksum = "c52bb6904475a6ca662f15ef787f18ade61d5740a1ee12c45d3b748efdb6f796" | |
| 3339 | 3716 | dependencies = [ | |
| 3340 | 3717 | "dunce", | |
| 3341 | 3718 | "embed-resource", | |
| 3342 | 3719 | "toml 1.1.8+spec-1.1.0", | |
| 3343 | 3720 | ] | |
| 3344 | 3721 | ||
| 3722 | + | [[package]] | |
| 3723 | + | name = "tempfile" | |
| 3724 | + | version = "3.27.0" | |
| 3725 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3726 | + | checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" | |
| 3727 | + | dependencies = [ | |
| 3728 | + | "fastrand", | |
| 3729 | + | "getrandom 0.4.3", | |
| 3730 | + | "once_cell", | |
| 3731 | + | "rustix", | |
| 3732 | + | "windows-sys 0.61.2", | |
| 3733 | + | ] | |
| 3734 | + | ||
| 3345 | 3735 | [[package]] | |
| 3346 | 3736 | name = "tendril" | |
| 3347 | 3737 | version = "0.5.1" |
| ... | ... | @@ -3425,9 +3815,9 @@ dependencies = [ | |
| 3425 | 3815 | ||
| 3426 | 3816 | [[package]] | |
| 3427 | 3817 | name = "tinystr" | |
| 3428 | - | version = "0.8.4" | |
| 3818 | + | version = "0.8.5" | |
| 3429 | 3819 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3430 | - | checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" | |
| 3820 | + | checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a" | |
| 3431 | 3821 | dependencies = [ | |
| 3432 | 3822 | "displaydoc", | |
| 3433 | 3823 | "zerovec", |
| ... | ... | @@ -3441,9 +3831,9 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" | |
| 3441 | 3831 | ||
| 3442 | 3832 | [[package]] | |
| 3443 | 3833 | name = "tokio" | |
| 3444 | - | version = "1.53.3" | |
| 3834 | + | version = "1.54.1" | |
| 3445 | 3835 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3446 | - | checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" | |
| 3836 | + | checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" | |
| 3447 | 3837 | dependencies = [ | |
| 3448 | 3838 | "bytes", | |
| 3449 | 3839 | "libc", |
| ... | ... | @@ -3455,9 +3845,9 @@ dependencies = [ | |
| 3455 | 3845 | ||
| 3456 | 3846 | [[package]] | |
| 3457 | 3847 | name = "tokio-util" | |
| 3458 | - | version = "0.7.20" | |
| 3848 | + | version = "0.7.18" | |
| 3459 | 3849 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3460 | - | checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" | |
| 3850 | + | checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" | |
| 3461 | 3851 | dependencies = [ | |
| 3462 | 3852 | "bytes", | |
| 3463 | 3853 | "futures-core", |
| ... | ... | @@ -3468,14 +3858,14 @@ dependencies = [ | |
| 3468 | 3858 | ||
| 3469 | 3859 | [[package]] | |
| 3470 | 3860 | name = "toml" | |
| 3471 | - | version = "0.8.2" | |
| 3861 | + | version = "0.8.23" | |
| 3472 | 3862 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3473 | - | checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" | |
| 3863 | + | checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" | |
| 3474 | 3864 | dependencies = [ | |
| 3475 | 3865 | "serde", | |
| 3476 | 3866 | "serde_spanned 0.6.9", | |
| 3477 | - | "toml_datetime 0.6.3", | |
| 3478 | - | "toml_edit 0.20.2", | |
| 3867 | + | "toml_datetime 0.6.11", | |
| 3868 | + | "toml_edit 0.22.27", | |
| 3479 | 3869 | ] | |
| 3480 | 3870 | ||
| 3481 | 3871 | [[package]] |
| ... | ... | @@ -3495,9 +3885,9 @@ dependencies = [ | |
| 3495 | 3885 | ||
| 3496 | 3886 | [[package]] | |
| 3497 | 3887 | name = "toml_datetime" | |
| 3498 | - | version = "0.6.3" | |
| 3888 | + | version = "0.6.11" | |
| 3499 | 3889 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3500 | - | checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" | |
| 3890 | + | checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" | |
| 3501 | 3891 | dependencies = [ | |
| 3502 | 3892 | "serde", | |
| 3503 | 3893 | ] |
| ... | ... | @@ -3518,7 +3908,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3518 | 3908 | checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" | |
| 3519 | 3909 | dependencies = [ | |
| 3520 | 3910 | "indexmap 2.14.2", | |
| 3521 | - | "toml_datetime 0.6.3", | |
| 3911 | + | "toml_datetime 0.6.11", | |
| 3522 | 3912 | "winnow 0.5.40", | |
| 3523 | 3913 | ] | |
| 3524 | 3914 |
| ... | ... | @@ -3527,12 +3917,23 @@ name = "toml_edit" | |
| 3527 | 3917 | version = "0.20.2" | |
| 3528 | 3918 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3529 | 3919 | checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" | |
| 3920 | + | dependencies = [ | |
| 3921 | + | "indexmap 2.14.2", | |
| 3922 | + | "toml_datetime 0.6.11", | |
| 3923 | + | "winnow 0.5.40", | |
| 3924 | + | ] | |
| 3925 | + | ||
| 3926 | + | [[package]] | |
| 3927 | + | name = "toml_edit" | |
| 3928 | + | version = "0.22.27" | |
| 3929 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3930 | + | checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" | |
| 3530 | 3931 | dependencies = [ | |
| 3531 | 3932 | "indexmap 2.14.2", | |
| 3532 | 3933 | "serde", | |
| 3533 | 3934 | "serde_spanned 0.6.9", | |
| 3534 | - | "toml_datetime 0.6.3", | |
| 3535 | - | "winnow 0.5.40", | |
| 3935 | + | "toml_datetime 0.6.11", | |
| 3936 | + | "winnow 0.7.15", | |
| 3536 | 3937 | ] | |
| 3537 | 3938 | ||
| 3538 | 3939 | [[package]] |
| ... | ... | @@ -3614,9 +4015,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3614 | 4015 | checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" | |
| 3615 | 4016 | dependencies = [ | |
| 3616 | 4017 | "pin-project-lite", | |
| 4018 | + | "tracing-attributes", | |
| 3617 | 4019 | "tracing-core", | |
| 3618 | 4020 | ] | |
| 3619 | 4021 | ||
| 4022 | + | [[package]] | |
| 4023 | + | name = "tracing-attributes" | |
| 4024 | + | version = "0.1.31" | |
| 4025 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4026 | + | checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" | |
| 4027 | + | dependencies = [ | |
| 4028 | + | "proc-macro2", | |
| 4029 | + | "quote", | |
| 4030 | + | "syn 2.0.119", | |
| 4031 | + | ] | |
| 4032 | + | ||
| 3620 | 4033 | [[package]] | |
| 3621 | 4034 | name = "tracing-core" | |
| 3622 | 4035 | version = "0.1.36" |
| ... | ... | @@ -3666,6 +4079,23 @@ version = "1.20.1" | |
| 3666 | 4079 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3667 | 4080 | checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" | |
| 3668 | 4081 | ||
| 4082 | + | [[package]] | |
| 4083 | + | name = "uds_windows" | |
| 4084 | + | version = "1.2.1" | |
| 4085 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4086 | + | checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" | |
| 4087 | + | dependencies = [ | |
| 4088 | + | "memoffset", | |
| 4089 | + | "tempfile", | |
| 4090 | + | "windows-sys 0.61.2", | |
| 4091 | + | ] | |
| 4092 | + | ||
| 4093 | + | [[package]] | |
| 4094 | + | name = "unicase" | |
| 4095 | + | version = "2.10.0" | |
| 4096 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4097 | + | checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" | |
| 4098 | + | ||
| 3669 | 4099 | [[package]] | |
| 3670 | 4100 | name = "unicode-ident" | |
| 3671 | 4101 | version = "1.0.27" |
| ... | ... | @@ -3711,9 +4141,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" | |
| 3711 | 4141 | ||
| 3712 | 4142 | [[package]] | |
| 3713 | 4143 | name = "uuid" | |
| 3714 | - | version = "1.28.0" | |
| 4144 | + | version = "1.29.0" | |
| 3715 | 4145 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3716 | - | checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" | |
| 4146 | + | checksum = "4a0e2d11620c50695fe12bbbfde96b3477fe606e7284c22d505c12ce51214514" | |
| 3717 | 4147 | dependencies = [ | |
| 3718 | 4148 | "getrandom 0.4.3", | |
| 3719 | 4149 | "js-sys", |
| ... | ... | @@ -3765,9 +4195,9 @@ dependencies = [ | |
| 3765 | 4195 | ||
| 3766 | 4196 | [[package]] | |
| 3767 | 4197 | name = "want" | |
| 3768 | - | version = "0.3.2" | |
| 4198 | + | version = "0.3.1" | |
| 3769 | 4199 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3770 | - | checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" | |
| 4200 | + | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" | |
| 3771 | 4201 | dependencies = [ | |
| 3772 | 4202 | "try-lock", | |
| 3773 | 4203 | ] |
| ... | ... | @@ -4216,6 +4646,15 @@ dependencies = [ | |
| 4216 | 4646 | "memchr", | |
| 4217 | 4647 | ] | |
| 4218 | 4648 | ||
| 4649 | + | [[package]] | |
| 4650 | + | name = "winnow" | |
| 4651 | + | version = "0.7.15" | |
| 4652 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4653 | + | checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" | |
| 4654 | + | dependencies = [ | |
| 4655 | + | "memchr", | |
| 4656 | + | ] | |
| 4657 | + | ||
| 4219 | 4658 | [[package]] | |
| 4220 | 4659 | name = "winnow" | |
| 4221 | 4660 | version = "1.0.4" |
| ... | ... | @@ -4249,11 +4688,11 @@ checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" | |
| 4249 | 4688 | ||
| 4250 | 4689 | [[package]] | |
| 4251 | 4690 | name = "wry" | |
| 4252 | - | version = "0.57.0" | |
| 4691 | + | version = "0.57.1" | |
| 4253 | 4692 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4254 | - | checksum = "a819957a01b3119af85e638a38d242af76dbc87d130dca67bfd0441072e21ff0" | |
| 4693 | + | checksum = "759c50ddad0ef9913bb5496cb9d509a50433a3b78cb0238d61105526589cf0e9" | |
| 4255 | 4694 | dependencies = [ | |
| 4256 | - | "base64 0.22.1", | |
| 4695 | + | "base64 0.23.1", | |
| 4257 | 4696 | "block2", | |
| 4258 | 4697 | "cookie", | |
| 4259 | 4698 | "crossbeam-channel", |
| ... | ... | @@ -4314,9 +4752,9 @@ dependencies = [ | |
| 4314 | 4752 | ||
| 4315 | 4753 | [[package]] | |
| 4316 | 4754 | name = "yoke" | |
| 4317 | - | version = "0.8.3" | |
| 4755 | + | version = "0.8.4" | |
| 4318 | 4756 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4319 | - | checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" | |
| 4757 | + | checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3" | |
| 4320 | 4758 | dependencies = [ | |
| 4321 | 4759 | "stable_deref_trait", | |
| 4322 | 4760 | "yoke-derive", |
| ... | ... | @@ -4335,6 +4773,76 @@ dependencies = [ | |
| 4335 | 4773 | "synstructure", | |
| 4336 | 4774 | ] | |
| 4337 | 4775 | ||
| 4776 | + | [[package]] | |
| 4777 | + | name = "zbus" | |
| 4778 | + | version = "5.19.0" | |
| 4779 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4780 | + | checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" | |
| 4781 | + | dependencies = [ | |
| 4782 | + | "async-broadcast", | |
| 4783 | + | "async-executor", | |
| 4784 | + | "async-io", | |
| 4785 | + | "async-lock", | |
| 4786 | + | "async-process", | |
| 4787 | + | "async-recursion", | |
| 4788 | + | "async-task", | |
| 4789 | + | "async-trait", | |
| 4790 | + | "blocking", | |
| 4791 | + | "enumflags2", | |
| 4792 | + | "event-listener", | |
| 4793 | + | "futures-core", | |
| 4794 | + | "futures-lite", | |
| 4795 | + | "hex", | |
| 4796 | + | "libc", | |
| 4797 | + | "ordered-stream", | |
| 4798 | + | "rustix", | |
| 4799 | + | "serde", | |
| 4800 | + | "serde_repr", | |
| 4801 | + | "tracing", | |
| 4802 | + | "uds_windows", | |
| 4803 | + | "uuid", | |
| 4804 | + | "windows-sys 0.61.2", | |
| 4805 | + | "winnow 1.0.4", | |
| 4806 | + | "zbus_macros", | |
| 4807 | + | "zbus_names", | |
| 4808 | + | "zvariant", | |
| 4809 | + | ] | |
| 4810 | + | ||
| 4811 | + | [[package]] | |
| 4812 | + | name = "zbus_macros" | |
| 4813 | + | version = "5.19.0" | |
| 4814 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4815 | + | checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" | |
| 4816 | + | dependencies = [ | |
| 4817 | + | "proc-macro-crate 3.5.0", | |
| 4818 | + | "proc-macro2", | |
| 4819 | + | "quote", | |
| 4820 | + | "syn 3.0.9", | |
| 4821 | + | "zbus_names", | |
| 4822 | + | "zvariant", | |
| 4823 | + | "zvariant_utils", | |
| 4824 | + | ] | |
| 4825 | + | ||
| 4826 | + | [[package]] | |
| 4827 | + | name = "zbus_names" | |
| 4828 | + | version = "4.3.5" | |
| 4829 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4830 | + | checksum = "812a12be4ead7a59e18ea8f047d08add0066b73bc62fc1467066a2472ae088dd" | |
| 4831 | + | dependencies = [ | |
| 4832 | + | "serde", | |
| 4833 | + | "winnow 1.0.4", | |
| 4834 | + | "zvariant", | |
| 4835 | + | ] | |
| 4836 | + | ||
| 4837 | + | [[package]] | |
| 4838 | + | name = "zcheapstr" | |
| 4839 | + | version = "1.1.0" | |
| 4840 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4841 | + | checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" | |
| 4842 | + | dependencies = [ | |
| 4843 | + | "serde", | |
| 4844 | + | ] | |
| 4845 | + | ||
| 4338 | 4846 | [[package]] | |
| 4339 | 4847 | name = "zerofrom" | |
| 4340 | 4848 | version = "0.1.8" |
| ... | ... | @@ -4369,9 +4877,9 @@ dependencies = [ | |
| 4369 | 4877 | ||
| 4370 | 4878 | [[package]] | |
| 4371 | 4879 | name = "zerovec" | |
| 4372 | - | version = "0.11.8" | |
| 4880 | + | version = "0.11.9" | |
| 4373 | 4881 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4374 | - | checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" | |
| 4882 | + | checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40" | |
| 4375 | 4883 | dependencies = [ | |
| 4376 | 4884 | "yoke", | |
| 4377 | 4885 | "zerofrom", |
| ... | ... | @@ -4380,9 +4888,9 @@ dependencies = [ | |
| 4380 | 4888 | ||
| 4381 | 4889 | [[package]] | |
| 4382 | 4890 | name = "zerovec-derive" | |
| 4383 | - | version = "0.11.6" | |
| 4891 | + | version = "0.11.7" | |
| 4384 | 4892 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4385 | - | checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" | |
| 4893 | + | checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a" | |
| 4386 | 4894 | dependencies = [ | |
| 4387 | 4895 | "proc-macro2", | |
| 4388 | 4896 | "quote", |
| ... | ... | @@ -4400,3 +4908,44 @@ name = "zmij" | |
| 4400 | 4908 | version = "1.0.23" | |
| 4401 | 4909 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4402 | 4910 | checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" | |
| 4911 | + | ||
| 4912 | + | [[package]] | |
| 4913 | + | name = "zvariant" | |
| 4914 | + | version = "5.15.1" | |
| 4915 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4916 | + | checksum = "80bf6d6cd6a4b601acb64cfb8284cdc643fcfa75ef26ae3c46ec8aa4e8a1fbb7" | |
| 4917 | + | dependencies = [ | |
| 4918 | + | "endi", | |
| 4919 | + | "enumflags2", | |
| 4920 | + | "serde", | |
| 4921 | + | "winnow 1.0.4", | |
| 4922 | + | "zcheapstr", | |
| 4923 | + | "zvariant_derive", | |
| 4924 | + | "zvariant_utils", | |
| 4925 | + | ] | |
| 4926 | + | ||
| 4927 | + | [[package]] | |
| 4928 | + | name = "zvariant_derive" | |
| 4929 | + | version = "5.15.1" | |
| 4930 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4931 | + | checksum = "ef2672456ba1cb54b5eaec136e63135b3c7495fb938e53b28a62e633496d486d" | |
| 4932 | + | dependencies = [ | |
| 4933 | + | "proc-macro-crate 3.5.0", | |
| 4934 | + | "proc-macro2", | |
| 4935 | + | "quote", | |
| 4936 | + | "syn 3.0.9", | |
| 4937 | + | "zvariant_utils", | |
| 4938 | + | ] | |
| 4939 | + | ||
| 4940 | + | [[package]] | |
| 4941 | + | name = "zvariant_utils" | |
| 4942 | + | version = "4.2.0" | |
| 4943 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4944 | + | checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" | |
| 4945 | + | dependencies = [ | |
| 4946 | + | "proc-macro2", | |
| 4947 | + | "quote", | |
| 4948 | + | "serde", | |
| 4949 | + | "syn 3.0.9", | |
| 4950 | + | "winnow 1.0.4", | |
| 4951 | + | ] |
+12
-0
src-tauri/Cargo.toml
#
| ... | ... | @@ -22,3 +22,15 @@ serde = { version = "1.0", features = ["derive"] } | |
| 22 | 22 | log = "0.4" | |
| 23 | 23 | tauri = { version = "2.12.4", features = [] } | |
| 24 | 24 | tauri-plugin-log = "2" | |
| 25 | + | base64 = "0.22" | |
| 26 | + | hex = "0.4.3" | |
| 27 | + | mime_guess = "2" | |
| 28 | + | tokio = { version = "1", features = ["time"] } | |
| 29 | + | sha1 = "0.10" | |
| 30 | + | sha2 = "0.10" | |
| 31 | + | uuid = { version = "1", features = ["v4"] } | |
| 32 | + | infer = "0.22.0" | |
| 33 | + | tempfile = "3.27.0" | |
| 34 | + | ||
| 35 | + | sanitize-filename = "0.6" | |
| 36 | + | tauri-plugin-opener = "2" |
+38
-0
src-tauri/src/lib.rs
#
| ... | ... | @@ -1,14 +1,52 @@ | |
| 1 | + | mod media; | |
| 2 | + | ||
| 3 | + | use tauri::Manager; | |
| 4 | + | ||
| 1 | 5 | #[cfg_attr(mobile, tauri::mobile_entry_point)] | |
| 2 | 6 | pub fn run() { | |
| 3 | 7 | tauri::Builder::default() | |
| 8 | + | .plugin(tauri_plugin_opener::init()) | |
| 9 | + | .invoke_handler(tauri::generate_handler![ | |
| 10 | + | media::begin_media, | |
| 11 | + | media::write_media, | |
| 12 | + | media::finish_media, | |
| 13 | + | media::has_media, | |
| 14 | + | media::remove_media | |
| 15 | + | ]) | |
| 16 | + | // Is this not the protocol that tauri uses by default? Do we need to register it manually? | |
| 17 | + | // | |
| 18 | + | // eric: this is us registering our own handler here (media::protocol). So yes we do need to | |
| 19 | + | // register manually | |
| 20 | + | .register_asynchronous_uri_scheme_protocol("tauri", media::protocol) | |
| 4 | 21 | .setup(|app| { | |
| 22 | + | app.manage(media::MediaCache::new( | |
| 23 | + | // This is fine for now, but maybe we'll want some to be in cache dir and some in | |
| 24 | + | // more "permanent" dir eventually? We do than on Android anyway | |
| 25 | + | // | |
| 26 | + | // eric: there is also app_data_dir we could use which would be more permanent. | |
| 27 | + | // not sure right now how we would decide to split those but I think it can be | |
| 28 | + | // a future thing | |
| 29 | + | app.path().app_cache_dir()?.join("media"), | |
| 30 | + | )?); | |
| 5 | 31 | if cfg!(debug_assertions) { | |
| 32 | + | // Should we have this kind of debug log controlled by an env var so it can be | |
| 33 | + | // enabled at runtime instead of compile time? | |
| 34 | + | // | |
| 35 | + | // eric: this is just the default I didn't change it here. Could look into this | |
| 36 | + | // in a different change in the future. | |
| 6 | 37 | app.handle().plugin( | |
| 7 | 38 | tauri_plugin_log::Builder::default() | |
| 8 | 39 | .level(log::LevelFilter::Info) | |
| 9 | 40 | .build(), | |
| 10 | 41 | )?; | |
| 11 | 42 | } | |
| 43 | + | // Build the configured window here so native navigation hooks are installed | |
| 44 | + | // before its first page loads. | |
| 45 | + | let navigation_app = app.handle().clone(); | |
| 46 | + | let window = | |
| 47 | + | tauri::WebviewWindowBuilder::from_config(app, &app.config().app.windows[0])? | |
| 48 | + | .on_navigation(move |url| !media::open_navigation(&navigation_app, url)); | |
| 49 | + | window.build()?; | |
| 12 | 50 | Ok(()) | |
| 13 | 51 | }) | |
| 14 | 52 | .run(tauri::generate_context!()) |
+395
-0
src-tauri/src/media.rs
#
| ... | ... | @@ -0,0 +1,395 @@ | |
| 1 | + | mod hash; | |
| 2 | + | mod store; | |
| 3 | + | mod upload_slot; | |
| 4 | + | ||
| 5 | + | use hash::MediaHash; | |
| 6 | + | pub use store::MediaCache; | |
| 7 | + | use upload_slot::UploadSlot; | |
| 8 | + | ||
| 9 | + | use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; | |
| 10 | + | use std::{io, sync::Mutex, time::Duration}; | |
| 11 | + | use tauri::{ | |
| 12 | + | Manager, | |
| 13 | + | http::{Response, StatusCode, header::HeaderValue}, | |
| 14 | + | }; | |
| 15 | + | ||
| 16 | + | struct UploadResource { | |
| 17 | + | upload: Mutex<UploadSlot>, | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | impl tauri::Resource for UploadResource {} | |
| 21 | + | ||
| 22 | + | #[tauri::command] | |
| 23 | + | pub async fn begin_media<R: tauri::Runtime>( | |
| 24 | + | webview: tauri::Webview<R>, | |
| 25 | + | mime: String, | |
| 26 | + | ) -> Result<tauri::ResourceId, String> { | |
| 27 | + | let mime = HeaderValue::from_str(&mime).map_err(|error| error.to_string())?; | |
| 28 | + | let upload = UploadSlot::new(mime).map_err(|error| error.to_string())?; | |
| 29 | + | ||
| 30 | + | Ok(webview.resources_table().add(UploadResource { | |
| 31 | + | upload: Mutex::new(upload), | |
| 32 | + | })) | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | #[tauri::command] | |
| 36 | + | pub async fn write_media<R: tauri::Runtime>( | |
| 37 | + | webview: tauri::Webview<R>, | |
| 38 | + | request: tauri::ipc::Request<'_>, | |
| 39 | + | ) -> Result<(), String> { | |
| 40 | + | let rid = request | |
| 41 | + | .headers() | |
| 42 | + | .get("X-Media-Id") | |
| 43 | + | .and_then(|value| value.to_str().ok()) | |
| 44 | + | .and_then(|value| value.parse::<tauri::ResourceId>().ok()) | |
| 45 | + | .ok_or("Missing media resource ID")?; | |
| 46 | + | ||
| 47 | + | let tauri::ipc::InvokeBody::Raw(data) = request.body() else { | |
| 48 | + | return Err("Expected binary media data".into()); | |
| 49 | + | }; | |
| 50 | + | ||
| 51 | + | let resource = webview | |
| 52 | + | .resources_table() | |
| 53 | + | .get::<UploadResource>(rid) | |
| 54 | + | .map_err(|error| error.to_string())?; | |
| 55 | + | ||
| 56 | + | resource | |
| 57 | + | .upload | |
| 58 | + | .lock() | |
| 59 | + | .unwrap() | |
| 60 | + | .write(data) | |
| 61 | + | .map_err(|error| error.to_string()) | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | #[tauri::command] | |
| 65 | + | pub async fn finish_media<R: tauri::Runtime>( | |
| 66 | + | cache: tauri::State<'_, MediaCache>, | |
| 67 | + | webview: tauri::Webview<R>, | |
| 68 | + | rid: tauri::ResourceId, | |
| 69 | + | ) -> Result<String, String> { | |
| 70 | + | let resource = webview | |
| 71 | + | .resources_table() | |
| 72 | + | .take::<UploadResource>(rid) | |
| 73 | + | .map_err(|error| error.to_string())?; | |
| 74 | + | ||
| 75 | + | let resource = std::sync::Arc::try_unwrap(resource) | |
| 76 | + | .map_err(|_| "Media upload is still in use".to_owned())?; | |
| 77 | + | ||
| 78 | + | resource | |
| 79 | + | .upload | |
| 80 | + | .into_inner() | |
| 81 | + | .unwrap() | |
| 82 | + | .finish(&cache) | |
| 83 | + | .map_err(|error| error.to_string()) | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | #[tauri::command] | |
| 87 | + | pub async fn has_media( | |
| 88 | + | cache: tauri::State<'_, MediaCache>, | |
| 89 | + | algorithm: String, | |
| 90 | + | hex: String, | |
| 91 | + | ) -> Result<Option<String>, String> { | |
| 92 | + | if !matches!(algorithm.as_str(), "sha-1" | "sha-256") { | |
| 93 | + | return Ok(None); | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | let hash = MediaHash::from_hex(&algorithm, &hex).map_err(|error| error.to_string())?; | |
| 97 | + | cache.has(&hash).map_err(|error| error.to_string()) | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | #[tauri::command] | |
| 101 | + | pub async fn remove_media( | |
| 102 | + | cache: tauri::State<'_, MediaCache>, | |
| 103 | + | algorithm: String, | |
| 104 | + | hex: String, | |
| 105 | + | ) -> Result<(), String> { | |
| 106 | + | if !matches!(algorithm.as_str(), "sha-1" | "sha-256") { | |
| 107 | + | return Ok(()); | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | let hash = MediaHash::from_hex(&algorithm, &hex).map_err(|error| error.to_string())?; | |
| 111 | + | cache.remove(&hash).map_err(|error| error.to_string()) | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | // Only intercept navigations to this app's media URLs, never resource requests. | |
| 115 | + | pub fn open_navigation<R: tauri::Runtime>(app: &tauri::AppHandle<R>, url: &tauri::Url) -> bool { | |
| 116 | + | // Windows/Android use http(s)://tauri.localhost; macOS/Linux use tauri://localhost. | |
| 117 | + | // https://docs.rs/tauri/latest/tauri/struct.Builder.html#method.register_uri_scheme_protocol | |
| 118 | + | let local = (url.scheme() == "tauri" && url.host_str() == Some("localhost")) | |
| 119 | + | || (matches!(url.scheme(), "http" | "https") && url.host_str() == Some("tauri.localhost")) | |
| 120 | + | || (cfg!(debug_assertions) | |
| 121 | + | && app | |
| 122 | + | .config() | |
| 123 | + | .build | |
| 124 | + | .dev_url | |
| 125 | + | .as_ref() | |
| 126 | + | .is_some_and(|dev| dev.origin() == url.origin())); | |
| 127 | + | if !local { | |
| 128 | + | return false; | |
| 129 | + | } | |
| 130 | + | let Some(hash) = parse_path(url.path()) else { | |
| 131 | + | return false; | |
| 132 | + | }; | |
| 133 | + | let name = url | |
| 134 | + | .query_pairs() | |
| 135 | + | .find_map(|(key, value)| (key == "name").then(|| value.into_owned())); | |
| 136 | + | let app = app.clone(); | |
| 137 | + | tauri::async_runtime::spawn(async move { | |
| 138 | + | use tauri_plugin_opener::OpenerExt; | |
| 139 | + | let cache = app.state::<MediaCache>(); | |
| 140 | + | let result = cache | |
| 141 | + | .prepare_open(&hash, name.as_deref()) | |
| 142 | + | .map_err(|error| error.to_string()) | |
| 143 | + | .and_then(|file| { | |
| 144 | + | app.opener() | |
| 145 | + | .open_path(file.to_string_lossy(), None::<&str>) | |
| 146 | + | .map_err(|e| e.to_string()) | |
| 147 | + | }); | |
| 148 | + | if let Err(error) = result { | |
| 149 | + | log::error!("Could not open cached attachment: {error}"); | |
| 150 | + | } | |
| 151 | + | }); | |
| 152 | + | true | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | fn parse_path(path: &str) -> Option<MediaHash> { | |
| 156 | + | let (algorithm, encoded) = path.strip_prefix("/.well-known/ni/")?.split_once('/')?; | |
| 157 | + | let bytes = URL_SAFE_NO_PAD.decode(encoded).ok()?; | |
| 158 | + | MediaHash::from_bytes(algorithm, &bytes) | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | fn error_response(status: StatusCode) -> Response<Vec<u8>> { | |
| 162 | + | Response::builder().status(status).body(vec![]).unwrap() | |
| 163 | + | } | |
| 164 | + | ||
| 165 | + | pub fn protocol<R: tauri::Runtime>( | |
| 166 | + | context: tauri::UriSchemeContext<'_, R>, | |
| 167 | + | request: tauri::http::Request<Vec<u8>>, | |
| 168 | + | responder: tauri::UriSchemeResponder, | |
| 169 | + | ) { | |
| 170 | + | let app = context.app_handle().clone(); | |
| 171 | + | let path = request.uri().path().to_owned(); | |
| 172 | + | let head = request.method() == tauri::http::Method::HEAD; | |
| 173 | + | if path.starts_with("/.well-known/ni/") { | |
| 174 | + | let Some(hash) = parse_path(&path) else { | |
| 175 | + | responder.respond(error_response(StatusCode::BAD_REQUEST)); | |
| 176 | + | return; | |
| 177 | + | }; | |
| 178 | + | if !head && request.method() != tauri::http::Method::GET { | |
| 179 | + | responder.respond(error_response(StatusCode::METHOD_NOT_ALLOWED)); | |
| 180 | + | return; | |
| 181 | + | } | |
| 182 | + | tauri::async_runtime::spawn(async move { | |
| 183 | + | let cache = app.state::<MediaCache>(); | |
| 184 | + | match wait_response(&cache, hash, head, Duration::from_secs(5)).await { | |
| 185 | + | Ok(response) => responder.respond(response), | |
| 186 | + | Err(error) => { | |
| 187 | + | log::error!("Failed to serve cached media: {error}"); | |
| 188 | + | responder.respond(error_response(StatusCode::INTERNAL_SERVER_ERROR)); | |
| 189 | + | } | |
| 190 | + | } | |
| 191 | + | }); | |
| 192 | + | } else { | |
| 193 | + | tauri::async_runtime::spawn_blocking(move || { | |
| 194 | + | if let Some(asset) = app | |
| 195 | + | .asset_resolver() | |
| 196 | + | .get_for_scheme(path, request.uri().scheme_str() == Some("https")) | |
| 197 | + | { | |
| 198 | + | let mut response = Response::builder().header("Content-Type", asset.mime_type); | |
| 199 | + | if let Some(csp) = asset.csp_header { | |
| 200 | + | response = response.header("Content-Security-Policy", csp); | |
| 201 | + | } | |
| 202 | + | responder.respond( | |
| 203 | + | response | |
| 204 | + | .body(if head { vec![] } else { asset.bytes }) | |
| 205 | + | .unwrap(), | |
| 206 | + | ); | |
| 207 | + | } else { | |
| 208 | + | responder.respond(error_response(StatusCode::NOT_FOUND)); | |
| 209 | + | } | |
| 210 | + | }); | |
| 211 | + | } | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | async fn wait_response( | |
| 215 | + | cache: &MediaCache, | |
| 216 | + | hash: MediaHash, | |
| 217 | + | head: bool, | |
| 218 | + | interval: Duration, | |
| 219 | + | ) -> Result<Response<Vec<u8>>, String> { | |
| 220 | + | loop { | |
| 221 | + | match cache.response(&hash, head) { | |
| 222 | + | Ok(response) => return Ok(response), | |
| 223 | + | Err(error) if error.kind() == io::ErrorKind::NotFound => {} | |
| 224 | + | Err(error) => return Err(error.to_string()), | |
| 225 | + | } | |
| 226 | + | tokio::time::sleep(interval).await; | |
| 227 | + | } | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | #[cfg(test)] | |
| 231 | + | mod tests { | |
| 232 | + | use super::*; | |
| 233 | + | use std::sync::Arc; | |
| 234 | + | ||
| 235 | + | const SHA256: &str = "b6acae78c76442a73a94762eb91711a59c642ac3cfdaad636a061bdb7fad6975"; | |
| 236 | + | const SHA1: &str = "daa728737f5b69b8f46dd7df156ee770e738759c"; | |
| 237 | + | const DATA: &[u8] = b"media bytes"; | |
| 238 | + | ||
| 239 | + | fn hash(algorithm: &str, hex: &str) -> MediaHash { | |
| 240 | + | MediaHash::from_hex(algorithm, hex).unwrap() | |
| 241 | + | } | |
| 242 | + | ||
| 243 | + | fn hash_url(algorithm: &str, hex: &str) -> String { | |
| 244 | + | hash(algorithm, hex).url() | |
| 245 | + | } | |
| 246 | + | ||
| 247 | + | struct Fixture { | |
| 248 | + | cache: Arc<MediaCache>, | |
| 249 | + | _directory: tempfile::TempDir, | |
| 250 | + | } | |
| 251 | + | impl Fixture { | |
| 252 | + | fn new() -> Self { | |
| 253 | + | let directory = tempfile::tempdir().unwrap(); | |
| 254 | + | Self { | |
| 255 | + | cache: Arc::new(MediaCache::new(directory.path().to_path_buf()).unwrap()), | |
| 256 | + | _directory: directory, | |
| 257 | + | } | |
| 258 | + | } | |
| 259 | + | fn begin(&self, mime: String) -> io::Result<UploadSlot> { | |
| 260 | + | UploadSlot::new( | |
| 261 | + | HeaderValue::from_str(&mime) | |
| 262 | + | .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?, | |
| 263 | + | ) | |
| 264 | + | } | |
| 265 | + | ||
| 266 | + | fn publish(&self, mime: &str) { | |
| 267 | + | let mut upload = self.begin(mime.into()).unwrap(); | |
| 268 | + | upload.write(&DATA[..3]).unwrap(); | |
| 269 | + | upload.write(&DATA[3..]).unwrap(); | |
| 270 | + | assert_eq!( | |
| 271 | + | upload.finish(&self.cache).unwrap(), | |
| 272 | + | hash_url("sha-256", SHA256) | |
| 273 | + | ); | |
| 274 | + | } | |
| 275 | + | } | |
| 276 | + | ||
| 277 | + | #[test] | |
| 278 | + | fn finish_consumes_resource() { | |
| 279 | + | let fixture = Fixture::new(); | |
| 280 | + | let mut resources = tauri::ResourceTable::default(); | |
| 281 | + | let rid = resources.add(UploadResource { | |
| 282 | + | upload: Mutex::new(fixture.begin("image/png".into()).unwrap()), | |
| 283 | + | }); | |
| 284 | + | resources | |
| 285 | + | .get::<UploadResource>(rid) | |
| 286 | + | .unwrap() | |
| 287 | + | .upload | |
| 288 | + | .lock() | |
| 289 | + | .unwrap() | |
| 290 | + | .write(DATA) | |
| 291 | + | .unwrap(); | |
| 292 | + | let resource = Arc::into_inner(resources.take::<UploadResource>(rid).unwrap()).unwrap(); | |
| 293 | + | let url = resource | |
| 294 | + | .upload | |
| 295 | + | .into_inner() | |
| 296 | + | .unwrap() | |
| 297 | + | .finish(&fixture.cache) | |
| 298 | + | .unwrap(); | |
| 299 | + | assert_eq!(url, hash_url("sha-256", SHA256)); | |
| 300 | + | assert!(resources.get::<UploadResource>(rid).is_err()); | |
| 301 | + | assert!(resources.take::<UploadResource>(rid).is_err()); | |
| 302 | + | assert!(!resources.has(rid)); | |
| 303 | + | assert_eq!( | |
| 304 | + | fixture.cache.has(&hash("sha-1", SHA1)).unwrap(), | |
| 305 | + | Some(hash_url("sha-1", SHA1)) | |
| 306 | + | ); | |
| 307 | + | } | |
| 308 | + | ||
| 309 | + | #[test] | |
| 310 | + | fn empty_and_interleaved_uploads_have_independent_hashes() { | |
| 311 | + | let fixture = Fixture::new(); | |
| 312 | + | let empty = fixture.begin("application/octet-stream".into()).unwrap(); | |
| 313 | + | let mut first = fixture.begin("image/png".into()).unwrap(); | |
| 314 | + | let mut second = fixture.begin("image/png".into()).unwrap(); | |
| 315 | + | first.write(&DATA[..3]).unwrap(); | |
| 316 | + | second.write(DATA).unwrap(); | |
| 317 | + | first.write(&DATA[3..]).unwrap(); | |
| 318 | + | assert_eq!( | |
| 319 | + | first.finish(&fixture.cache).unwrap(), | |
| 320 | + | second.finish(&fixture.cache).unwrap() | |
| 321 | + | ); | |
| 322 | + | let empty_hex = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; | |
| 323 | + | assert_eq!( | |
| 324 | + | empty.finish(&fixture.cache).unwrap(), | |
| 325 | + | hash_url("sha-256", empty_hex) | |
| 326 | + | ); | |
| 327 | + | let response = fixture | |
| 328 | + | .cache | |
| 329 | + | .response( | |
| 330 | + | &hash("sha-1", "da39a3ee5e6b4b0d3255bfef95601890afd80709"), | |
| 331 | + | false, | |
| 332 | + | ) | |
| 333 | + | .unwrap(); | |
| 334 | + | assert!(response.body().is_empty()); | |
| 335 | + | assert_eq!( | |
| 336 | + | fixture.cache.has(&hash("sha-1", SHA1)).unwrap(), | |
| 337 | + | Some(hash_url("sha-1", SHA1)) | |
| 338 | + | ); | |
| 339 | + | } | |
| 340 | + | ||
| 341 | + | #[test] | |
| 342 | + | fn validates_paths_and_publication() { | |
| 343 | + | let fixture = Fixture::new(); | |
| 344 | + | for path in [ | |
| 345 | + | "/etc/passwd", | |
| 346 | + | "/.well-known/ni/sha-256/../../etc/passwd", | |
| 347 | + | "/.well-known/ni/sha-1/../file", | |
| 348 | + | "/.well-known/ni/sha-1/AA", | |
| 349 | + | "/.well-known/ni/sha-512/AA", | |
| 350 | + | "/.well-known/ni/sha-256/%2e%2e", | |
| 351 | + | "/.well-known/ni/sha-1/", | |
| 352 | + | ] { | |
| 353 | + | assert!(parse_path(path).is_none(), "{path}"); | |
| 354 | + | } | |
| 355 | + | for (algorithm, hex, length) in [("sha-1", SHA1, 20), ("sha-256", SHA256, 32)] { | |
| 356 | + | let bytes: Vec<u8> = (0..length) | |
| 357 | + | .map(|i| u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).unwrap()) | |
| 358 | + | .collect(); | |
| 359 | + | assert_eq!( | |
| 360 | + | parse_path(&format!( | |
| 361 | + | "/.well-known/ni/{algorithm}/{}", | |
| 362 | + | URL_SAFE_NO_PAD.encode(bytes) | |
| 363 | + | )), | |
| 364 | + | Some(hash(algorithm, hex)) | |
| 365 | + | ); | |
| 366 | + | } | |
| 367 | + | assert!(fixture.begin("image/png\r\nX-Test: bad".into()).is_err()); | |
| 368 | + | } | |
| 369 | + | ||
| 370 | + | #[test] | |
| 371 | + | fn pending_request_loads_after_publication() { | |
| 372 | + | let fixture = Fixture::new(); | |
| 373 | + | tauri::async_runtime::block_on(async { | |
| 374 | + | let cache = fixture.cache.clone(); | |
| 375 | + | let pending = tauri::async_runtime::spawn(async move { | |
| 376 | + | wait_response( | |
| 377 | + | &cache, | |
| 378 | + | hash("sha-1", SHA1), | |
| 379 | + | false, | |
| 380 | + | Duration::from_millis(10), | |
| 381 | + | ) | |
| 382 | + | .await | |
| 383 | + | }); | |
| 384 | + | tokio::time::sleep(Duration::from_millis(30)).await; | |
| 385 | + | assert!(!pending.inner().is_finished()); | |
| 386 | + | fixture.publish("image/png"); | |
| 387 | + | let response = tokio::time::timeout(Duration::from_secs(2), pending) | |
| 388 | + | .await | |
| 389 | + | .unwrap() | |
| 390 | + | .unwrap() | |
| 391 | + | .unwrap(); | |
| 392 | + | assert_eq!(response.body(), DATA); | |
| 393 | + | }); | |
| 394 | + | } | |
| 395 | + | } |
+79
-0
src-tauri/src/media/hash.rs
#
| ... | ... | @@ -0,0 +1,79 @@ | |
| 1 | + | use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; | |
| 2 | + | use std::io; | |
| 3 | + | ||
| 4 | + | #[cfg_attr(test, derive(Debug, PartialEq))] | |
| 5 | + | pub(super) enum MediaHash { | |
| 6 | + | Sha1([u8; 20]), | |
| 7 | + | Sha256([u8; 32]), | |
| 8 | + | } | |
| 9 | + | ||
| 10 | + | impl MediaHash { | |
| 11 | + | pub(super) fn from_bytes(algorithm: &str, bytes: &[u8]) -> Option<Self> { | |
| 12 | + | match algorithm { | |
| 13 | + | "sha-1" => Some(Self::Sha1(bytes.try_into().ok()?)), | |
| 14 | + | "sha-256" => Some(Self::Sha256(bytes.try_into().ok()?)), | |
| 15 | + | _ => None, | |
| 16 | + | } | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | pub(super) fn from_hex(algorithm: &str, hex: &str) -> io::Result<Self> { | |
| 20 | + | let bytes = | |
| 21 | + | hex::decode(hex).map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?; | |
| 22 | + | Self::from_bytes(algorithm, &bytes) | |
| 23 | + | .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Invalid media hash")) | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | fn bytes(&self) -> &[u8] { | |
| 27 | + | match self { | |
| 28 | + | Self::Sha1(bytes) => bytes, | |
| 29 | + | Self::Sha256(bytes) => bytes, | |
| 30 | + | } | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | pub(super) fn hex(&self) -> String { | |
| 34 | + | hex::encode(self.bytes()) | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | pub(super) fn url(&self) -> String { | |
| 38 | + | let algorithm = match self { | |
| 39 | + | Self::Sha1(_) => "sha-1", | |
| 40 | + | Self::Sha256(_) => "sha-256", | |
| 41 | + | }; | |
| 42 | + | format!( | |
| 43 | + | "/.well-known/ni/{algorithm}/{}", | |
| 44 | + | URL_SAFE_NO_PAD.encode(self.bytes()) | |
| 45 | + | ) | |
| 46 | + | } | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | #[cfg(test)] | |
| 50 | + | mod tests { | |
| 51 | + | use super::*; | |
| 52 | + | #[test] | |
| 53 | + | fn validates_external_algorithms_and_hashes() { | |
| 54 | + | assert!(MediaHash::from_hex("sha-512", "00").is_err()); | |
| 55 | + | for algorithm in ["sha-1", "sha-256"] { | |
| 56 | + | for hex in ["00".to_owned(), "../outside".to_owned(), "g".repeat(64)] { | |
| 57 | + | assert!(MediaHash::from_hex(algorithm, &hex).is_err()); | |
| 58 | + | } | |
| 59 | + | } | |
| 60 | + | } | |
| 61 | + | #[test] | |
| 62 | + | fn hashes_round_trip_and_generate_urls() { | |
| 63 | + | for (algorithm, length, encoded) in [ | |
| 64 | + | ("sha-1", 20, "AAAAAAAAAAAAAAAAAAAAAAAAAAA"), | |
| 65 | + | ("sha-256", 32, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), | |
| 66 | + | ] { | |
| 67 | + | let hex = "00".repeat(length); | |
| 68 | + | let hash = MediaHash::from_hex(algorithm, &hex).unwrap(); | |
| 69 | + | assert_eq!( | |
| 70 | + | hash, | |
| 71 | + | MediaHash::from_bytes(algorithm, &vec![0; length]).unwrap() | |
| 72 | + | ); | |
| 73 | + | assert_eq!(hash.hex(), hex); | |
| 74 | + | assert_eq!(hash.url(), format!("/.well-known/ni/{algorithm}/{encoded}")); | |
| 75 | + | assert!(MediaHash::from_bytes(algorithm, &vec![0; length - 1]).is_none()); | |
| 76 | + | assert!(MediaHash::from_bytes(algorithm, &vec![0; length + 1]).is_none()); | |
| 77 | + | } | |
| 78 | + | } | |
| 79 | + | } |
+441
-0
src-tauri/src/media/store.rs
#
| ... | ... | @@ -0,0 +1,441 @@ | |
| 1 | + | use super::hash::MediaHash; | |
| 2 | + | use std::{ | |
| 3 | + | fs, | |
| 4 | + | io::{self, Read}, | |
| 5 | + | path::PathBuf, | |
| 6 | + | }; | |
| 7 | + | use tauri::http::{Response, header::HeaderValue}; | |
| 8 | + | ||
| 9 | + | pub struct MediaCache { | |
| 10 | + | root: PathBuf, | |
| 11 | + | tmp_dir: PathBuf, | |
| 12 | + | } | |
| 13 | + | ||
| 14 | + | struct ResolvedMedia { | |
| 15 | + | root: PathBuf, | |
| 16 | + | sha256: MediaHash, | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | impl ResolvedMedia { | |
| 20 | + | fn hex(&self) -> String { | |
| 21 | + | self.sha256.hex() | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | fn media_path(&self) -> PathBuf { | |
| 25 | + | self.root.join("files").join(format!("f{}", self.hex())) | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | fn mime_path(&self) -> PathBuf { | |
| 29 | + | self.root.join("mime").join(format!("{}.mime", self.hex())) | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | fn alias_path(&self, hash: &MediaHash) -> PathBuf { | |
| 33 | + | self.root.join("aliases").join(hash.hex()) | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | fn read(&self) -> io::Result<Vec<u8>> { | |
| 37 | + | fs::read(self.media_path()) | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | fn mime(&self) -> io::Result<String> { | |
| 41 | + | fs::read_to_string(self.mime_path()) | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | fn write_mime(&self, mime: &HeaderValue) -> io::Result<()> { | |
| 45 | + | fs::write(self.mime_path(), mime.as_bytes()) | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | fn write_alias(&self, hash: &MediaHash) -> io::Result<()> { | |
| 49 | + | fs::write(self.alias_path(hash), self.hex()) | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | fn exists(&self) -> io::Result<bool> { | |
| 53 | + | Ok(self.media_path().try_exists()? && self.mime_path().try_exists()?) | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | fn remove(&self) -> io::Result<()> { | |
| 57 | + | self.remove_file(self.media_path())?; | |
| 58 | + | self.remove_file(self.mime_path()) | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | fn remove_file(&self, path: PathBuf) -> io::Result<()> { | |
| 62 | + | match fs::remove_file(path) { | |
| 63 | + | Ok(()) => Ok(()), | |
| 64 | + | Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()), | |
| 65 | + | Err(error) => Err(error), | |
| 66 | + | } | |
| 67 | + | } | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | impl MediaCache { | |
| 71 | + | pub(super) fn prepare_open(&self, hash: &MediaHash, name: Option<&str>) -> io::Result<PathBuf> { | |
| 72 | + | let media = self.resolve(hash)?; | |
| 73 | + | let mime = media.mime()?; | |
| 74 | + | let mime = mime.split(';').next().unwrap().trim(); | |
| 75 | + | let source = media.media_path(); | |
| 76 | + | let name = name | |
| 77 | + | .map(sanitize_filename::sanitize) | |
| 78 | + | .filter(|name| !name.is_empty()); | |
| 79 | + | let name = if let Some(name) = name { | |
| 80 | + | name | |
| 81 | + | } else { | |
| 82 | + | // TODO: This sucks I think, need to rework this | |
| 83 | + | // | |
| 84 | + | // mime_guess lists JPEG's `jfif` alias first; prefer the extension | |
| 85 | + | // commonly registered with desktop image viewers. | |
| 86 | + | let extension = if mime.eq_ignore_ascii_case("image/jpeg") { | |
| 87 | + | Some("jpg") | |
| 88 | + | } else if mime.eq_ignore_ascii_case("application/octet-stream") { | |
| 89 | + | // Generic binary MIME has no meaningful extension. Inspect the bytes | |
| 90 | + | // rather than selecting an arbitrary alias such as `aaf`. | |
| 91 | + | ||
| 92 | + | let mut header = Vec::new(); | |
| 93 | + | fs::File::open(&source)? | |
| 94 | + | .take(8192) | |
| 95 | + | .read_to_end(&mut header)?; | |
| 96 | + | Some(infer::get(&header).map_or("bin", |kind| kind.extension())) | |
| 97 | + | } else { | |
| 98 | + | mime_guess::get_mime_extensions_str(mime).and_then(|v| v.first().copied()) | |
| 99 | + | }; | |
| 100 | + | extension.map_or_else( | |
| 101 | + | || "attachment".to_owned(), | |
| 102 | + | |ext| format!("attachment.{ext}"), | |
| 103 | + | ) | |
| 104 | + | }; | |
| 105 | + | let directory = self.tmp_dir.join(media.hex()); | |
| 106 | + | fs::create_dir_all(&directory)?; | |
| 107 | + | let destination = directory.join(name); | |
| 108 | + | fs::copy(source, &destination)?; | |
| 109 | + | Ok(destination) | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | pub fn new(root: PathBuf) -> io::Result<Self> { | |
| 113 | + | for directory in ["files", "mime", "aliases"] { | |
| 114 | + | fs::create_dir_all(root.join(directory))?; | |
| 115 | + | } | |
| 116 | + | Ok(Self { | |
| 117 | + | root, | |
| 118 | + | tmp_dir: std::env::temp_dir().join("cheogram"), | |
| 119 | + | }) | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | fn resolve(&self, hash: &MediaHash) -> io::Result<ResolvedMedia> { | |
| 123 | + | let sha256 = match hash { | |
| 124 | + | MediaHash::Sha256(bytes) => MediaHash::Sha256(*bytes), | |
| 125 | + | MediaHash::Sha1(_) => { | |
| 126 | + | let alias = fs::read_to_string(self.root.join("aliases").join(hash.hex()))?; | |
| 127 | + | MediaHash::from_hex("sha-256", &alias) | |
| 128 | + | .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))? | |
| 129 | + | } | |
| 130 | + | }; | |
| 131 | + | Ok(ResolvedMedia { | |
| 132 | + | root: self.root.clone(), | |
| 133 | + | sha256, | |
| 134 | + | }) | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | pub(super) fn publish( | |
| 138 | + | &self, | |
| 139 | + | temporary: tempfile::TempPath, | |
| 140 | + | mime: &HeaderValue, | |
| 141 | + | sha256: MediaHash, | |
| 142 | + | sha1: &MediaHash, | |
| 143 | + | ) -> io::Result<()> { | |
| 144 | + | let media = ResolvedMedia { | |
| 145 | + | root: self.root.clone(), | |
| 146 | + | sha256, | |
| 147 | + | }; | |
| 148 | + | fs::copy(&temporary, media.media_path())?; | |
| 149 | + | media.write_mime(mime)?; | |
| 150 | + | media.write_alias(sha1)?; | |
| 151 | + | Ok(()) | |
| 152 | + | } | |
| 153 | + | ||
| 154 | + | pub(super) fn has(&self, hash: &MediaHash) -> io::Result<Option<String>> { | |
| 155 | + | match self.resolve(hash) { | |
| 156 | + | Ok(media) => media.exists().map(|found| found.then(|| hash.url())), | |
| 157 | + | Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), | |
| 158 | + | Err(error) => Err(error), | |
| 159 | + | } | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | pub(super) fn remove(&self, hash: &MediaHash) -> io::Result<()> { | |
| 163 | + | match self.resolve(hash) { | |
| 164 | + | Ok(media) => media.remove(), | |
| 165 | + | Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()), | |
| 166 | + | Err(error) => Err(error), | |
| 167 | + | } | |
| 168 | + | } | |
| 169 | + | ||
| 170 | + | pub(super) fn response(&self, hash: &MediaHash, head: bool) -> io::Result<Response<Vec<u8>>> { | |
| 171 | + | let media = self.resolve(hash)?; | |
| 172 | + | let mime = media.mime()?; | |
| 173 | + | let bytes = media.read()?; | |
| 174 | + | Response::builder() | |
| 175 | + | .header("Content-Type", mime) | |
| 176 | + | .header("Content-Length", bytes.len()) | |
| 177 | + | .body(if head { vec![] } else { bytes }) | |
| 178 | + | .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error)) | |
| 179 | + | } | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | #[cfg(test)] | |
| 183 | + | mod tests { | |
| 184 | + | use std::fs; | |
| 185 | + | ||
| 186 | + | use super::*; | |
| 187 | + | use crate::media::upload_slot::UploadSlot; | |
| 188 | + | ||
| 189 | + | const SHA256: &str = "b6acae78c76442a73a94762eb91711a59c642ac3cfdaad636a061bdb7fad6975"; | |
| 190 | + | const SHA1: &str = "daa728737f5b69b8f46dd7df156ee770e738759c"; | |
| 191 | + | const DATA: &[u8] = b"media bytes"; | |
| 192 | + | ||
| 193 | + | fn hash(algorithm: &str, hex: &str) -> MediaHash { | |
| 194 | + | MediaHash::from_hex(algorithm, hex).unwrap() | |
| 195 | + | } | |
| 196 | + | ||
| 197 | + | fn hash_url(algorithm: &str, hex: &str) -> String { | |
| 198 | + | hash(algorithm, hex).url() | |
| 199 | + | } | |
| 200 | + | ||
| 201 | + | struct Fixture { | |
| 202 | + | cache: MediaCache, | |
| 203 | + | _directory: tempfile::TempDir, | |
| 204 | + | } | |
| 205 | + | impl Fixture { | |
| 206 | + | fn new() -> Self { | |
| 207 | + | let directory = tempfile::tempdir().unwrap(); | |
| 208 | + | let mut cache = MediaCache::new(directory.path().join("cache")).unwrap(); | |
| 209 | + | assert_eq!(cache.tmp_dir, std::env::temp_dir().join("cheogram")); | |
| 210 | + | cache.tmp_dir = directory.path().join("open"); | |
| 211 | + | Self { | |
| 212 | + | cache, | |
| 213 | + | _directory: directory, | |
| 214 | + | } | |
| 215 | + | } | |
| 216 | + | fn begin(&self, mime: String) -> io::Result<UploadSlot> { | |
| 217 | + | UploadSlot::new( | |
| 218 | + | HeaderValue::from_str(&mime) | |
| 219 | + | .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?, | |
| 220 | + | ) | |
| 221 | + | } | |
| 222 | + | ||
| 223 | + | fn publish(&self, mime: &str) { | |
| 224 | + | let mut upload = self.begin(mime.into()).unwrap(); | |
| 225 | + | upload.write(&DATA[..3]).unwrap(); | |
| 226 | + | upload.write(&DATA[3..]).unwrap(); | |
| 227 | + | assert_eq!( | |
| 228 | + | upload.finish(&self.cache).unwrap(), | |
| 229 | + | hash_url("sha-256", SHA256) | |
| 230 | + | ); | |
| 231 | + | } | |
| 232 | + | } | |
| 233 | + | ||
| 234 | + | #[test] | |
| 235 | + | fn invalid_mime_sidecars_return_io_errors() { | |
| 236 | + | let fixture = Fixture::new(); | |
| 237 | + | fixture.publish("image/png"); | |
| 238 | + | let media = fixture.cache.resolve(&hash("sha-256", SHA256)).unwrap(); | |
| 239 | + | for bytes in [b"image/png\r\nX-Test: bad".as_slice(), &[0xff]] { | |
| 240 | + | fs::write(media.mime_path(), bytes).unwrap(); | |
| 241 | + | let error = fixture | |
| 242 | + | .cache | |
| 243 | + | .response(&hash("sha-1", SHA1), false) | |
| 244 | + | .unwrap_err(); | |
| 245 | + | assert_eq!(error.kind(), io::ErrorKind::InvalidData); | |
| 246 | + | } | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | #[test] | |
| 250 | + | fn opening_copies_aliases_with_mime_extensions() { | |
| 251 | + | let fixture = Fixture::new(); | |
| 252 | + | fixture.publish("image/png"); | |
| 253 | + | let path = fixture | |
| 254 | + | .cache | |
| 255 | + | .prepare_open(&hash("sha-1", SHA1), None) | |
| 256 | + | .unwrap(); | |
| 257 | + | assert_eq!(path.extension().unwrap(), "png"); | |
| 258 | + | assert!(path.starts_with(fixture.cache.tmp_dir.clone())); | |
| 259 | + | assert_eq!(fs::read(&path).unwrap(), DATA); | |
| 260 | + | fs::write(&path, b"edited").unwrap(); | |
| 261 | + | assert_eq!( | |
| 262 | + | fs::read(fixture.cache.root.join("files").join(format!("f{SHA256}"))).unwrap(), | |
| 263 | + | DATA | |
| 264 | + | ); | |
| 265 | + | } | |
| 266 | + | ||
| 267 | + | #[test] | |
| 268 | + | fn reopening_reuses_the_path_and_restores_cached_contents() { | |
| 269 | + | let fixture = Fixture::new(); | |
| 270 | + | fixture.publish("image/png"); | |
| 271 | + | let first = fixture | |
| 272 | + | .cache | |
| 273 | + | .prepare_open(&hash("sha-1", SHA1), Some("photo.png")) | |
| 274 | + | .unwrap(); | |
| 275 | + | fs::write(&first, b"edited").unwrap(); | |
| 276 | + | let second = fixture | |
| 277 | + | .cache | |
| 278 | + | .prepare_open(&hash("sha-256", SHA256), Some("photo.png")) | |
| 279 | + | .unwrap(); | |
| 280 | + | assert_eq!(first, second); | |
| 281 | + | assert_eq!(fs::read(second).unwrap(), DATA); | |
| 282 | + | } | |
| 283 | + | ||
| 284 | + | #[test] | |
| 285 | + | fn opening_uses_the_original_filename_or_falls_back_to_mime() { | |
| 286 | + | let fixture = Fixture::new(); | |
| 287 | + | fixture.publish("image/png"); | |
| 288 | + | let hash = hash("sha-256", SHA256); | |
| 289 | + | for name in ["photo.jpg", "résumé + notes & stuff #1.pdf", "notes"] { | |
| 290 | + | let path = fixture.cache.prepare_open(&hash, Some(name)).unwrap(); | |
| 291 | + | assert_eq!(path.file_name().unwrap(), name); | |
| 292 | + | assert_eq!( | |
| 293 | + | path.parent().unwrap().parent().unwrap(), | |
| 294 | + | fixture.cache.tmp_dir.clone() | |
| 295 | + | ); | |
| 296 | + | assert_eq!(fs::read(&path).unwrap(), DATA); | |
| 297 | + | } | |
| 298 | + | for (name, expected) in [ | |
| 299 | + | ("", "attachment.png"), | |
| 300 | + | (".", "attachment.png"), | |
| 301 | + | ("..", "attachment.png"), | |
| 302 | + | ("/", "attachment.png"), | |
| 303 | + | ("../outside.png", "..outside.png"), | |
| 304 | + | ("/outside.png", "outside.png"), | |
| 305 | + | ("..\\outside.png", "..outside.png"), | |
| 306 | + | ("bad\0.png", "bad.png"), | |
| 307 | + | ("C:photo.png", "Cphoto.png"), | |
| 308 | + | ] { | |
| 309 | + | let path = fixture.cache.prepare_open(&hash, Some(name)).unwrap(); | |
| 310 | + | assert_eq!(path.file_name().unwrap(), expected); | |
| 311 | + | assert_eq!( | |
| 312 | + | path.parent().unwrap().parent().unwrap(), | |
| 313 | + | fixture.cache.tmp_dir.clone() | |
| 314 | + | ); | |
| 315 | + | assert_eq!(fs::read(&path).unwrap(), DATA); | |
| 316 | + | } | |
| 317 | + | } | |
| 318 | + | ||
| 319 | + | #[test] | |
| 320 | + | fn opening_generic_binary_detects_content_or_uses_bin() { | |
| 321 | + | let fixture = Fixture::new(); | |
| 322 | + | fixture.publish("application/octet-stream"); | |
| 323 | + | let path = fixture | |
| 324 | + | .cache | |
| 325 | + | .prepare_open(&hash("sha-256", SHA256), None) | |
| 326 | + | .unwrap(); | |
| 327 | + | assert_eq!(path.extension().unwrap(), "bin"); | |
| 328 | + | ||
| 329 | + | let mut upload = fixture.begin("application/octet-stream".into()).unwrap(); | |
| 330 | + | upload.write(b"%PDF-1.7\n").unwrap(); | |
| 331 | + | let url = upload.finish(&fixture.cache).unwrap(); | |
| 332 | + | let hash = crate::media::parse_path(&url).unwrap(); | |
| 333 | + | let path = fixture.cache.prepare_open(&hash, None).unwrap(); | |
| 334 | + | assert_eq!(path.extension().unwrap(), "pdf"); | |
| 335 | + | } | |
| 336 | + | ||
| 337 | + | #[test] | |
| 338 | + | fn opening_jpeg_uses_jpg_instead_of_jfif() { | |
| 339 | + | let fixture = Fixture::new(); | |
| 340 | + | fixture.publish("image/jpeg; charset=binary"); | |
| 341 | + | let path = fixture | |
| 342 | + | .cache | |
| 343 | + | .prepare_open(&hash("sha-256", SHA256), None) | |
| 344 | + | .unwrap(); | |
| 345 | + | assert_eq!(path.file_name().unwrap(), "attachment.jpg"); | |
| 346 | + | assert_eq!(fs::read(path).unwrap(), DATA); | |
| 347 | + | } | |
| 348 | + | ||
| 349 | + | #[test] | |
| 350 | + | fn serves_aliases_and_mime_after_restart() { | |
| 351 | + | let fixture = Fixture::new(); | |
| 352 | + | fixture.publish("image/svg+xml"); | |
| 353 | + | let reopened = MediaCache::new(fixture.cache.root.clone()).unwrap(); | |
| 354 | + | for (algorithm, hex) in [("sha-1", SHA1), ("sha-256", SHA256)] { | |
| 355 | + | assert_eq!( | |
| 356 | + | reopened.has(&hash(algorithm, hex)).unwrap(), | |
| 357 | + | Some(hash_url(algorithm, hex)) | |
| 358 | + | ); | |
| 359 | + | let response = reopened.response(&hash(algorithm, hex), false).unwrap(); | |
| 360 | + | assert_eq!(response.body(), DATA); | |
| 361 | + | assert_eq!(response.headers()["content-type"], "image/svg+xml"); | |
| 362 | + | assert_eq!(response.headers()["content-length"], DATA.len().to_string()); | |
| 363 | + | } | |
| 364 | + | let head = reopened.response(&hash("sha-1", SHA1), true).unwrap(); | |
| 365 | + | assert!(head.body().is_empty()); | |
| 366 | + | assert_eq!(head.headers()["content-length"], DATA.len().to_string()); | |
| 367 | + | } | |
| 368 | + | ||
| 369 | + | #[test] | |
| 370 | + | fn duplicates_and_deletion_work_for_both_algorithms() { | |
| 371 | + | for algorithm in ["sha-1", "sha-256"] { | |
| 372 | + | let fixture = Fixture::new(); | |
| 373 | + | fixture.publish("image/png"); | |
| 374 | + | fixture.publish("application/octet-stream"); | |
| 375 | + | let response = fixture.cache.response(&hash("sha-1", SHA1), false).unwrap(); | |
| 376 | + | assert_eq!(response.body(), DATA); | |
| 377 | + | assert_eq!( | |
| 378 | + | response.headers()["content-type"], | |
| 379 | + | "application/octet-stream" | |
| 380 | + | ); | |
| 381 | + | let hex = if algorithm == "sha-1" { SHA1 } else { SHA256 }; | |
| 382 | + | fixture.cache.remove(&hash(algorithm, hex)).unwrap(); | |
| 383 | + | fixture.cache.remove(&hash(algorithm, hex)).unwrap(); | |
| 384 | + | assert_eq!(fixture.cache.has(&hash("sha-1", SHA1)).unwrap(), None); | |
| 385 | + | assert_eq!(fixture.cache.has(&hash("sha-256", SHA256)).unwrap(), None); | |
| 386 | + | assert!( | |
| 387 | + | !fixture | |
| 388 | + | .cache | |
| 389 | + | .root | |
| 390 | + | .join("mime") | |
| 391 | + | .join(format!("{SHA256}.mime")) | |
| 392 | + | .exists() | |
| 393 | + | ); | |
| 394 | + | } | |
| 395 | + | } | |
| 396 | + | ||
| 397 | + | #[test] | |
| 398 | + | fn missing_or_evicted_files_and_old_files_without_mime_are_misses() { | |
| 399 | + | let fixture = Fixture::new(); | |
| 400 | + | assert!( | |
| 401 | + | fixture | |
| 402 | + | .cache | |
| 403 | + | .response(&hash("sha-1", SHA1), false) | |
| 404 | + | .is_err_and(|error| error.kind() == io::ErrorKind::NotFound) | |
| 405 | + | ); | |
| 406 | + | let path = fixture.cache.root.join("files").join(format!("f{SHA256}")); | |
| 407 | + | fs::write(&path, DATA).unwrap(); | |
| 408 | + | // Old JSON sidecars are ignored; the next download writes a MIME sidecar. | |
| 409 | + | fs::create_dir(fixture.cache.root.join("metadata")).unwrap(); | |
| 410 | + | fs::write( | |
| 411 | + | fixture | |
| 412 | + | .cache | |
| 413 | + | .root | |
| 414 | + | .join("metadata") | |
| 415 | + | .join(format!("{SHA256}.json")), | |
| 416 | + | br#"{"contentType":"image/png"}"#, | |
| 417 | + | ) | |
| 418 | + | .unwrap(); | |
| 419 | + | assert_eq!(fixture.cache.has(&hash("sha-256", SHA256)).unwrap(), None); | |
| 420 | + | fixture.publish("image/png"); | |
| 421 | + | fs::remove_file(path).unwrap(); | |
| 422 | + | assert_eq!(fixture.cache.has(&hash("sha-1", SHA1)).unwrap(), None); | |
| 423 | + | assert!( | |
| 424 | + | fixture | |
| 425 | + | .cache | |
| 426 | + | .response(&hash("sha-256", SHA256), false) | |
| 427 | + | .is_err_and(|error| error.kind() == io::ErrorKind::NotFound) | |
| 428 | + | ); | |
| 429 | + | } | |
| 430 | + | ||
| 431 | + | #[test] | |
| 432 | + | fn validates_disk_aliases() { | |
| 433 | + | let fixture = Fixture::new(); | |
| 434 | + | fs::write( | |
| 435 | + | fixture.cache.root.join("aliases").join(SHA1), | |
| 436 | + | "../../outside", | |
| 437 | + | ) | |
| 438 | + | .unwrap(); | |
| 439 | + | assert!(fixture.cache.has(&hash("sha-1", SHA1)).is_err()); | |
| 440 | + | } | |
| 441 | + | } |
+132
-0
src-tauri/src/media/upload_slot.rs
#
| ... | ... | @@ -0,0 +1,132 @@ | |
| 1 | + | use super::{hash::MediaHash, store::MediaCache}; | |
| 2 | + | use sha1::Sha1; | |
| 3 | + | use sha2::{Digest, Sha256}; | |
| 4 | + | use std::io::{self, Write}; | |
| 5 | + | use tauri::http::header::HeaderValue; | |
| 6 | + | use tempfile::NamedTempFile; | |
| 7 | + | ||
| 8 | + | pub(super) struct UploadSlot { | |
| 9 | + | temporary: NamedTempFile, | |
| 10 | + | mime: HeaderValue, | |
| 11 | + | sha256: Sha256, | |
| 12 | + | sha1: Sha1, | |
| 13 | + | } | |
| 14 | + | ||
| 15 | + | impl UploadSlot { | |
| 16 | + | pub(super) fn new(mime: HeaderValue) -> io::Result<Self> { | |
| 17 | + | let temporary = NamedTempFile::new()?; | |
| 18 | + | Ok(Self { | |
| 19 | + | temporary, | |
| 20 | + | mime, | |
| 21 | + | sha256: Sha256::new(), | |
| 22 | + | sha1: Sha1::new(), | |
| 23 | + | }) | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | pub(super) fn write(&mut self, data: &[u8]) -> io::Result<()> { | |
| 27 | + | self.temporary.write_all(data)?; | |
| 28 | + | self.sha256.update(data); | |
| 29 | + | self.sha1.update(data); | |
| 30 | + | Ok(()) | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | pub(super) fn finish(self, cache: &MediaCache) -> io::Result<String> { | |
| 34 | + | let sha256 = MediaHash::Sha256(self.sha256.finalize().into()); | |
| 35 | + | let sha1 = MediaHash::Sha1(self.sha1.finalize().into()); | |
| 36 | + | let url = sha256.url(); | |
| 37 | + | cache.publish(self.temporary.into_temp_path(), &self.mime, sha256, &sha1)?; | |
| 38 | + | Ok(url) | |
| 39 | + | } | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | #[cfg(test)] | |
| 43 | + | mod tests { | |
| 44 | + | use super::*; | |
| 45 | + | use std::fs; | |
| 46 | + | ||
| 47 | + | const SHA256: &str = "b6acae78c76442a73a94762eb91711a59c642ac3cfdaad636a061bdb7fad6975"; | |
| 48 | + | const SHA1: &str = "daa728737f5b69b8f46dd7df156ee770e738759c"; | |
| 49 | + | const DATA: &[u8] = b"media bytes"; | |
| 50 | + | ||
| 51 | + | fn hash(algorithm: &str, hex: &str) -> MediaHash { | |
| 52 | + | MediaHash::from_hex(algorithm, hex).unwrap() | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | fn hash_url(algorithm: &str, hex: &str) -> String { | |
| 56 | + | hash(algorithm, hex).url() | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | #[test] | |
| 60 | + | fn upload_stores_plain_text_mime_and_returns_a_url() { | |
| 61 | + | let directory = tempfile::tempdir().unwrap(); | |
| 62 | + | let cache = MediaCache::new(directory.path().to_path_buf()).unwrap(); | |
| 63 | + | let mime = "text/plain; charset=utf-8"; | |
| 64 | + | let mut upload = UploadSlot::new(HeaderValue::from_static(mime)).unwrap(); | |
| 65 | + | upload.write(&DATA[..3]).unwrap(); | |
| 66 | + | upload.write(&DATA[3..]).unwrap(); | |
| 67 | + | let temporary = upload.temporary.path().to_path_buf(); | |
| 68 | + | let url = upload.finish(&cache).unwrap(); | |
| 69 | + | assert!(!temporary.exists()); | |
| 70 | + | assert_eq!(url, hash_url("sha-256", SHA256)); | |
| 71 | + | let response = cache.response(&hash("sha-256", SHA256), false).unwrap(); | |
| 72 | + | assert_eq!(response.headers()["content-type"], mime); | |
| 73 | + | assert_eq!(response.body(), DATA); | |
| 74 | + | assert_eq!( | |
| 75 | + | fs::read_to_string(directory.path().join("aliases").join(SHA1)).unwrap(), | |
| 76 | + | SHA256 | |
| 77 | + | ); | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | #[test] | |
| 81 | + | fn write_failure_cleans_up_temporary_file_on_drop() { | |
| 82 | + | let mut upload = UploadSlot::new(HeaderValue::from_static("image/png")).unwrap(); | |
| 83 | + | let temporary = upload.temporary.path().to_path_buf(); | |
| 84 | + | // Replace the writable handle with a read-only one to force a disk write error. | |
| 85 | + | let read_only = fs::File::open(upload.temporary.path()).unwrap(); | |
| 86 | + | *upload.temporary.as_file_mut() = read_only; | |
| 87 | + | assert!(upload.write(DATA).is_err()); | |
| 88 | + | drop(upload); | |
| 89 | + | assert!(!temporary.exists()); | |
| 90 | + | } | |
| 91 | + | #[test] | |
| 92 | + | fn publication_failures_clean_up_temporary_files() { | |
| 93 | + | for stage in ["files", "mime", "aliases"] { | |
| 94 | + | let directory = tempfile::tempdir().unwrap(); | |
| 95 | + | let cache = MediaCache::new(directory.path().to_path_buf()).unwrap(); | |
| 96 | + | let mut upload = UploadSlot::new(HeaderValue::from_static("image/png")).unwrap(); | |
| 97 | + | upload.write(DATA).unwrap(); | |
| 98 | + | let temporary = upload.temporary.path().to_path_buf(); | |
| 99 | + | match stage { | |
| 100 | + | // A missing parent prevents content persistence; directories at | |
| 101 | + | // the sidecar destinations prevent writing them. | |
| 102 | + | "files" => fs::remove_dir(directory.path().join("files")).unwrap(), | |
| 103 | + | "mime" => { | |
| 104 | + | fs::create_dir(directory.path().join("mime").join(format!("{SHA256}.mime"))) | |
| 105 | + | .unwrap() | |
| 106 | + | } | |
| 107 | + | _ => fs::create_dir(directory.path().join("aliases").join(SHA1)).unwrap(), | |
| 108 | + | } | |
| 109 | + | assert!(upload.finish(&cache).is_err(), "{stage}"); | |
| 110 | + | assert!(!temporary.exists(), "{stage}"); | |
| 111 | + | } | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | #[test] | |
| 115 | + | fn close_and_resource_drop_remove_partial_uploads() { | |
| 116 | + | for close in [true, false] { | |
| 117 | + | let mut upload = UploadSlot::new(HeaderValue::from_static("image/png")).unwrap(); | |
| 118 | + | upload.write(DATA).unwrap(); | |
| 119 | + | let temporary = upload.temporary.path().to_path_buf(); | |
| 120 | + | let mut resources = tauri::ResourceTable::default(); | |
| 121 | + | let rid = resources.add(crate::media::UploadResource { | |
| 122 | + | upload: std::sync::Mutex::new(upload), | |
| 123 | + | }); | |
| 124 | + | if close { | |
| 125 | + | resources.close(rid).unwrap(); | |
| 126 | + | assert!(!resources.has(rid)); | |
| 127 | + | } | |
| 128 | + | drop(resources); | |
| 129 | + | assert!(!temporary.exists()); | |
| 130 | + | } | |
| 131 | + | } | |
| 132 | + | } |
+1
-0
src-tauri/tauri.conf.json
#
+1
-1
src/App.tsx
#
| ... | ... | @@ -17,6 +17,7 @@ import { ios, webkit, href, useEffectEvent, showNotification } from "./util"; | |
| 17 | 17 | import { SettingsContext, SettingsData } from "./components/Settings"; | |
| 18 | 18 | import { Call } from "./components/Call"; | |
| 19 | 19 | import { LoginRequest } from "./components/LoginRequest"; | |
| 20 | + | import { mediaStore } from "./media-store"; | |
| 20 | 21 | ||
| 21 | 22 | Sentry.init({ | |
| 22 | 23 | dsn: "https://ec9ae5aef68e4035937d4c5943c4156c@app.glitchtip.com/7125", |
| ... | ... | @@ -33,7 +34,6 @@ Sentry.init({ | |
| 33 | 34 | replaysOnErrorSampleRate: 1.0, | |
| 34 | 35 | }); | |
| 35 | 36 | ||
| 36 | - | const mediaStore = await borogove.persistence.MediaStoreCache("snikket"); | |
| 37 | 37 | const idbp = await borogove.persistence.IDB("snikket", mediaStore, null, stem); | |
| 38 | 38 | let persistence = null; | |
| 39 | 39 | if (typeof navigator.storage?.getDirectory === "function" && (webkit || ios)) { |
+25
-0
src/components/MessageAttachments.test.tsx
#
| ... | ... | @@ -70,6 +70,31 @@ function setup( | |
| 70 | 70 | afterEach(cleanup); | |
| 71 | 71 | ||
| 72 | 72 | describe("MessageAttachments", () => { | |
| 73 | + | it.each(["image/png", "application/pdf"])( | |
| 74 | + | "passes the original filename on cached %s links", | |
| 75 | + | (mime) => { | |
| 76 | + | const uri = "/.well-known/ni/sha-256/abc"; | |
| 77 | + | const name = "résumé + notes & stuff #1.pdf"; | |
| 78 | + | const view = setup([ | |
| 79 | + | { attachment: { ...attachment(name, uri), mime }, outcomes: [] }, | |
| 80 | + | ]); | |
| 81 | + | const url = new URL( | |
| 82 | + | view.getByRole("link").getAttribute("href")!, | |
| 83 | + | location.href, | |
| 84 | + | ); | |
| 85 | + | expect(url.pathname).toBe(uri); | |
| 86 | + | expect(url.searchParams.get("name")).toBe(name); | |
| 87 | + | if (mime === "image/png") | |
| 88 | + | expect(view.getByRole("img").getAttribute("src")).toBe(uri); | |
| 89 | + | }, | |
| 90 | + | ); | |
| 91 | + | ||
| 92 | + | it("keeps unnamed cached links unchanged", () => { | |
| 93 | + | const uri = "/.well-known/ni/sha-256/abc"; | |
| 94 | + | const view = setup([{ attachment: attachment("", uri), outcomes: [] }]); | |
| 95 | + | expect(view.getByRole("link").getAttribute("href")).toBe(uri); | |
| 96 | + | }); | |
| 97 | + | ||
| 73 | 98 | it("fetches a whole batch, hides controls during fetching, and reveals content together", async () => { | |
| 74 | 99 | const first = attachment("first", null); | |
| 75 | 100 | const second = attachment("second", null); |
+7
-3
src/components/MessageAttachments.tsx
#
| ... | ... | @@ -120,11 +120,15 @@ const AttachmentContent = ({ | |
| 120 | 120 | fetchedAttachment: FetchedAttachment; | |
| 121 | 121 | }) => { | |
| 122 | 122 | const { uri, embeddable } = fetchedAttachment; | |
| 123 | - | if (!embeddable) return <a href={uri}>{attachment.name || "attachment"}</a>; | |
| 123 | + | const href = | |
| 124 | + | attachment.name && uri.startsWith("/.well-known/ni/") | |
| 125 | + | ? `${uri}?name=${encodeURIComponent(attachment.name)}` | |
| 126 | + | : uri; | |
| 127 | + | if (!embeddable) return <a href={href}>{attachment.name || "attachment"}</a>; | |
| 124 | 128 | ||
| 125 | 129 | if (attachment.mime?.indexOf("image/") === 0) { | |
| 126 | 130 | return ( | |
| 127 | - | <a href={uri}> | |
| 131 | + | <a href={href}> | |
| 128 | 132 | <img | |
| 129 | 133 | alt={attachment.name || "attachment"} | |
| 130 | 134 | crossOrigin="anonymous" |
+116
-0
src/media-store-tauri.test.ts
#
| ... | ... | @@ -0,0 +1,116 @@ | |
| 1 | + | import { beforeEach, expect, it, vi } from "vitest"; | |
| 2 | + | import { Hash } from "borogove"; | |
| 3 | + | import { MediaStoreTauri } from "./media-store-tauri"; | |
| 4 | + | ||
| 5 | + | const native = vi.hoisted(() => ({ invoke: vi.fn() })); | |
| 6 | + | vi.mock("@tauri-apps/api/core", () => ({ | |
| 7 | + | ...native, | |
| 8 | + | Resource: class { | |
| 9 | + | constructor(readonly rid: number) {} | |
| 10 | + | close() { | |
| 11 | + | return native.invoke("plugin:resources|close", { rid: this.rid }); | |
| 12 | + | } | |
| 13 | + | }, | |
| 14 | + | })); | |
| 15 | + | ||
| 16 | + | const data = new TextEncoder().encode("media bytes"); | |
| 17 | + | const sha1 = "daa728737f5b69b8f46dd7df156ee770e738759c"; | |
| 18 | + | const sha256 = | |
| 19 | + | "b6acae78c76442a73a94762eb91711a59c642ac3cfdaad636a061bdb7fad6975"; | |
| 20 | + | const url = "/.well-known/ni/sha-256/native-result"; | |
| 21 | + | const stream = (chunks: Uint8Array[] = [data]) => | |
| 22 | + | new ReadableStream<Uint8Array>({ | |
| 23 | + | start(controller) { | |
| 24 | + | for (const chunk of chunks) controller.enqueue(chunk); | |
| 25 | + | controller.close(); | |
| 26 | + | }, | |
| 27 | + | }); | |
| 28 | + | ||
| 29 | + | beforeEach(() => { | |
| 30 | + | vi.resetAllMocks(); | |
| 31 | + | native.invoke.mockImplementation(async (command) => { | |
| 32 | + | if (command === "begin_media") return 42; | |
| 33 | + | if (command === "finish_media") return url; | |
| 34 | + | if (command === "has_media") return null; | |
| 35 | + | }); | |
| 36 | + | }); | |
| 37 | + | ||
| 38 | + | it("sends chunk views in order and returns the native URL", async () => { | |
| 39 | + | const store = new MediaStoreTauri(); | |
| 40 | + | const padded = new Uint8Array(data.length + 4); | |
| 41 | + | padded.set(data, 2); | |
| 42 | + | const chunks = [padded.subarray(2, 5), padded.subarray(5, data.length + 2)]; | |
| 43 | + | const source = stream(chunks); | |
| 44 | + | expect(await store.storeMedia("image/png", source)).toBe(url); | |
| 45 | + | expect(native.invoke.mock.calls).toEqual([ | |
| 46 | + | ["begin_media", { mime: "image/png" }], | |
| 47 | + | ["write_media", chunks[0], { headers: { "X-Media-Id": "42" } }], | |
| 48 | + | ["write_media", chunks[1], { headers: { "X-Media-Id": "42" } }], | |
| 49 | + | ["finish_media", { rid: 42 }], | |
| 50 | + | ]); | |
| 51 | + | expect(source.locked).toBe(false); | |
| 52 | + | }); | |
| 53 | + | ||
| 54 | + | it("closes the native resource after a write fails", async () => { | |
| 55 | + | const store = new MediaStoreTauri(); | |
| 56 | + | const error = new Error("disk full"); | |
| 57 | + | const source = stream(); | |
| 58 | + | native.invoke.mockResolvedValueOnce(42).mockRejectedValueOnce(error); | |
| 59 | + | await expect(store.storeMedia("image/png", source)).rejects.toBe(error); | |
| 60 | + | expect(source.locked).toBe(false); | |
| 61 | + | expect(native.invoke).toHaveBeenLastCalledWith("plugin:resources|close", { | |
| 62 | + | rid: 42, | |
| 63 | + | }); | |
| 64 | + | }); | |
| 65 | + | ||
| 66 | + | it("does not close an already consumed resource when finishing fails", async () => { | |
| 67 | + | const store = new MediaStoreTauri(); | |
| 68 | + | const error = new Error("publication failed"); | |
| 69 | + | native.invoke.mockImplementation(async (command) => { | |
| 70 | + | if (command === "begin_media") return 42; | |
| 71 | + | if (command === "finish_media") throw error; | |
| 72 | + | if (command === "plugin:resources|close") | |
| 73 | + | throw new Error("bad resource ID"); | |
| 74 | + | }); | |
| 75 | + | await expect(store.storeMedia("image/png", stream())).rejects.toBe(error); | |
| 76 | + | expect(native.invoke).toHaveBeenLastCalledWith("finish_media", { rid: 42 }); | |
| 77 | + | }); | |
| 78 | + | ||
| 79 | + | it("closes the native resource when the source fails", async () => { | |
| 80 | + | const store = new MediaStoreTauri(); | |
| 81 | + | const error = new Error("download failed"); | |
| 82 | + | const source = new ReadableStream<Uint8Array>({ | |
| 83 | + | start(controller) { | |
| 84 | + | controller.error(error); | |
| 85 | + | }, | |
| 86 | + | }); | |
| 87 | + | await expect(store.storeMedia("image/png", source)).rejects.toBe(error); | |
| 88 | + | expect(native.invoke.mock.calls).toEqual([ | |
| 89 | + | ["begin_media", { mime: "image/png" }], | |
| 90 | + | ["plugin:resources|close", { rid: 42 }], | |
| 91 | + | ]); | |
| 92 | + | }); | |
| 93 | + | ||
| 94 | + | it.each(["sha-1", "sha-256"])( | |
| 95 | + | "delegates %s lookup and removal to Rust", | |
| 96 | + | async (algorithm) => { | |
| 97 | + | const store = new MediaStoreTauri(); | |
| 98 | + | const hex = algorithm === "sha-1" ? sha1 : sha256; | |
| 99 | + | const hash = Hash.fromHex(algorithm, hex)!; | |
| 100 | + | native.invoke.mockResolvedValueOnce(null).mockResolvedValueOnce(url); | |
| 101 | + | expect(await store.hasMedia(hash)).toBeNull(); | |
| 102 | + | expect(await store.hasMedia(hash)).toBe(url); | |
| 103 | + | expect(native.invoke).toHaveBeenLastCalledWith("has_media", { | |
| 104 | + | algorithm, | |
| 105 | + | hex, | |
| 106 | + | }); | |
| 107 | + | const bytes = Uint8Array.from(hex.match(/../g)!, (byte) => | |
| 108 | + | parseInt(byte, 16), | |
| 109 | + | ).buffer; | |
| 110 | + | expect(await store.removeMedia(algorithm, bytes)).toBe(true); | |
| 111 | + | expect(native.invoke).toHaveBeenLastCalledWith("remove_media", { | |
| 112 | + | algorithm, | |
| 113 | + | hex, | |
| 114 | + | }); | |
| 115 | + | }, | |
| 116 | + | ); |
+44
-0
src/media-store-tauri.ts
#
| ... | ... | @@ -0,0 +1,44 @@ | |
| 1 | + | import { Hash, type persistence } from "borogove"; | |
| 2 | + | import { invoke, Resource } from "@tauri-apps/api/core"; | |
| 3 | + | ||
| 4 | + | export class MediaStoreTauri implements persistence.MediaStore { | |
| 5 | + | // Instead of using KV we store data on the filesystem because the rust | |
| 6 | + | // implementation doesn't have access to this KV store | |
| 7 | + | setKV(value: persistence.KeyValueStore) { | |
| 8 | + | void value; | |
| 9 | + | } | |
| 10 | + | ||
| 11 | + | async storeMedia(mime: string, source: ReadableStream<Uint8Array>) { | |
| 12 | + | const upload = new Resource(await invoke<number>("begin_media", { mime })); | |
| 13 | + | try { | |
| 14 | + | await source.pipeTo( | |
| 15 | + | new WritableStream<Uint8Array>({ | |
| 16 | + | write(chunk) { | |
| 17 | + | return invoke("write_media", chunk, { | |
| 18 | + | headers: { "X-Media-Id": String(upload.rid) }, | |
| 19 | + | }); | |
| 20 | + | }, | |
| 21 | + | }), | |
| 22 | + | ); | |
| 23 | + | } catch (error) { | |
| 24 | + | await upload.close(); | |
| 25 | + | throw error; | |
| 26 | + | } | |
| 27 | + | return await invoke<string>("finish_media", { rid: upload.rid }); | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | hasMedia(hash: Hash) { | |
| 31 | + | return invoke<string | null>("has_media", { | |
| 32 | + | algorithm: hash.algorithm, | |
| 33 | + | hex: hash.toHex(), | |
| 34 | + | }); | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | async removeMedia(algorithm: string, bytes: ArrayBuffer) { | |
| 38 | + | await invoke("remove_media", { | |
| 39 | + | algorithm, | |
| 40 | + | hex: new Hash(algorithm, bytes).toHex(), | |
| 41 | + | }); | |
| 42 | + | return true; | |
| 43 | + | } | |
| 44 | + | } |