feat: extract downloaded archives in place behind a feature flag (#3009)
* feat: extract downloaded archives in place behind a feature flag Adds in-place extraction through hatchzip-core to the native addon. The archive is cut back as its files come out, so extracting a download only needs room for the game, not for the game and the archive side by side. Only top-level archives that would be deleted after extraction are consumed. Archives hatchzip refuses (solid RAR, files already in the way, too little space) are left untouched and go to 7-Zip. An archive hatchzip has started is never handed to 7-Zip and resumes from its journal. The whole flow sits behind the IN_PLACE_ARCHIVE_EXTRACTION flag. With it off, or with /features unreachable, archives go straight to 7-Zip and the usual delete-or-prompt step runs afterwards. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014J6EZ4VGuJ6sWKrXDd67nW * chore(eslint): mark the config as root Git worktrees under .claude/worktrees sit inside the main checkout. ESLint 8 kept walking up into the outer .eslintrc.cjs, found eslint-plugin-prettier in both node_modules folders, and crashed before linting anything, which also broke the pre-push hook. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014J6EZ4VGuJ6sWKrXDd67nW * chore: drop in-place extraction tests and RAR fixtures Removes the Rust and JS tests for in-place extraction, the RAR 5 fixtures they used, the zip dev-dependency, and the *.rar binary attribute that only existed for those fixtures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014J6EZ4VGuJ6sWKrXDd67nW * fix: keep extraction retryable after an in-place extraction An archive extracted in place is gone, but the download still pointed at it while nested archives were extracted. If a nested extraction failed, retrying reported the archive as missing. The extracted folder is now saved as soon as the archive is consumed, so a retry starts from it. Also names the progress update step count in the native addon. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014J6EZ4VGuJ6sWKrXDd67nW * fix: keep nested archives intact when retrying after in-place extraction The previous fix pointed the download at the extracted folder, so a retry ran the directory extraction without the outer archive and could delete nested archives the first attempt kept. The download now keeps naming the archive and is marked extractedInPlace once the archive is consumed. A retry skips the outer archive, extracts the nested ones with the outer archive as context, and clears the mark. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014J6EZ4VGuJ6sWKrXDd67nW * fix: resume in-place extractions only from Hydra's own record A progress file next to an archive can arrive with a download, so it is no longer trusted on its own. Hydra now records each archive on the download before hatchzip may consume it, and only those archives are resumed. A progress file Hydra did not create leaves the archive to 7-Zip. Resuming no longer depends on the feature flag. Only a new in-place run asks the API, once per extraction, so an offline retry finishes a half-consumed archive instead of handing it to 7-Zip. If the addon cannot load for a new run, 7-Zip extracts instead, and a panic in the addon still reports whether the archive was touched. The record also covers a run that stopped after the archive was gone: the retry lets hatchzip finish the links and progress files, or report the archive done, instead of failing with a missing file. Folder downloads pick up such archives too. hatchzip-core moves to 0.1.8, which checks that a journal only names the archive's own volumes and links inside the output folder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014J6EZ4VGuJ6sWKrXDd67nW * fix: record an in-place start only for runs hatchzip can begin The start record was saved before hatchzip rejected a journal it did not create. If 7-Zip then failed, the record survived and the retry resumed from the journal that came with the download. A new run now checks for a journal before saving the record and goes to 7-Zip if one is there. The record is also dropped whenever hatchzip reports the archive untouched, or the addon could not run, so it only stays for a run hatchzip actually started. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014J6EZ4VGuJ6sWKrXDd67nW * feat: delete archive files after extraction by default The "delete archive files after extraction" checkbox now starts checked for anyone who has not set the preference, in the download modal and settings of both the desktop UI and Big Picture. The main process falls back to the same default. A preference someone already saved keeps its value. In-place extraction only runs for archives that are deleted afterwards, so this lets new downloads use it without a settings change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014J6EZ4VGuJ6sWKrXDd67nW --------- Co-authored-by: Chubby Granny Chaser <hydralauncher@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
dcb5bcfaff
commit
57441a2885
15 changed files with 1079 additions and 74 deletions
|
|
@ -1,4 +1,7 @@
|
|||
module.exports = {
|
||||
// Worktrees live inside the main checkout; without this, ESLint also loads
|
||||
// the outer config and finds two copies of every plugin.
|
||||
root: true,
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
|
|
|
|||
479
native/hydra-native/Cargo.lock
generated
479
native/hydra-native/Cargo.lock
generated
|
|
@ -8,6 +8,17 @@ version = "2.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
"cpubits",
|
||||
"cpufeatures 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
|
|
@ -19,9 +30,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
|
|
@ -69,7 +80,7 @@ dependencies = [
|
|||
"arrayvec",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"constant_time_eq",
|
||||
"constant_time_eq 0.4.2",
|
||||
"cpufeatures 0.3.0",
|
||||
]
|
||||
|
||||
|
|
@ -82,6 +93,25 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-padding"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.13.0"
|
||||
|
|
@ -94,9 +124,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.2"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
|
|
@ -116,6 +146,24 @@ version = "1.12.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
|
||||
[[package]]
|
||||
name = "bzip2"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c"
|
||||
dependencies = [
|
||||
"libbz2-rs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cbc"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.4.0"
|
||||
|
|
@ -123,6 +171,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
|
|
@ -132,6 +182,28 @@ version = "1.0.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c"
|
||||
dependencies = [
|
||||
"crypto-common 0.2.2",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cmov"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
|
|
@ -148,12 +220,27 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9eb6feb175df65d0a3ccd3e73484e940d0ac664e8034f0519fd49a93db3071d9"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.11.0"
|
||||
|
|
@ -189,6 +276,12 @@ version = "0.8.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "cpubits"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
|
|
@ -209,9 +302,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
version = "1.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
checksum = "0ba106e844045ddc02c20768ac4b0b7cb45f2cc2ecbd833ac9f5dacb183c2055"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
|
@ -251,6 +344,15 @@ dependencies = [
|
|||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.6.3"
|
||||
|
|
@ -267,6 +369,21 @@ version = "0.0.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
|
||||
|
||||
[[package]]
|
||||
name = "ctutils"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38d28fb6aa7dd680b3ffd1a3e64e115c177bdba63c0ae7f9cd48129ff05c4fa0"
|
||||
dependencies = [
|
||||
"cmov",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deflate64"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.8"
|
||||
|
|
@ -282,8 +399,21 @@ version = "0.10.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
"block-buffer 0.10.4",
|
||||
"crypto-common 0.1.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||
dependencies = [
|
||||
"block-buffer 0.12.1",
|
||||
"const-oid",
|
||||
"crypto-common 0.2.2",
|
||||
"ctutils",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -382,6 +512,7 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
|||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
"zlib-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -405,6 +536,16 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs4"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e72ed92b67c146290f88e9c89d60ca163ea417a446f61ffd7b72df3e7f1dfd5"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.32"
|
||||
|
|
@ -531,10 +672,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -600,12 +743,46 @@ version = "0.17.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "hatchzip-core"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5a32a608e9965b27dcecfe903335d8efd0fe269f4dd79678ed658dd187a18b2"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"anyhow",
|
||||
"cbc",
|
||||
"crc32fast",
|
||||
"fs4",
|
||||
"libc",
|
||||
"libz-rs-sys",
|
||||
"pbkdf2",
|
||||
"rayon",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sevenz-rust2",
|
||||
"sha2 0.11.1",
|
||||
"tar",
|
||||
"unrar-ng-sys",
|
||||
"windows-sys 0.61.2",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
|
||||
dependencies = [
|
||||
"digest 0.11.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.5.0"
|
||||
|
|
@ -645,6 +822,15 @@ version = "1.10.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "hybrid-array"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hydra-native"
|
||||
version = "0.1.0"
|
||||
|
|
@ -654,6 +840,7 @@ dependencies = [
|
|||
"filetime",
|
||||
"globetter",
|
||||
"globset",
|
||||
"hatchzip-core",
|
||||
"image",
|
||||
"indexmap",
|
||||
"known-folders",
|
||||
|
|
@ -667,7 +854,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml_ng",
|
||||
"sha2",
|
||||
"sha2 0.10.9",
|
||||
"sysinfo",
|
||||
"tempfile",
|
||||
"time",
|
||||
|
|
@ -889,6 +1076,16 @@ dependencies = [
|
|||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b5cf4372d022092674a83b8febf5865f7a573cc0c5baec5e49c879b6a20978f"
|
||||
dependencies = [
|
||||
"block-padding",
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.12.0"
|
||||
|
|
@ -950,6 +1147,16 @@ dependencies = [
|
|||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
|
||||
dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.91"
|
||||
|
|
@ -976,10 +1183,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.183"
|
||||
name = "libbz2-rs-sys"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.191"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
|
|
@ -991,6 +1204,15 @@ dependencies = [
|
|||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libz-rs-sys"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "434324ee61a8e4a05dd3fdf03d51c073ea7fdaf5a35b2ca33c4ca9f3f37def4a"
|
||||
dependencies = [
|
||||
"zlib-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
|
|
@ -1005,9 +1227,18 @@ checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
|||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "lzma-rust2"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1c46f3fcd2e888afdb01a07efc071cb21300e07c68d32c3e8b01b5c8cb1fcaa"
|
||||
dependencies = [
|
||||
"sha2 0.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
|
|
@ -1120,6 +1351,18 @@ dependencies = [
|
|||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.31.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nohash-hasher"
|
||||
version = "0.2.0"
|
||||
|
|
@ -1181,6 +1424,16 @@ version = "0.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629"
|
||||
dependencies = [
|
||||
"digest 0.11.3",
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
|
|
@ -1193,6 +1446,12 @@ version = "0.2.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.18.1"
|
||||
|
|
@ -1221,6 +1480,12 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppmd-rust"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd4acc8d264ea385db64ccfdcdfd5909ee9eda0975666723d59f3bfec80b3611"
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
|
|
@ -1269,9 +1534,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
|||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.11.0"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
|
||||
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
|
|
@ -1521,9 +1786,9 @@ checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
|
|
@ -1531,29 +1796,29 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
version = "1.0.154"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
|
|
@ -1575,6 +1840,37 @@ dependencies = [
|
|||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sevenz-rust2"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f0c68c0e8289bfd09e49ef433e8de02c7c464afc1d7f5a275c99749a880e301"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"bzip2",
|
||||
"cbc",
|
||||
"crc32fast",
|
||||
"flate2",
|
||||
"getrandom 0.4.2",
|
||||
"js-sys",
|
||||
"lzma-rust2",
|
||||
"ppmd-rust",
|
||||
"sha2 0.11.1",
|
||||
"wasm-bindgen",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
"digest 0.11.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
|
|
@ -1583,7 +1879,18 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
|||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
"digest 0.11.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1594,9 +1901,9 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
|||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.8"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
||||
|
||||
[[package]]
|
||||
name = "simd_cesu8"
|
||||
|
|
@ -1725,6 +2032,17 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
|
|
@ -1765,6 +2083,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"js-sys",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde_core",
|
||||
|
|
@ -1933,6 +2252,12 @@ version = "0.2.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "typed-path"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ce898823d8cd0f028662d2f349694b1af0a5cc9be83f1d213a00d3147445e1e"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
|
|
@ -1972,6 +2297,17 @@ version = "0.2.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "unrar-ng-sys"
|
||||
version = "0.7.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72678d22d4d6aa9f0b905f5d6d5cd3e462c55631b526297d06fb1663532daa76"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
|
|
@ -2572,6 +2908,16 @@ version = "0.13.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.3"
|
||||
|
|
@ -2655,12 +3001,87 @@ dependencies = [
|
|||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "9.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2886b00003a72c4ccee3f42cfc34908d33b2508a74d827dc2a4244706475c704"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"bzip2",
|
||||
"constant_time_eq 0.6.1",
|
||||
"crc32fast",
|
||||
"deflate64",
|
||||
"flate2",
|
||||
"getrandom 0.4.2",
|
||||
"hmac",
|
||||
"indexmap",
|
||||
"lzma-rust2",
|
||||
"memchr",
|
||||
"nix",
|
||||
"pbkdf2",
|
||||
"ppmd-rust",
|
||||
"sha1",
|
||||
"time",
|
||||
"typed-path",
|
||||
"windows-sys 0.61.2",
|
||||
"zeroize",
|
||||
"zopfli",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aaf7fc5d30c28483d93805c4a5e12b05bbb52407fa67c5f8bd552374cd01fb11"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"crc32fast",
|
||||
"log",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db3ae9bf90af6e40197bef767eeefc46ad6397420fe873830bd85e5fbbfb3c70"
|
||||
dependencies = [
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "8.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ded520f16b29e1a9b771c722221b2b0a0398c2316d9bb3c42b458a5414d7434b"
|
||||
dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.1.2+zstd.1.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "481ada9c543b2323cbb7bbdbb5249177fbf0188ed959ed2c15e4b22421c84ed3"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.5.1"
|
||||
|
|
|
|||
|
|
@ -12,12 +12,16 @@ blake3 = "=1.8.5"
|
|||
filetime = "0.2"
|
||||
globetter = "=0.2.0"
|
||||
globset = "=0.4.15"
|
||||
# In-place archive extraction. The `rar` feature compiles UnRAR's C++ source,
|
||||
# which is under its own license (LICENSE-UNRAR, shipped with the addon):
|
||||
# free for extracting RAR archives, not for building a RAR compressor.
|
||||
hatchzip-core = { version = "=0.1.8", features = ["rar"] }
|
||||
image = { version = "0.25.8", default-features = false, features = ["gif", "jpeg", "png", "webp"] }
|
||||
indexmap = { version = "2.14", features = ["serde"] }
|
||||
mime_guess = "2.0.5"
|
||||
napi = { version = "3.5.2", default-features = false, features = ["napi8", "tokio_rt"] }
|
||||
napi-derive = "3.3.2"
|
||||
rayon = "=1.11.0"
|
||||
rayon = "=1.12.0"
|
||||
reqwest = { version = "0.13", default-features = false, features = ["stream", "charset", "http2", "system-proxy", "rustls-no-provider"] }
|
||||
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
|
|
|
|||
42
native/hydra-native/LICENSE-UNRAR
Normal file
42
native/hydra-native/LICENSE-UNRAR
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
****** ***** ****** UnRAR - free utility for RAR archives
|
||||
** ** ** ** ** ** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
****** ******* ****** License for use and distribution of
|
||||
** ** ** ** ** ** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
** ** ** ** ** ** FREE portable version
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The source code of UnRAR utility is freeware. This means:
|
||||
|
||||
1. All copyrights to RAR and the utility UnRAR are exclusively
|
||||
owned by the author - Alexander Roshal.
|
||||
|
||||
2. UnRAR source code may be used in any software to handle
|
||||
RAR archives without limitations free of charge, but cannot be
|
||||
used to develop RAR (WinRAR) compatible archiver and to
|
||||
re-create RAR compression algorithm, which is proprietary.
|
||||
Distribution of modified UnRAR source code in separate form
|
||||
or as a part of other software is permitted, provided that
|
||||
full text of this paragraph, starting from "UnRAR source code"
|
||||
words, is included in license, or in documentation if license
|
||||
is not available, and in source code comments of resulting package.
|
||||
|
||||
3. The UnRAR utility may be freely distributed. It is allowed
|
||||
to distribute UnRAR inside of other software packages.
|
||||
|
||||
4. THE RAR ARCHIVER AND THE UnRAR UTILITY ARE DISTRIBUTED "AS IS".
|
||||
NO WARRANTY OF ANY KIND IS EXPRESSED OR IMPLIED. YOU USE AT
|
||||
YOUR OWN RISK. THE AUTHOR WILL NOT BE LIABLE FOR DATA LOSS,
|
||||
DAMAGES, LOSS OF PROFITS OR ANY OTHER KIND OF LOSS WHILE USING
|
||||
OR MISUSING THIS SOFTWARE.
|
||||
|
||||
5. Installing and using the UnRAR utility signifies acceptance of
|
||||
these terms and conditions of the license.
|
||||
|
||||
6. If you don't agree with terms of the license you must remove
|
||||
UnRAR files from your storage devices and cease to use the
|
||||
utility.
|
||||
|
||||
Thank you for your interest in RAR and UnRAR.
|
||||
|
||||
|
||||
Alexander L. Roshal
|
||||
251
native/hydra-native/src/in_place_extraction.rs
Normal file
251
native/hydra-native/src/in_place_extraction.rs
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
//! In-place archive extraction through hatchzip-core. The archive is cut back
|
||||
//! as its files come out, so extracting a download needs room for the output
|
||||
//! only, not for the output and the archive side by side.
|
||||
//!
|
||||
//! Anything that stops a fresh extraction before its journal is written
|
||||
//! leaves the archive untouched and reports `unavailable`, so the caller can
|
||||
//! fall back to 7-Zip. Once the journal exists the archive is partly consumed:
|
||||
//! errors report `failed`, and only another call (which resumes) can finish it.
|
||||
//!
|
||||
//! A journal lies next to the archive, where a download could have brought
|
||||
//! one, so it is only followed when Hydra says it started that extraction.
|
||||
|
||||
use std::panic::{self, AssertUnwindSafe};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use hatchzip_core::engine::{self, Outcome, Progress, Run};
|
||||
use hatchzip_core::session::{self, State};
|
||||
use napi::bindgen_prelude::Error;
|
||||
use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode};
|
||||
use napi::Status;
|
||||
use napi_derive::napi;
|
||||
|
||||
/// Free space the extraction never eats into, so the rest of the system keeps
|
||||
/// working while a large archive unpacks.
|
||||
const KEEP_FREE_BYTES: u64 = 1 << 30;
|
||||
|
||||
/// Most progress updates one extraction sends across the bridge.
|
||||
const PROGRESS_UPDATE_STEPS: u64 = 1000;
|
||||
|
||||
// Hydra never pauses an extraction; a quit mid-way is resumed from the journal.
|
||||
static STOP: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
// UnRAR keeps state in globals and is not safe to drive from several threads,
|
||||
// and two games can finish downloading at once, so extractions take turns.
|
||||
static ONE_AT_A_TIME: Mutex<()> = Mutex::new(());
|
||||
|
||||
#[napi(object)]
|
||||
pub struct InPlaceExtractionOptions {
|
||||
pub archive_path: String,
|
||||
pub output_path: String,
|
||||
/// Tried in order when the archive is encrypted.
|
||||
pub passwords: Vec<String>,
|
||||
/// Allows starting a new in-place extraction.
|
||||
pub allow_fresh: bool,
|
||||
/// Hydra recorded starting an in-place extraction of this archive, so a
|
||||
/// journal next to it is Hydra's own and what is left gets finished.
|
||||
pub resume: bool,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct InPlaceExtractionResult {
|
||||
/// `finished`, `unavailable` (archive untouched) or `failed` (archive
|
||||
/// partly consumed, progress saved).
|
||||
pub status: String,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
impl InPlaceExtractionResult {
|
||||
fn finished() -> Self {
|
||||
Self {
|
||||
status: "finished".into(),
|
||||
message: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn unavailable(message: impl ToString) -> Self {
|
||||
Self {
|
||||
status: "unavailable".into(),
|
||||
message: Some(message.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn failed(message: impl ToString) -> Self {
|
||||
Self {
|
||||
status: "failed".into(),
|
||||
message: Some(message.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether an in-place extraction journal lies next to the archive.
|
||||
#[napi]
|
||||
pub fn in_place_journal_exists(archive_path: String) -> bool {
|
||||
journal_exists(Path::new(&archive_path))
|
||||
}
|
||||
|
||||
fn journal_exists(archive_path: &Path) -> bool {
|
||||
session::locate(archive_path).is_ok_and(|(_, paths)| paths.exists())
|
||||
}
|
||||
|
||||
/// `on_progress` receives the extracted fraction, from 0 to 1.
|
||||
#[napi]
|
||||
pub async fn extract_archive_in_place(
|
||||
options: InPlaceExtractionOptions,
|
||||
on_progress: ThreadsafeFunction<f64, (), f64, Status, false>,
|
||||
) -> napi::Result<InPlaceExtractionResult> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let archive_path = PathBuf::from(&options.archive_path);
|
||||
let on_progress = move |fraction| {
|
||||
on_progress.call(fraction, ThreadsafeFunctionCallMode::NonBlocking);
|
||||
};
|
||||
// A panic still says whether the archive was touched, so the caller
|
||||
// never hands a partly consumed one to 7-Zip.
|
||||
panic::catch_unwind(AssertUnwindSafe(|| extract(options, on_progress))).unwrap_or_else(
|
||||
|_| {
|
||||
if journal_exists(&archive_path) {
|
||||
InPlaceExtractionResult::failed("in-place extraction crashed")
|
||||
} else {
|
||||
InPlaceExtractionResult::unavailable("in-place extraction crashed")
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
.await
|
||||
.map_err(|err| Error::from_reason(err.to_string()))
|
||||
}
|
||||
|
||||
fn extract(
|
||||
options: InPlaceExtractionOptions,
|
||||
on_progress: impl Fn(f64) + Send + Sync + 'static,
|
||||
) -> InPlaceExtractionResult {
|
||||
let (primary, paths) = match session::locate(Path::new(&options.archive_path)) {
|
||||
Ok(located) => located,
|
||||
Err(err) => return InPlaceExtractionResult::unavailable(format!("{err:#}")),
|
||||
};
|
||||
if paths.exists() && !options.resume {
|
||||
return InPlaceExtractionResult::unavailable(format!(
|
||||
"{} was not created by Hydra",
|
||||
paths.journal.display()
|
||||
));
|
||||
}
|
||||
if !paths.exists() {
|
||||
// Hydra started this run, and both the archive and its journal are
|
||||
// gone, so every file is out.
|
||||
if options.resume && !primary.exists() {
|
||||
return InPlaceExtractionResult::finished();
|
||||
}
|
||||
if !options.allow_fresh {
|
||||
return InPlaceExtractionResult::unavailable("in-place extraction was not requested");
|
||||
}
|
||||
}
|
||||
let _turn = ONE_AT_A_TIME
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
// Before the journal exists nothing has been touched, so any error leaves
|
||||
// the archive whole for another extractor.
|
||||
let not_started = |message: String| {
|
||||
if paths.exists() {
|
||||
InPlaceExtractionResult::failed(message)
|
||||
} else {
|
||||
InPlaceExtractionResult::unavailable(message)
|
||||
}
|
||||
};
|
||||
|
||||
let mut passwords = options.passwords.into_iter();
|
||||
let opened = session::open_encrypted(
|
||||
&primary,
|
||||
&paths,
|
||||
Some(PathBuf::from(&options.output_path)),
|
||||
true,
|
||||
None,
|
||||
&Arc::default(),
|
||||
|_| passwords.next(),
|
||||
);
|
||||
let state = match opened {
|
||||
Ok((state, _)) => state,
|
||||
Err(err) => return not_started(format!("{err:#}")),
|
||||
};
|
||||
|
||||
let (job, journal) = match state {
|
||||
State::Orphan(journal) => {
|
||||
return match engine::finish_orphaned(&journal, &paths) {
|
||||
Ok(()) => InPlaceExtractionResult::finished(),
|
||||
Err(err) => InPlaceExtractionResult::failed(format!("{err:#}")),
|
||||
};
|
||||
}
|
||||
State::Resume(job, journal) => {
|
||||
let plan =
|
||||
match session::plan(&job, &journal.output, true, KEEP_FREE_BYTES, journal.keep) {
|
||||
Ok(plan) => plan,
|
||||
Err(err) => return InPlaceExtractionResult::failed(format!("{err:#}")),
|
||||
};
|
||||
if !plan.fits {
|
||||
return InPlaceExtractionResult::failed(format!(
|
||||
"not enough disk space to resume: {} required, {} available",
|
||||
engine::human(plan.needed + plan.keep_free),
|
||||
engine::human(plan.free)
|
||||
));
|
||||
}
|
||||
if let Err(err) = session::clear_uncommitted(&job, &journal.output) {
|
||||
return InPlaceExtractionResult::failed(format!("{err:#}"));
|
||||
}
|
||||
(job, journal)
|
||||
}
|
||||
State::Fresh(job, output) => {
|
||||
if let Some(reason) = job.in_place_refusal() {
|
||||
return InPlaceExtractionResult::unavailable(reason);
|
||||
}
|
||||
let plan = match session::plan(&job, &output, false, KEEP_FREE_BYTES, false) {
|
||||
Ok(plan) => plan,
|
||||
Err(err) => return InPlaceExtractionResult::unavailable(format!("{err:#}")),
|
||||
};
|
||||
if !plan.fits {
|
||||
return InPlaceExtractionResult::unavailable(format!(
|
||||
"not enough disk space: {} required, {} available",
|
||||
engine::human(plan.needed + plan.keep_free),
|
||||
engine::human(plan.free)
|
||||
));
|
||||
}
|
||||
match session::begin(&job, &paths, output, false, None) {
|
||||
Ok(journal) => (job, journal),
|
||||
Err(err) => return not_started(format!("{err:#}")),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let total = journal.total_size.max(1);
|
||||
let extracted = Arc::new(AtomicU64::new(
|
||||
journal.total_size.saturating_sub(job.remaining_size()),
|
||||
));
|
||||
let reported_steps = Arc::new(AtomicU64::new(0));
|
||||
on_progress(extracted.load(Ordering::Relaxed) as f64 / total as f64);
|
||||
|
||||
let run = Run {
|
||||
job,
|
||||
journal,
|
||||
paths,
|
||||
stop: &STOP,
|
||||
progress: Progress {
|
||||
on_bytes: Box::new(move |bytes| {
|
||||
let done = extracted.fetch_add(bytes, Ordering::Relaxed) + bytes;
|
||||
let steps = done.saturating_mul(PROGRESS_UPDATE_STEPS) / total;
|
||||
if steps > reported_steps.fetch_max(steps, Ordering::Relaxed) {
|
||||
on_progress(
|
||||
steps.min(PROGRESS_UPDATE_STEPS) as f64 / PROGRESS_UPDATE_STEPS as f64,
|
||||
);
|
||||
}
|
||||
}),
|
||||
on_commit: Box::new(|_| {}),
|
||||
},
|
||||
keep_free: KEEP_FREE_BYTES,
|
||||
};
|
||||
|
||||
match run.execute() {
|
||||
Ok(Outcome::Finished) => InPlaceExtractionResult::finished(),
|
||||
Ok(Outcome::Paused) => InPlaceExtractionResult::failed("extraction paused"),
|
||||
Err(err) => InPlaceExtractionResult::failed(format!("{err:#}")),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
mod active_window;
|
||||
mod cloud_save;
|
||||
mod constants;
|
||||
mod in_place_extraction;
|
||||
mod linux_process;
|
||||
mod torrent;
|
||||
|
||||
|
|
|
|||
|
|
@ -176,6 +176,11 @@ const build = async () => {
|
|||
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
fs.copyFileSync(sourceLibraryPath, outputNodePath);
|
||||
// The addon embeds UnRAR, whose license must ship with it.
|
||||
fs.copyFileSync(
|
||||
path.join(path.dirname(manifestPath), "LICENSE-UNRAR"),
|
||||
path.join(outputDir, "LICENSE-UNRAR")
|
||||
);
|
||||
|
||||
await copySidecarLibrariesOnWindows();
|
||||
await ensureDepsResolvableOnLinux();
|
||||
|
|
|
|||
|
|
@ -555,7 +555,7 @@ function DownloadGameModalSession({
|
|||
const [
|
||||
deleteArchiveFilesAfterExtraction,
|
||||
setDeleteArchiveFilesAfterExtraction,
|
||||
] = useState(false);
|
||||
] = useState(true);
|
||||
const [isPreparingOptions, setIsPreparingOptions] = useState(false);
|
||||
const stepFrameRef = useRef<HTMLDivElement | null>(null);
|
||||
const activeStepRef = useRef<HTMLDivElement | null>(null);
|
||||
|
|
@ -730,7 +730,7 @@ function DownloadGameModalSession({
|
|||
|
||||
if (!deleteArchiveTouchedRef.current) {
|
||||
setDeleteArchiveFilesAfterExtraction(
|
||||
userPreferences?.deleteArchiveFilesAfterExtractionByDefault ?? false
|
||||
userPreferences?.deleteArchiveFilesAfterExtractionByDefault ?? true
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const DEFAULT_FORM: DownloadsBehaviorForm = {
|
|||
seedAfterDownloadComplete: false,
|
||||
showDownloadSpeedInMegabytes: false,
|
||||
extractFilesByDefault: true,
|
||||
deleteArchiveFilesAfterExtractionByDefault: false,
|
||||
deleteArchiveFilesAfterExtractionByDefault: true,
|
||||
createStartMenuShortcut: true,
|
||||
};
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ const buildForm = (
|
|||
preferences.showDownloadSpeedInMegabytes ?? false,
|
||||
extractFilesByDefault: preferences.extractFilesByDefault ?? true,
|
||||
deleteArchiveFilesAfterExtractionByDefault:
|
||||
preferences.deleteArchiveFilesAfterExtractionByDefault ?? false,
|
||||
preferences.deleteArchiveFilesAfterExtractionByDefault ?? true,
|
||||
createStartMenuShortcut: preferences.createStartMenuShortcut ?? true,
|
||||
}
|
||||
: DEFAULT_FORM;
|
||||
|
|
|
|||
124
src/main/services/archive-extraction.ts
Normal file
124
src/main/services/archive-extraction.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import type { ExtractionProgress } from "./7zip";
|
||||
import type {
|
||||
InPlaceExtractionOptions,
|
||||
InPlaceExtractionResult,
|
||||
} from "./native-addon";
|
||||
|
||||
export const ARCHIVE_PASSWORDS = ["online-fix.me", "steamrip.com"];
|
||||
|
||||
export interface ArchiveExtractors {
|
||||
inPlaceEnabled: () => Promise<boolean>;
|
||||
inPlace: (
|
||||
options: InPlaceExtractionOptions,
|
||||
onProgress: (fraction: number) => void
|
||||
) => Promise<InPlaceExtractionResult>;
|
||||
hasInPlaceJournal: (archivePath: string) => boolean;
|
||||
sevenZip: (
|
||||
options: { filePath: string; outputPath: string; passwords: string[] },
|
||||
onProgress: (progress: ExtractionProgress) => void
|
||||
) => Promise<{ success: boolean }>;
|
||||
log: (message: string) => void;
|
||||
}
|
||||
|
||||
export interface ArchiveExtractionRequest {
|
||||
filePath: string;
|
||||
outputPath: string;
|
||||
// The archive is deleted afterwards anyway, so it may be consumed while
|
||||
// extracting.
|
||||
inPlace: boolean;
|
||||
// Hydra recorded starting an in-place extraction of this archive before.
|
||||
resume: boolean;
|
||||
// Records that an in-place extraction of this archive may start. Runs
|
||||
// before hatchzip touches the archive, so a retry knows to resume it.
|
||||
recordInPlaceStart: () => Promise<void>;
|
||||
// Drops that record once hatchzip reports the archive untouched.
|
||||
forgetInPlaceStart: () => Promise<void>;
|
||||
}
|
||||
|
||||
// In place, hatchzip cuts the archive back as files come out, so the disk
|
||||
// never holds the archive and the game in full at once. An archive it refuses
|
||||
// (a solid RAR, files already in the way, too little space) is untouched and
|
||||
// goes to 7-Zip. One it has started is never handed to 7-Zip, since part of it
|
||||
// is gone: hatchzip finishes it, whatever the feature flag says now. Only a
|
||||
// new run asks the API for the flag, and with the flag off 7-Zip extracts all.
|
||||
export const extractArchive = async (
|
||||
extractors: ArchiveExtractors,
|
||||
{
|
||||
filePath,
|
||||
outputPath,
|
||||
inPlace,
|
||||
resume,
|
||||
recordInPlaceStart,
|
||||
forgetInPlaceStart,
|
||||
}: ArchiveExtractionRequest,
|
||||
onProgress: (progress: ExtractionProgress) => void
|
||||
): Promise<{ success: boolean }> => {
|
||||
const extractWithSevenZip = () =>
|
||||
extractors.sevenZip(
|
||||
{ filePath, outputPath, passwords: ARCHIVE_PASSWORDS },
|
||||
onProgress
|
||||
);
|
||||
|
||||
const allowFresh = inPlace && !resume && (await extractors.inPlaceEnabled());
|
||||
|
||||
if (!resume && !allowFresh) {
|
||||
return extractWithSevenZip();
|
||||
}
|
||||
|
||||
if (allowFresh) {
|
||||
// A journal Hydra did not create may have come with the download, and
|
||||
// recording a start now would let a retry follow it.
|
||||
if (extractors.hasInPlaceJournal(filePath)) {
|
||||
extractors.log(
|
||||
`Extracting ${filePath} with 7-Zip, not in place: it has a journal Hydra did not create`
|
||||
);
|
||||
return extractWithSevenZip();
|
||||
}
|
||||
|
||||
await recordInPlaceStart();
|
||||
}
|
||||
|
||||
let result: InPlaceExtractionResult;
|
||||
try {
|
||||
result = await extractors.inPlace(
|
||||
{
|
||||
archivePath: filePath,
|
||||
outputPath,
|
||||
passwords: ARCHIVE_PASSWORDS,
|
||||
allowFresh,
|
||||
resume,
|
||||
},
|
||||
(fraction) =>
|
||||
onProgress({ percent: fraction * 100, fileCount: 0, file: "" })
|
||||
);
|
||||
} catch (error) {
|
||||
// The addon did not run, so it touched nothing now. What an earlier run
|
||||
// left of the archive still can't go to 7-Zip.
|
||||
if (resume) throw error;
|
||||
|
||||
await forgetInPlaceStart();
|
||||
extractors.log(
|
||||
`Extracting ${filePath} with 7-Zip, in-place extraction could not run: ${error}`
|
||||
);
|
||||
return extractWithSevenZip();
|
||||
}
|
||||
|
||||
if (result.status === "finished") {
|
||||
extractors.log(`Extracted ${filePath} in place`);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
if (result.status === "failed") {
|
||||
throw new Error(
|
||||
`In-place extraction of ${filePath} stopped: ${result.message}`
|
||||
);
|
||||
}
|
||||
|
||||
// Nothing was started, so there is nothing for a retry to resume.
|
||||
await forgetInPlaceStart();
|
||||
extractors.log(
|
||||
`Extracting ${filePath} with 7-Zip, not in place: ${result.message}`
|
||||
);
|
||||
|
||||
return extractWithSevenZip();
|
||||
};
|
||||
|
|
@ -18,6 +18,7 @@ import {
|
|||
} from "@shared";
|
||||
import type {
|
||||
ClassicsDisc,
|
||||
Download,
|
||||
EmulatorSystem,
|
||||
ExtractionFailure,
|
||||
Game,
|
||||
|
|
@ -32,6 +33,9 @@ import path from "node:path";
|
|||
import pngToIco from "png-to-ico";
|
||||
import sharp from "sharp";
|
||||
import { ExtractionProgress, SevenZip } from "./7zip";
|
||||
import { type ArchiveExtractors, extractArchive } from "./archive-extraction";
|
||||
import { NativeAddon } from "./native-addon";
|
||||
import { HydraApi } from "./hydra-api";
|
||||
import * as emulators from "./emulators";
|
||||
import * as retroarch from "./retroarch";
|
||||
import { getPathType } from "./extraction-path";
|
||||
|
|
@ -53,9 +57,52 @@ const MAX_ARCHIVE_EXTRACTION_PASSES = 2;
|
|||
|
||||
const INSTALLED_EXECUTABLE_MAX_DEPTH = 4;
|
||||
|
||||
const IN_PLACE_EXTRACTION_FEATURE = "IN_PLACE_ARCHIVE_EXTRACTION";
|
||||
const FEATURES_REQUEST_TIMEOUT_MS = 10_000;
|
||||
|
||||
// Fails closed: without an answer from the API, archives go to 7-Zip.
|
||||
const isInPlaceExtractionEnabled = () =>
|
||||
HydraApi.get<string[]>("/features", null, {
|
||||
needsAuth: false,
|
||||
timeout: FEATURES_REQUEST_TIMEOUT_MS,
|
||||
})
|
||||
.then(
|
||||
(features) =>
|
||||
Array.isArray(features) &&
|
||||
features.includes(IN_PLACE_EXTRACTION_FEATURE)
|
||||
)
|
||||
.catch((error) => {
|
||||
logger.warn("[GameFilesManager] Could not load feature flags", error);
|
||||
return false;
|
||||
});
|
||||
|
||||
const archiveExtractors: Omit<ArchiveExtractors, "inPlaceEnabled"> = {
|
||||
inPlace: (options, onProgress) =>
|
||||
NativeAddon.extractArchiveInPlace(options, onProgress),
|
||||
hasInPlaceJournal: (archivePath) => {
|
||||
try {
|
||||
return NativeAddon.inPlaceJournalExists(archivePath);
|
||||
} catch {
|
||||
// Without the addon the in-place call fails too, and 7-Zip extracts.
|
||||
return false;
|
||||
}
|
||||
},
|
||||
sevenZip: (options, onProgress) => SevenZip.extractFile(options, onProgress),
|
||||
log: (message) => logger.info(`[GameFilesManager] ${message}`),
|
||||
};
|
||||
|
||||
export class GameFilesManager {
|
||||
private lastProgressUpdateTime = 0;
|
||||
private lastProgressUpdateValue = 0;
|
||||
private inPlaceExtractionEnabled: Promise<boolean> | null = null;
|
||||
|
||||
// The flag is read once per extraction run, so every archive of a download
|
||||
// gets the same answer and a slow API only delays the first.
|
||||
private readonly extractors: ArchiveExtractors = {
|
||||
...archiveExtractors,
|
||||
inPlaceEnabled: () =>
|
||||
(this.inPlaceExtractionEnabled ??= isInPlaceExtractionEnabled()),
|
||||
};
|
||||
|
||||
constructor(
|
||||
private readonly shop: GameShop,
|
||||
|
|
@ -176,21 +223,81 @@ export class GameFilesManager {
|
|||
this.updateExtractionProgress(progress.percent / 100);
|
||||
};
|
||||
|
||||
private async shouldDeleteArchives(download?: Download | null) {
|
||||
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||
levelKeys.userPreferences,
|
||||
{ valueEncoding: "json" }
|
||||
);
|
||||
|
||||
return (
|
||||
download?.automaticallyDeleteArchiveFiles ??
|
||||
userPreferences?.deleteArchiveFilesAfterExtractionByDefault ??
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
private async recordInPlaceStart(archivePath: string) {
|
||||
const download = await downloadsSublevel.get(this.gameKey);
|
||||
|
||||
if (!download) {
|
||||
throw new Error(`No download to record extracting ${archivePath} for`);
|
||||
}
|
||||
|
||||
if (download.inPlaceArchives?.includes(archivePath)) return;
|
||||
|
||||
await downloadsSublevel.put(this.gameKey, {
|
||||
...download,
|
||||
inPlaceArchives: [...(download.inPlaceArchives ?? []), archivePath],
|
||||
});
|
||||
}
|
||||
|
||||
private async forgetInPlaceStart(archivePath: string) {
|
||||
const download = await downloadsSublevel.get(this.gameKey);
|
||||
|
||||
if (!download?.inPlaceArchives?.includes(archivePath)) return;
|
||||
|
||||
const inPlaceArchives = download.inPlaceArchives.filter(
|
||||
(startedArchive) => startedArchive !== archivePath
|
||||
);
|
||||
|
||||
await downloadsSublevel.put(this.gameKey, {
|
||||
...download,
|
||||
inPlaceArchives: inPlaceArchives.length > 0 ? inPlaceArchives : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private async clearInPlaceArchives() {
|
||||
const download = await downloadsSublevel.get(this.gameKey);
|
||||
|
||||
if (!download?.inPlaceArchives) return;
|
||||
|
||||
await downloadsSublevel.put(this.gameKey, {
|
||||
...download,
|
||||
inPlaceArchives: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private async extractArchivePass(
|
||||
directoryPath: string,
|
||||
filesToExtract: string[],
|
||||
pass: number,
|
||||
extractedFiles: Set<string>
|
||||
extractedFiles: Set<string>,
|
||||
inPlace: boolean,
|
||||
startedInPlace: Set<string>
|
||||
): Promise<boolean> {
|
||||
let completedFiles = 0;
|
||||
for (const file of filesToExtract) {
|
||||
const filePath = path.join(directoryPath, file);
|
||||
try {
|
||||
const result = await SevenZip.extractFile(
|
||||
const result = await extractArchive(
|
||||
this.extractors,
|
||||
{
|
||||
filePath,
|
||||
cwd: path.dirname(filePath),
|
||||
passwords: ["online-fix.me", "steamrip.com"],
|
||||
outputPath: path.dirname(filePath),
|
||||
inPlace,
|
||||
resume: startedInPlace.has(filePath),
|
||||
recordInPlaceStart: () => this.recordInPlaceStart(filePath),
|
||||
forgetInPlaceStart: () => this.forgetInPlaceStart(filePath),
|
||||
},
|
||||
(progress) => {
|
||||
const passProgress =
|
||||
|
|
@ -240,17 +347,9 @@ export class GameFilesManager {
|
|||
.filter((archivePath) => fs.existsSync(archivePath));
|
||||
|
||||
if (archivePaths.length > 0) {
|
||||
const [download, userPreferences] = await Promise.all([
|
||||
downloadsSublevel.get(this.gameKey),
|
||||
db.get<string, UserPreferences | null>(levelKeys.userPreferences, {
|
||||
valueEncoding: "json",
|
||||
}),
|
||||
]);
|
||||
|
||||
const shouldDelete =
|
||||
download?.automaticallyDeleteArchiveFiles ??
|
||||
userPreferences?.deleteArchiveFilesAfterExtractionByDefault ??
|
||||
false;
|
||||
const shouldDelete = await this.shouldDeleteArchives(
|
||||
await downloadsSublevel.get(this.gameKey)
|
||||
);
|
||||
|
||||
if (shouldDelete) {
|
||||
for (const archivePath of archivePaths) {
|
||||
|
|
@ -294,9 +393,19 @@ export class GameFilesManager {
|
|||
|
||||
const compressedFiles = new Set<string>();
|
||||
const extractedFiles = new Set<string>();
|
||||
// Only multi-part hoster downloads, which need the feature to start, number their first volume part01.
|
||||
const download = await downloadsSublevel.get(this.gameKey);
|
||||
// Only multi-part hoster downloads, which need the feature to start, number their first volume part01.
|
||||
const firstRarVolume = download?.parts ? /part0*1\.rar$/i : /part1\.rar$/i;
|
||||
// Archives nested in an outer one are kept, so only top-level downloads
|
||||
// that will be deleted anyway are consumed while extracting.
|
||||
const inPlace =
|
||||
!outerArchivePath && (await this.shouldDeleteArchives(download));
|
||||
const startedInPlace = new Set(download?.inPlaceArchives ?? []);
|
||||
// An archive consumed in place is no longer listed, but a run that
|
||||
// stopped before cleaning up still has links and progress files to finish.
|
||||
const unlistedInPlace = [...startedInPlace]
|
||||
.map((archivePath) => path.basename(archivePath))
|
||||
.filter((file) => startedInPlace.has(path.join(directoryPath, file)));
|
||||
|
||||
// Only process archives at the extraction root. Archives in game subfolders
|
||||
// may be runtime assets and must remain intact.
|
||||
|
|
@ -312,6 +421,7 @@ export class GameFilesManager {
|
|||
return false;
|
||||
}
|
||||
|
||||
archives = [...new Set([...archives, ...unlistedInPlace])];
|
||||
archives.forEach((file) => compressedFiles.add(file));
|
||||
const filesToExtract = archives.filter(
|
||||
(file) =>
|
||||
|
|
@ -328,7 +438,9 @@ export class GameFilesManager {
|
|||
directoryPath,
|
||||
filesToExtract,
|
||||
pass,
|
||||
extractedFiles
|
||||
extractedFiles,
|
||||
inPlace,
|
||||
startedInPlace
|
||||
))
|
||||
) {
|
||||
return false;
|
||||
|
|
@ -341,6 +453,10 @@ export class GameFilesManager {
|
|||
compressedFiles,
|
||||
extractedFiles
|
||||
);
|
||||
|
||||
// With an outer archive, the caller clears them once that is done too.
|
||||
if (!outerArchivePath) await this.clearInPlaceArchives();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -979,12 +1095,6 @@ export class GameFilesManager {
|
|||
}
|
||||
|
||||
const filePath = path.join(download.downloadPath, download.folderName);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
await this.failMissingExtractionSource(filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
const archivePath = path.parse(download.folderName);
|
||||
const extractedFolderName = path.join(archivePath.dir, archivePath.name);
|
||||
const extractionPath = path.join(
|
||||
|
|
@ -992,14 +1102,29 @@ export class GameFilesManager {
|
|||
extractedFolderName
|
||||
);
|
||||
|
||||
// An in-place run Hydra started may have consumed the archive already.
|
||||
// hatchzip then finishes what is left of it, or reports it done, so a
|
||||
// retry after a failed nested archive goes on from there.
|
||||
const resume = download.inPlaceArchives?.includes(filePath) ?? false;
|
||||
|
||||
if (!resume && !fs.existsSync(filePath)) {
|
||||
await this.failMissingExtractionSource(filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.updateExtractionProgress(0, true);
|
||||
|
||||
try {
|
||||
const result = await SevenZip.extractFile(
|
||||
const shouldDelete = await this.shouldDeleteArchives(download);
|
||||
const result = await extractArchive(
|
||||
this.extractors,
|
||||
{
|
||||
filePath,
|
||||
outputPath: extractionPath,
|
||||
passwords: ["online-fix.me", "steamrip.com"],
|
||||
inPlace: shouldDelete,
|
||||
resume,
|
||||
recordInPlaceStart: () => this.recordInPlaceStart(filePath),
|
||||
forgetInPlaceStart: () => this.forgetInPlaceStart(filePath),
|
||||
},
|
||||
this.handleProgress
|
||||
);
|
||||
|
|
@ -1014,20 +1139,11 @@ export class GameFilesManager {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (fs.existsSync(extractionPath) && fs.existsSync(filePath)) {
|
||||
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||
levelKeys.userPreferences,
|
||||
{ valueEncoding: "json" }
|
||||
);
|
||||
|
||||
const shouldDelete =
|
||||
download.automaticallyDeleteArchiveFiles ??
|
||||
userPreferences?.deleteArchiveFilesAfterExtractionByDefault ??
|
||||
false;
|
||||
|
||||
if (fs.existsSync(extractionPath)) {
|
||||
if (shouldDelete) {
|
||||
// Clears the installer size too when the archive went in place.
|
||||
await deleteArchiveFile(filePath);
|
||||
} else {
|
||||
} else if (fs.existsSync(filePath)) {
|
||||
WindowManager.sendToAppWindows("on-archive-deletion-prompt", [
|
||||
filePath,
|
||||
]);
|
||||
|
|
@ -1038,6 +1154,7 @@ export class GameFilesManager {
|
|||
...download,
|
||||
folderName: extractedFolderName,
|
||||
sourceArchiveName: download.folderName,
|
||||
inPlaceArchives: undefined,
|
||||
});
|
||||
|
||||
await this.setExtractionComplete();
|
||||
|
|
|
|||
|
|
@ -46,6 +46,23 @@ type NativeActiveWindowResponse = {
|
|||
process_id?: number;
|
||||
};
|
||||
|
||||
export type InPlaceExtractionOptions = {
|
||||
archivePath: string;
|
||||
outputPath: string;
|
||||
passwords: string[];
|
||||
allowFresh: boolean;
|
||||
// Hydra recorded starting an in-place extraction of this archive.
|
||||
resume: boolean;
|
||||
};
|
||||
|
||||
// "unavailable" leaves the archive untouched; "failed" means part of it is
|
||||
// already extracted and removed, and only another in-place call resumes it.
|
||||
// The call itself only rejects when the addon could not run.
|
||||
export type InPlaceExtractionResult = {
|
||||
status: "finished" | "unavailable" | "failed";
|
||||
message?: string | null;
|
||||
};
|
||||
|
||||
export type NativeProcessPayload = ProcessPayload &
|
||||
Pick<
|
||||
EpicProcessNode,
|
||||
|
|
@ -73,6 +90,11 @@ type HydraNativeModule = {
|
|||
) => Promise<NativeProcessSizedImageResponse>;
|
||||
listProcesses: () => NativeProcessPayload[];
|
||||
getLinuxActiveWindow: () => NativeActiveWindowResponse | null;
|
||||
extractArchiveInPlace: (
|
||||
options: InPlaceExtractionOptions,
|
||||
onProgress: (fraction: number) => void
|
||||
) => Promise<InPlaceExtractionResult>;
|
||||
inPlaceJournalExists: (archivePath: string) => boolean;
|
||||
buildLocalGameSnapshotPipeline: (
|
||||
input: BuildLocalGameSnapshotPipelineInput
|
||||
) => Promise<NativeLocalGameSnapshotPipelineResult>;
|
||||
|
|
@ -426,6 +448,17 @@ export class NativeAddon {
|
|||
});
|
||||
}
|
||||
|
||||
public static extractArchiveInPlace(
|
||||
options: InPlaceExtractionOptions,
|
||||
onProgress: (fraction: number) => void
|
||||
) {
|
||||
return this.load().extractArchiveInPlace(options, onProgress);
|
||||
}
|
||||
|
||||
public static inPlaceJournalExists(archivePath: string) {
|
||||
return this.load().inPlaceJournalExists(archivePath);
|
||||
}
|
||||
|
||||
public static buildLocalGameSnapshotPipeline(
|
||||
input: BuildLocalGameSnapshotPipelineInput
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -306,7 +306,7 @@ export function DownloadSettingsModal({
|
|||
deleteArchiveFilesAfterExtraction,
|
||||
setDeleteArchiveFilesAfterExtraction,
|
||||
] = useState(
|
||||
userPreferences?.deleteArchiveFilesAfterExtractionByDefault ?? false
|
||||
userPreferences?.deleteArchiveFilesAfterExtractionByDefault ?? true
|
||||
);
|
||||
const [selectedDownloader, setSelectedDownloader] =
|
||||
useState<Downloader | null>(null);
|
||||
|
|
@ -574,7 +574,7 @@ export function DownloadSettingsModal({
|
|||
useEffect(() => {
|
||||
if (visible) {
|
||||
setDeleteArchiveFilesAfterExtraction(
|
||||
userPreferences?.deleteArchiveFilesAfterExtractionByDefault ?? false
|
||||
userPreferences?.deleteArchiveFilesAfterExtractionByDefault ?? true
|
||||
);
|
||||
setAutomaticExtractionEnabled(
|
||||
userPreferences?.extractFilesByDefault ?? true
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const buildForm = (preferences: UserPreferences | null) => ({
|
|||
)
|
||||
: "",
|
||||
deleteArchiveFilesAfterExtractionByDefault:
|
||||
preferences?.deleteArchiveFilesAfterExtractionByDefault ?? false,
|
||||
preferences?.deleteArchiveFilesAfterExtractionByDefault ?? true,
|
||||
torrentNetworkInterface: preferences?.torrentNetworkInterface ?? "",
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -144,6 +144,10 @@ export interface Download {
|
|||
installerPath?: string;
|
||||
hasInstaller?: boolean;
|
||||
sourceArchiveName?: string;
|
||||
// Archives Hydra started extracting in place, kept until the whole download
|
||||
// is extracted. Only these are resumed, since a progress file next to an
|
||||
// archive may have come with the download.
|
||||
inPlaceArchives?: string[];
|
||||
automaticallyExtract: boolean;
|
||||
automaticallyDeleteArchiveFiles: boolean;
|
||||
fileIndices?: number[];
|
||||
|
|
|
|||
Loading…
Reference in a new issue