mirror of
https://codeberg.org/spire/depot-prefetch.git
synced 2026-06-03 10:04:09 +02:00
initial version
This commit is contained in:
parent
aab6263aae
commit
932c20cd71
6 changed files with 3300 additions and 2 deletions
3037
Cargo.lock
generated
3037
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
10
Cargo.toml
10
Cargo.toml
|
|
@ -2,5 +2,15 @@
|
||||||
name = "depot-prefetch"
|
name = "depot-prefetch"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
repository = "https://codeberg.org/spire/depot-prefetch"
|
||||||
|
license = "EUPL-1.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
vdf-reader = "0.3.1"
|
||||||
|
steam-vent = "0.3.1"
|
||||||
|
thiserror = "2.0.12"
|
||||||
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
|
tokio = { version = "1.45.1", features = ["macros", "rt-multi-thread"] }
|
||||||
|
main_error = "0.1.2"
|
||||||
|
clap = { version = "4.5.40", features = ["derive"] }
|
||||||
|
serde_json = "1.0.140"
|
||||||
1
src/error.rs
Normal file
1
src/error.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
67
src/main.rs
67
src/main.rs
|
|
@ -1,3 +1,66 @@
|
||||||
fn main() {
|
use clap::Parser;
|
||||||
println!("Hello, world!");
|
use crate::product_info::ProductInfoFetcher;
|
||||||
|
use main_error::MainResult;
|
||||||
|
use serde::Serialize;
|
||||||
|
use crate::prefetch::prefetch;
|
||||||
|
|
||||||
|
mod product_info;
|
||||||
|
mod error;
|
||||||
|
mod prefetch;
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
struct Args {
|
||||||
|
/// App to prefetch depots for
|
||||||
|
app_id: u32,
|
||||||
|
/// Branch to prefetch, defaults to "public"
|
||||||
|
#[clap(long, default_value_t = String::from("public"))]
|
||||||
|
branch: String,
|
||||||
|
/// Only prefetch the listed depot(s)
|
||||||
|
#[clap(long)]
|
||||||
|
depot: Vec<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> MainResult {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
let info_fetcher = ProductInfoFetcher::new().await?;
|
||||||
|
let depots = info_fetcher.fetch_depots(args.app_id).await?;
|
||||||
|
|
||||||
|
let manifests = depots.into_iter()
|
||||||
|
.filter(|(depot_id, _)| args.depot.is_empty() || args.depot.contains(depot_id))
|
||||||
|
.flat_map(|(depot_id, depot)| depot.branches.into_iter().map(move |(branch, manifest)| (depot_id, branch, manifest)))
|
||||||
|
.filter(|(_, branch, _)| &args.branch == branch)
|
||||||
|
.map(|(depot_id, _, manifest)| ManifestInput {
|
||||||
|
app_id: args.app_id,
|
||||||
|
depot_id,
|
||||||
|
manifest: manifest.gid,
|
||||||
|
});
|
||||||
|
|
||||||
|
let outputs = manifests.map(|input| {
|
||||||
|
prefetch(&input).map(|hash| ManifestOutput {
|
||||||
|
app_id: input.app_id,
|
||||||
|
depot_id: input.depot_id,
|
||||||
|
manifest: input.manifest,
|
||||||
|
hash,
|
||||||
|
})
|
||||||
|
}).collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
|
serde_json::to_writer_pretty(std::io::stdout(), &outputs)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ManifestInput {
|
||||||
|
app_id: u32,
|
||||||
|
depot_id: u32,
|
||||||
|
manifest: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct ManifestOutput {
|
||||||
|
app_id: u32,
|
||||||
|
depot_id: u32,
|
||||||
|
manifest: u64,
|
||||||
|
hash: String,
|
||||||
}
|
}
|
||||||
63
src/prefetch.rs
Normal file
63
src/prefetch.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
use crate::ManifestInput;
|
||||||
|
use std::process::Command;
|
||||||
|
use std::string::FromUtf8Error;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
const EXPRESSION_TEMPLATE: &str = r#"
|
||||||
|
{
|
||||||
|
pkgs ? import (fetchTarball {
|
||||||
|
url = "https://github.com/NixOS/nixpkgs/archive/9ba04bda9249d5d5e5238303c9755de5a49a79c5.tar.gz";
|
||||||
|
sha256 = "sha256-H8J4H2XCIMEJ5g6fZ179QfQvsc2dUqhqfBjC8RAHNRY=";
|
||||||
|
}) {}
|
||||||
|
}: let
|
||||||
|
steam-fetcher = pkgs.fetchFromGitHub {
|
||||||
|
owner = "nix-community";
|
||||||
|
repo = "steam-fetcher";
|
||||||
|
rev = "12f66eafb7862d91b3e30c14035f96a21941bd9c";
|
||||||
|
hash = "sha256-PkgC9jqoN6cJ8XYzTA2PlrWs7aPJkM3BGiTxNqax0cA=";
|
||||||
|
};
|
||||||
|
fetchSteam = pkgs.callPackage (steam-fetcher + "/fetch-steam") {};
|
||||||
|
in fetchSteam {
|
||||||
|
name = "steam-depot-APP_ID-DEPOT_ID";
|
||||||
|
appId = "APP_ID";
|
||||||
|
depotId = "DEPOT_ID";
|
||||||
|
manifestId = "MANIFEST_ID";
|
||||||
|
hash = "";
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
pub fn prefetch(manifest: &ManifestInput) -> Result<String, PrefetchError> {
|
||||||
|
let expression = EXPRESSION_TEMPLATE
|
||||||
|
.replace("APP_ID", &manifest.app_id.to_string())
|
||||||
|
.replace("DEPOT_ID", &manifest.depot_id.to_string())
|
||||||
|
.replace("MANIFEST_ID", &manifest.manifest.to_string());
|
||||||
|
|
||||||
|
let command = Command::new("nix-build")
|
||||||
|
.arg("-E")
|
||||||
|
.arg(&expression)
|
||||||
|
.arg("--no-out-link")
|
||||||
|
.output()
|
||||||
|
.map_err(PrefetchError::CommandFailed)?;
|
||||||
|
|
||||||
|
let stderr = String::from_utf8(command.stderr).map_err(PrefetchError::Utf8)?;
|
||||||
|
let Some(line) = stderr.lines().find(|line| line.contains("got:")) else {
|
||||||
|
return Err(PrefetchError::BuildFailed);
|
||||||
|
};
|
||||||
|
let Some(start) = line.find("sha256-") else {
|
||||||
|
return Err(PrefetchError::BuildFailed);
|
||||||
|
};
|
||||||
|
let Some(length) = line[start..].find("=") else {
|
||||||
|
return Err(PrefetchError::BuildFailed);
|
||||||
|
};
|
||||||
|
Ok(line[start..(start + length + 1)].to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum PrefetchError {
|
||||||
|
#[error("Error while running nix-build: {0:#}")]
|
||||||
|
CommandFailed(std::io::Error),
|
||||||
|
#[error("nix-build failed or didn't provide an output hash")]
|
||||||
|
BuildFailed,
|
||||||
|
#[error("nix-build returned non-utf8 output")]
|
||||||
|
Utf8(FromUtf8Error),
|
||||||
|
}
|
||||||
124
src/product_info.rs
Normal file
124
src/product_info.rs
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::string::FromUtf8Error;
|
||||||
|
use steam_vent::proto::steammessages_clientserver_appinfo::{
|
||||||
|
CMsgClientPICSProductInfoRequest, CMsgClientPICSProductInfoResponse,
|
||||||
|
cmsg_client_picsproduct_info_request,
|
||||||
|
};
|
||||||
|
use steam_vent::{Connection, ConnectionError, ConnectionTrait, NetworkError, ServerList};
|
||||||
|
use thiserror::Error;
|
||||||
|
use vdf_reader::VdfError;
|
||||||
|
|
||||||
|
pub struct ProductInfoFetcher {
|
||||||
|
connection: Connection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProductInfoFetcher {
|
||||||
|
pub async fn new() -> Result<ProductInfoFetcher, ProductInfoError> {
|
||||||
|
let server_list = ServerList::discover()
|
||||||
|
.await
|
||||||
|
.map_err(ConnectionError::Discovery)?;
|
||||||
|
let connection = Connection::anonymous(&server_list).await?;
|
||||||
|
Ok(ProductInfoFetcher { connection })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_depots(
|
||||||
|
&self,
|
||||||
|
app_id: u32,
|
||||||
|
) -> Result<BTreeMap<u32, Depot>, ProductInfoError> {
|
||||||
|
let msg = CMsgClientPICSProductInfoRequest {
|
||||||
|
apps: vec![cmsg_client_picsproduct_info_request::AppInfo {
|
||||||
|
appid: Some(app_id),
|
||||||
|
only_public_obsolete: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
meta_data_only: Some(false),
|
||||||
|
single_response: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let response: CMsgClientPICSProductInfoResponse = self.connection.job(msg).await?;
|
||||||
|
let app_info = response.apps.into_iter().next().unwrap();
|
||||||
|
let mut buffer = app_info.buffer.unwrap_or_default();
|
||||||
|
while buffer.ends_with(&[0]) {
|
||||||
|
let _ = buffer.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
let raw_vdf = String::from_utf8(buffer)?;
|
||||||
|
let vdf: Response = vdf_reader::from_str(&raw_vdf)?;
|
||||||
|
assert_eq!(app_id, vdf.app_info.app_id);
|
||||||
|
|
||||||
|
Ok(vdf
|
||||||
|
.app_info
|
||||||
|
.depots
|
||||||
|
.depots
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(depot_id, depot)| Some((u32::from_str(&depot_id).ok()?, depot)))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Response {
|
||||||
|
#[serde(rename = "appinfo")]
|
||||||
|
app_info: AppInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AppInfo {
|
||||||
|
#[serde(rename = "appid")]
|
||||||
|
app_id: u32,
|
||||||
|
depots: Depots,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Depots {
|
||||||
|
overridescddb: bool,
|
||||||
|
branches: HashMap<String, Branch>,
|
||||||
|
#[serde(flatten)]
|
||||||
|
depots: BTreeMap<String, Depot>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Depot {
|
||||||
|
#[serde(default)]
|
||||||
|
pub config: DepotConfig,
|
||||||
|
#[serde(rename = "manifests")]
|
||||||
|
pub branches: HashMap<String, Manifest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
pub struct DepotConfig {
|
||||||
|
pub oslist: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Manifest {
|
||||||
|
pub gid: u64,
|
||||||
|
pub size: u64,
|
||||||
|
pub download: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Branch {
|
||||||
|
timeupdated: u64,
|
||||||
|
buildid: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ProductInfoError {
|
||||||
|
#[error("Error while connecting to steam: {0:#}")]
|
||||||
|
Connect(#[from] ConnectionError),
|
||||||
|
#[error("Error while fetching product info from steam: {0:#}")]
|
||||||
|
Steam(#[from] NetworkError),
|
||||||
|
#[error("Steam returned non-utf8 data")]
|
||||||
|
Utf8(#[from] FromUtf8Error),
|
||||||
|
#[error("Error while parsing product info: {0:#}")]
|
||||||
|
Vdf(#[from] VdfError),
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue