Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 10 additions & 16 deletions crates/surge-cli/src/commands/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use surge_core::error::{Result, SurgeError};
use surge_core::install::{self as core_install, InstallProfile};
use surge_core::installer_package::{
InstallerPackageAcquisition, ResolveInstallerPackageOptions, ResolvedInstallerPackage, install_artifact_cache_dir,
prune_install_artifact_cache, resolve_installer_package, retained_artifacts_for_install_cache_without_index,
prune_install_artifact_cache_with_stats, resolve_installer_package, retained_artifacts_for_resolved_install_cache,
};
use surge_core::platform::fs::make_executable;
use surge_core::platform::paths::default_install_root;
Expand Down Expand Up @@ -379,23 +379,17 @@ fn ensure_stage_cache_entry(
}

fn prune_setup_artifact_cache(install_root: &Path, package: &ResolvedPackage, manifest: &InstallerManifest) {
let owned_retained_artifacts;
let retained_artifacts = match package.retained_artifacts.as_ref() {
Some(retained_artifacts) => retained_artifacts,
None => match retained_artifacts_for_install_cache_without_index(manifest) {
Some(retained_artifacts) => {
owned_retained_artifacts = retained_artifacts;
&owned_retained_artifacts
}
None => return,
},
let Some(retained_artifacts) = retained_artifacts_for_resolved_install_cache(package, manifest) else {
return;
};
match prune_install_artifact_cache(install_root, retained_artifacts) {
Ok(0) => {}
Ok(pruned) => {
match prune_install_artifact_cache_with_stats(install_root, retained_artifacts.as_ref()) {
Ok(result) if result.pruned_artifact_count == 0 => {}
Ok(result) => {
logline::info(&format!(
"Pruned {pruned} stale artifact cache entr{}.",
if pruned == 1 { "y" } else { "ies" }
"Pruned {} stale artifact cache entr{}; retained policy key count: {}.",
result.pruned_artifact_count,
if result.pruned_artifact_count == 1 { "y" } else { "ies" },
result.retained_policy_key_count
));
Comment thread
peters marked this conversation as resolved.
Comment thread
peters marked this conversation as resolved.
}
Err(e) => {
Expand Down
37 changes: 37 additions & 0 deletions crates/surge-core/src/installer_package.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};

Expand Down Expand Up @@ -41,6 +42,12 @@ pub struct ResolvedInstallerPackage {
pub acquisition: InstallerPackageAcquisition,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InstallArtifactCachePruneResult {
pub retained_policy_key_count: usize,
pub pruned_artifact_count: usize,
}

impl ResolvedInstallerPackage {
#[must_use]
pub fn path(&self) -> &Path {
Expand Down Expand Up @@ -176,6 +183,36 @@ pub fn prune_install_artifact_cache(install_root: &Path, retained_artifacts: &BT
prune_cached_artifacts(&install_artifact_cache_dir(install_root), retained_artifacts)
}

pub fn prune_install_artifact_cache_with_stats(
install_root: &Path,
retained_artifacts: &BTreeSet<String>,
) -> Result<InstallArtifactCachePruneResult> {
prune_install_artifact_cache_dir_with_stats(&install_artifact_cache_dir(install_root), retained_artifacts)
}

pub fn prune_install_artifact_cache_dir_with_stats(
artifact_cache_dir: &Path,
retained_artifacts: &BTreeSet<String>,
) -> Result<InstallArtifactCachePruneResult> {
let pruned_artifact_count = prune_cached_artifacts(artifact_cache_dir, retained_artifacts)?;
Ok(InstallArtifactCachePruneResult {
retained_policy_key_count: retained_artifacts.len(),
pruned_artifact_count,
})
}

#[must_use]
pub fn retained_artifacts_for_resolved_install_cache<'a>(
package: &'a ResolvedInstallerPackage,
manifest: &InstallerManifest,
) -> Option<Cow<'a, BTreeSet<String>>> {
if let Some(retained_artifacts) = &package.retained_artifacts {
Some(Cow::Borrowed(retained_artifacts))
} else {
retained_artifacts_for_install_cache_without_index(manifest).map(Cow::Owned)
}
}
Comment thread
peters marked this conversation as resolved.

#[must_use]
pub fn retained_artifacts_for_install_cache_without_index(manifest: &InstallerManifest) -> Option<BTreeSet<String>> {
retained_artifacts_for_cache_policy_without_index(
Expand Down
91 changes: 91 additions & 0 deletions crates/surge-core/src/update/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3288,6 +3288,97 @@ echo started > new-child-started
assert!(remaining.is_empty(), "none retention should prune all cached artifacts");
}

#[tokio::test]
async fn test_download_and_apply_latest_full_artifact_retention_prunes_old_fulls_and_deltas() {
let tmp = tempfile::tempdir().unwrap();
let store_root = tmp.path().join("store");
let install_root = tmp.path().join("install");
let app_id = "test-app";
std::fs::create_dir_all(&store_root).unwrap();
std::fs::create_dir_all(&install_root).unwrap();
let app_store = app_scoped_store_root(&store_root, app_id);

let current_app_dir = install_root.join("app");
std::fs::create_dir_all(&current_app_dir).unwrap();
std::fs::write(current_app_dir.join("payload.txt"), "old payload").unwrap();

let rid = current_rid();
let old_full_filename = format!("{app_id}-1.0.0-{rid}-full.tar.zst");
let old_delta_filename = format!("{app_id}-1.0.0-{rid}-delta.tar.zst");
let latest_full_filename = format!("{app_id}-1.1.0-{rid}-full.tar.zst");
let latest_full_path = app_store.join(&latest_full_filename);

let artifact_cache = install_root.join(".surge-cache").join("artifacts");
std::fs::create_dir_all(&artifact_cache).unwrap();
std::fs::write(artifact_cache.join(&old_full_filename), b"old full").unwrap();
std::fs::write(artifact_cache.join(&old_delta_filename), b"old delta").unwrap();

let mut packer = ArchivePacker::new(3).unwrap();
packer.add_buffer("payload.txt", b"new payload", 0o644).unwrap();
packer.finalize_to_file(&latest_full_path).unwrap();

let latest_full_size = std::fs::metadata(&latest_full_path).unwrap().len() as i64;
let latest_full_sha256 = sha256_hex_file(&latest_full_path).unwrap();
let os = current_os_label_for_tests();

let mut old_release = make_entry("1.0.0", "stable", &os, &rid);
old_release.full_filename = old_full_filename.clone();
old_release.deltas = Vec::new();
old_release.preferred_delta_id.clear();

let mut latest_release = make_entry("1.1.0", "stable", &os, &rid);
latest_release.full_filename = latest_full_filename.clone();
latest_release.full_size = latest_full_size;
latest_release.full_sha256 = latest_full_sha256;
latest_release.deltas = Vec::new();
latest_release.preferred_delta_id.clear();

let index = ReleaseIndex {
app_id: app_id.to_string(),
releases: vec![old_release, latest_release],
..ReleaseIndex::default()
};

write_app_scoped_release_index(&store_root, app_id, &index);

let ctx = Arc::new(Context::new());
ctx.set_storage(
StorageProvider::Filesystem,
store_root.to_str().unwrap(),
"",
"",
"",
"",
);

let mut manager = UpdateManager::new(ctx, app_id, "1.0.0", "stable", install_root.to_str().unwrap()).unwrap();
manager
.set_artifact_retention_policy(InstallArtifactCachePolicy {
retention: InstallArtifactCacheRetention::LatestFull,
keep_full_count: 1,
})
.unwrap();

let info = manager.check_for_updates().await.unwrap().unwrap();
manager
.download_and_apply(&info, None::<fn(ProgressInfo)>)
.await
.unwrap();

assert!(
artifact_cache.join(&latest_full_filename).is_file(),
"latest full should remain in the local artifact cache"
);
assert!(
!artifact_cache.join(&old_full_filename).exists(),
"old full should be pruned by latest_full retention"
);
assert!(
!artifact_cache.join(&old_delta_filename).exists(),
"old delta should be pruned by latest_full retention"
);
}

#[cfg(target_os = "linux")]
#[tokio::test]
async fn test_download_and_apply_full_installs_shortcuts() {
Expand Down
12 changes: 6 additions & 6 deletions crates/surge-core/src/update/manager/finalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ use crate::install::{
InstallProfile, RuntimeManifestMetadata, copy_persistent_assets, prune_version_snapshots,
storage_provider_manifest_name, write_runtime_manifest,
};
use crate::installer_package::prune_install_artifact_cache_dir_with_stats;
use crate::platform::fs::atomic_rename;
use crate::platform::shortcuts::install_shortcuts;
use crate::releases::artifact_cache::prune_cached_artifacts;
use crate::releases::manifest::decompress_release_index;
use crate::releases::restore::{
retained_artifacts_for_cache_policy, retained_artifacts_for_cache_policy_without_index,
Expand Down Expand Up @@ -247,12 +247,12 @@ where
retained_artifacts_for_cache_policy_without_index(manager.artifact_retention_policy, &latest.full_filename)
};
if let Some(retained_artifacts) = retained_artifacts {
match prune_cached_artifacts(artifact_cache_dir, &retained_artifacts) {
Ok(0) => {}
Ok(pruned) => {
match prune_install_artifact_cache_dir_with_stats(artifact_cache_dir, &retained_artifacts) {
Ok(result) if result.pruned_artifact_count == 0 => {}
Ok(result) => {
debug!(
pruned,
retained = retained_artifacts.len(),
pruned = result.pruned_artifact_count,
retained = result.retained_policy_key_count,
"Pruned stale local artifact cache entries"
);
}
Expand Down
82 changes: 66 additions & 16 deletions crates/surge-installer-ui/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ use surge_core::config::installer::InstallerManifest;
use surge_core::config::manifest::ShortcutLocation;
use surge_core::install::{self as core_install, InstallProfile, InstallProgress, InstallProgressStage};
use surge_core::installer_package::{
InstallerPackageStage, ResolveInstallerPackageOptions, ResolvedInstallerPackage, prune_install_artifact_cache,
resolve_installer_package, retained_artifacts_for_install_cache_without_index,
InstallArtifactCachePruneResult, InstallerPackageStage, ResolveInstallerPackageOptions, ResolvedInstallerPackage,
prune_install_artifact_cache_with_stats, resolve_installer_package, retained_artifacts_for_resolved_install_cache,
};
use surge_core::platform::paths::default_install_root;
use surge_core::releases::restore::RestoreProgress;
Expand All @@ -30,6 +30,12 @@ pub enum ProgressUpdate {

type ResolvedPackage = ResolvedInstallerPackage;

enum ArtifactCachePruneOutcome {
Skipped,
Completed(InstallArtifactCachePruneResult),
Failed(String),
}

const RESOLVE_PROGRESS: f32 = 0.05;
const DOWNLOAD_START_PROGRESS: f32 = 0.10;
const DOWNLOAD_END_PROGRESS: f32 = 0.40;
Expand Down Expand Up @@ -170,7 +176,11 @@ fn run_install_inner(
&manifest.storage.endpoint,
);
core_install::write_runtime_manifest(&install_root.join("app"), &profile, &runtime_manifest)?;
prune_artifact_cache_for_package(&install_root, &package, manifest);
send_artifact_cache_prune_outcome(
prune_artifact_cache_for_package(&install_root, &package, manifest),
progress_tx,
ctx,
);

send(progress_tx, ctx, ProgressUpdate::Progress(1.0));
send(progress_tx, ctx, ProgressUpdate::Complete(install_root));
Expand Down Expand Up @@ -449,25 +459,65 @@ pub fn run_headless(
&manifest.storage.endpoint,
);
core_install::write_runtime_manifest(&install_root.join("app"), &profile, &runtime_manifest)?;
prune_artifact_cache_for_package(&install_root, &package, manifest);
match prune_artifact_cache_for_package(&install_root, &package, manifest) {
ArtifactCachePruneOutcome::Skipped => {}
ArtifactCachePruneOutcome::Completed(result) => {
eprintln!("{}", format_artifact_cache_prune_result(result));
}
ArtifactCachePruneOutcome::Failed(error) => {
eprintln!("Artifact cache pruning failed: {error}");
}
}
eprintln!("Installed '{}' to '{}'", manifest.app_id, install_root.display());

Ok(install_root)
}

fn prune_artifact_cache_for_package(install_root: &Path, package: &ResolvedPackage, manifest: &InstallerManifest) {
let owned_retained_artifacts;
let retained_artifacts = match package.retained_artifacts.as_ref() {
Some(retained_artifacts) => retained_artifacts,
None => match retained_artifacts_for_install_cache_without_index(manifest) {
Some(retained_artifacts) => {
owned_retained_artifacts = retained_artifacts;
&owned_retained_artifacts
}
None => return,
},
fn prune_artifact_cache_for_package(
install_root: &Path,
package: &ResolvedPackage,
manifest: &InstallerManifest,
) -> ArtifactCachePruneOutcome {
let Some(retained_artifacts) = retained_artifacts_for_resolved_install_cache(package, manifest) else {
return ArtifactCachePruneOutcome::Skipped;
};
let _ = prune_install_artifact_cache(install_root, retained_artifacts);
match prune_install_artifact_cache_with_stats(install_root, retained_artifacts.as_ref()) {
Ok(result) => ArtifactCachePruneOutcome::Completed(result),
Err(e) => ArtifactCachePruneOutcome::Failed(e.to_string()),
}
}

fn send_artifact_cache_prune_outcome(
outcome: ArtifactCachePruneOutcome,
progress_tx: &Sender<ProgressUpdate>,
ctx: &egui::Context,
) {
match outcome {
ArtifactCachePruneOutcome::Skipped => {}
ArtifactCachePruneOutcome::Completed(result) => {
send(
progress_tx,
ctx,
ProgressUpdate::Status(format_artifact_cache_prune_result(result)),
);
}
ArtifactCachePruneOutcome::Failed(error) => {
send(
progress_tx,
ctx,
ProgressUpdate::Status(format!("Artifact cache pruning failed: {error}")),
);
}
}
}

fn format_artifact_cache_prune_result(result: InstallArtifactCachePruneResult) -> String {
format!(
"Pruned {} stale artifact cache entr{}; retained policy key count: {}.",
result.pruned_artifact_count,
if result.pruned_artifact_count == 1 { "y" } else { "ies" },
result.retained_policy_key_count
)
}

#[cfg(test)]
Expand Down
Loading