feat: add verified profile synchronizer
This commit is contained in:
Generated
+302
-2
@@ -309,6 +309,23 @@ 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 = "chacha20"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.1",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.45"
|
||||
@@ -390,6 +407,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.1"
|
||||
@@ -846,6 +872,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1026,8 +1053,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1049,8 +1078,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi 6.0.0",
|
||||
"rand_core",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1300,6 +1332,22 @@ dependencies = [
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
|
||||
dependencies = [
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.20"
|
||||
@@ -1754,6 +1802,12 @@ version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever"
|
||||
version = "0.38.0"
|
||||
@@ -2377,6 +2431,62 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.4.3",
|
||||
"lru-slab",
|
||||
"rand",
|
||||
"rand_pcg",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.20",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
@@ -2398,6 +2508,32 @@ version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
||||
dependencies = [
|
||||
"chacha20",
|
||||
"getrandom 0.4.3",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||
|
||||
[[package]]
|
||||
name = "rand_pcg"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-handle"
|
||||
version = "0.6.2"
|
||||
@@ -2473,6 +2609,46 @@ version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.13.4"
|
||||
@@ -2507,6 +2683,20 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.17",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.3"
|
||||
@@ -2522,12 +2712,53 @@ dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
@@ -2718,6 +2949,18 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.22.0"
|
||||
@@ -2789,7 +3032,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
]
|
||||
|
||||
@@ -2797,6 +3040,7 @@ dependencies = [
|
||||
name = "shacraft-launcher"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
@@ -2929,6 +3173,12 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "swift-rs"
|
||||
version = "1.0.8"
|
||||
@@ -3092,7 +3342,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"plist",
|
||||
"raw-window-handle",
|
||||
"reqwest",
|
||||
"reqwest 0.13.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
@@ -3393,6 +3643,16 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.19"
|
||||
@@ -3683,6 +3943,12 @@ version = "1.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
@@ -3870,6 +4136,16 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web_atoms"
|
||||
version = "0.2.6"
|
||||
@@ -3926,6 +4202,15 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webview2-com"
|
||||
version = "0.38.2"
|
||||
@@ -4156,6 +4441,15 @@ dependencies = [
|
||||
"windows-targets 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
@@ -4468,6 +4762,12 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.5"
|
||||
|
||||
@@ -18,3 +18,4 @@ serde_json = "1"
|
||||
sha2 = "0.10"
|
||||
tauri = { version = "2", features = [] }
|
||||
url = "2"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] }
|
||||
|
||||
+18
-1
@@ -51,9 +51,26 @@ async fn inspect_profile(app: AppHandle, manifest_json: String) -> Result<profil
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
/// Synchronizes launcher-managed files after manifest validation.
|
||||
#[tauri::command]
|
||||
async fn sync_profile(app: AppHandle, manifest_json: String) -> Result<profile::SyncResult, String> {
|
||||
let manifest = manifest::validate_json(&manifest_json).map_err(|error| error.to_string())?;
|
||||
let root = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|error| format!("Cannot resolve launcher data directory: {error}"))?
|
||||
.join("profiles")
|
||||
.join(&manifest.id);
|
||||
|
||||
tauri::async_runtime::spawn_blocking(move || profile::sync(&root, &manifest))
|
||||
.await
|
||||
.map_err(|error| format!("Profile synchronization task failed: {error}"))?
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![native_host, validate_manifest, inspect_profile])
|
||||
.invoke_handler(tauri::generate_handler![native_host, validate_manifest, inspect_profile, sync_profile])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running ShaCraft Launcher");
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ pub struct ManagedFile {
|
||||
pub policy: FilePolicy,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FilePolicy {
|
||||
Managed,
|
||||
@@ -135,7 +135,7 @@ fn is_safe_relative_path(value: &str) -> bool {
|
||||
&& !value.split('/').any(|part| part.is_empty() || part == "." || part == "..")
|
||||
}
|
||||
|
||||
fn is_allowed_download_url(value: &str) -> bool {
|
||||
pub(crate) fn is_allowed_download_url(value: &str) -> bool {
|
||||
let Ok(url) = Url::parse(value) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
+141
-2
@@ -1,7 +1,8 @@
|
||||
use crate::manifest::Manifest;
|
||||
use crate::manifest::{is_allowed_download_url, FilePolicy, ManagedFile, Manifest};
|
||||
use reqwest::{blocking::Client, redirect::Policy};
|
||||
use serde::Serialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{fmt, fs::File, io::{self, Read}, path::Path};
|
||||
use std::{fmt, fs::{self, File}, io::{self, Read, Write}, path::{Path, PathBuf}};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -13,15 +14,32 @@ pub struct ProfileInspection {
|
||||
pub up_to_date: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncResult {
|
||||
pub root: String,
|
||||
pub downloaded_files: usize,
|
||||
pub reused_files: usize,
|
||||
pub downloaded_bytes: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ProfileError {
|
||||
Io(io::Error),
|
||||
Network(reqwest::Error),
|
||||
HttpStatus { path: String, status: reqwest::StatusCode },
|
||||
InvalidResponse { path: String, message: String },
|
||||
Integrity { path: String, message: String },
|
||||
}
|
||||
|
||||
impl fmt::Display for ProfileError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Io(error) => write!(formatter, "Cannot inspect profile: {error}"),
|
||||
Self::Network(error) => write!(formatter, "Cannot download profile file: {error}"),
|
||||
Self::HttpStatus { path, status } => write!(formatter, "Download failed for {path}: server returned {status}"),
|
||||
Self::InvalidResponse { path, message } => write!(formatter, "Invalid response for {path}: {message}"),
|
||||
Self::Integrity { path, message } => write!(formatter, "Integrity check failed for {path}: {message}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,6 +77,127 @@ pub fn inspect(root: &Path, manifest: &Manifest) -> Result<ProfileInspection, Pr
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sync(root: &Path, manifest: &Manifest) -> Result<SyncResult, ProfileError> {
|
||||
let client = Client::builder()
|
||||
.redirect(Policy::custom(|attempt| {
|
||||
if is_allowed_download_url(attempt.url().as_str()) {
|
||||
attempt.follow()
|
||||
} else {
|
||||
attempt.stop()
|
||||
}
|
||||
}))
|
||||
.build()
|
||||
.map_err(ProfileError::Network)?;
|
||||
|
||||
let mut downloaded_files = 0;
|
||||
let mut reused_files = 0;
|
||||
let mut downloaded_bytes = 0;
|
||||
|
||||
for expected in &manifest.files {
|
||||
let target = root.join(&expected.path);
|
||||
if matches!(expected.policy, FilePolicy::Seed) && target.exists() {
|
||||
reused_files += 1;
|
||||
continue;
|
||||
}
|
||||
if is_current(&target, expected)? {
|
||||
reused_files += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let bytes = download_file(&client, expected, &target)?;
|
||||
downloaded_files += 1;
|
||||
downloaded_bytes += bytes;
|
||||
}
|
||||
|
||||
Ok(SyncResult {
|
||||
root: root.display().to_string(),
|
||||
downloaded_files,
|
||||
reused_files,
|
||||
downloaded_bytes,
|
||||
})
|
||||
}
|
||||
|
||||
fn is_current(path: &Path, expected: &ManagedFile) -> Result<bool, ProfileError> {
|
||||
let metadata = match path.metadata() {
|
||||
Ok(metadata) => metadata,
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(false),
|
||||
Err(error) => return Err(ProfileError::Io(error)),
|
||||
};
|
||||
Ok(metadata.is_file() && metadata.len() == expected.size && sha256(path)? == expected.sha256.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
fn download_file(client: &Client, expected: &ManagedFile, target: &Path) -> Result<u64, ProfileError> {
|
||||
let parent = target.parent().ok_or_else(|| ProfileError::Integrity {
|
||||
path: expected.path.clone(),
|
||||
message: "target has no parent directory".into(),
|
||||
})?;
|
||||
fs::create_dir_all(parent).map_err(ProfileError::Io)?;
|
||||
|
||||
let mut response = client.get(&expected.url).send().map_err(ProfileError::Network)?;
|
||||
if !response.status().is_success() {
|
||||
return Err(ProfileError::HttpStatus { path: expected.path.clone(), status: response.status() });
|
||||
}
|
||||
if let Some(length) = response.content_length() {
|
||||
if length != expected.size {
|
||||
return Err(ProfileError::InvalidResponse {
|
||||
path: expected.path.clone(),
|
||||
message: format!("expected {} bytes, received Content-Length {length}", expected.size),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let temporary = temp_path(target)?;
|
||||
let result = write_and_verify(&mut response, &temporary, expected);
|
||||
if let Err(error) = result {
|
||||
let _ = fs::remove_file(&temporary);
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
fs::rename(&temporary, target).map_err(ProfileError::Io)?;
|
||||
Ok(expected.size)
|
||||
}
|
||||
|
||||
fn temp_path(target: &Path) -> Result<PathBuf, ProfileError> {
|
||||
let file_name = target.file_name().and_then(|name| name.to_str()).ok_or_else(|| ProfileError::Integrity {
|
||||
path: target.display().to_string(),
|
||||
message: "target has no valid filename".into(),
|
||||
})?;
|
||||
Ok(target.with_file_name(format!(".{file_name}.shacraft.part")))
|
||||
}
|
||||
|
||||
fn write_and_verify(response: &mut reqwest::blocking::Response, temporary: &Path, expected: &ManagedFile) -> Result<(), ProfileError> {
|
||||
let mut output = File::create(temporary).map_err(ProfileError::Io)?;
|
||||
let mut digest = Sha256::new();
|
||||
let mut bytes = 0_u64;
|
||||
let mut buffer = [0_u8; 64 * 1024];
|
||||
|
||||
loop {
|
||||
let read = response.read(&mut buffer).map_err(ProfileError::Io)?;
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
output.write_all(&buffer[..read]).map_err(ProfileError::Io)?;
|
||||
digest.update(&buffer[..read]);
|
||||
bytes += read as u64;
|
||||
}
|
||||
output.sync_all().map_err(ProfileError::Io)?;
|
||||
|
||||
if bytes != expected.size {
|
||||
return Err(ProfileError::Integrity {
|
||||
path: expected.path.clone(),
|
||||
message: format!("expected {} bytes, downloaded {bytes}", expected.size),
|
||||
});
|
||||
}
|
||||
let actual = format!("{:x}", digest.finalize());
|
||||
if actual != expected.sha256.to_ascii_lowercase() {
|
||||
return Err(ProfileError::Integrity {
|
||||
path: expected.path.clone(),
|
||||
message: "SHA-256 does not match manifest".into(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sha256(path: &Path) -> Result<String, ProfileError> {
|
||||
let mut file = File::open(path).map_err(ProfileError::Io)?;
|
||||
let mut digest = Sha256::new();
|
||||
|
||||
Reference in New Issue
Block a user