Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-organises disk module #25

Merged
merged 1 commit into from
Apr 18, 2024
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
304 changes: 11 additions & 293 deletions src/disk.rs
Original file line number Diff line number Diff line change
@@ -1,296 +1,14 @@
// Copyright 2024 Dotanuki Labs
// SPDX-License-Identifier: MIT

use crate::models::{AllocatedResource, ProjectLevelDiskCache, UseCase, UserLevelDiskCache};
use itertools::Itertools;
use std::path::{Path, PathBuf};
use ubyte::ByteUnit;
use walkdir::{DirEntry, WalkDir};

pub fn usage_for_gradle_projects(projects: &[PathBuf]) -> anyhow::Result<AllocatedResource> {
let use_case = UseCase::from(ProjectLevelDiskCache::BuildOutput);
let allocated = projects
.iter()
.map(|project| usage_for_gradle_project(project.as_path()))
.collect::<anyhow::Result<Vec<AllocatedResource>>>()?;

let total_amount = allocated
.iter()
.fold(ByteUnit::from(0), |total, allocation| total + allocation.amount);

Ok(AllocatedResource::new(use_case, total_amount))
}

fn usage_for_gradle_project(gradle_project: &Path) -> anyhow::Result<AllocatedResource> {
let use_case = UseCase::from(ProjectLevelDiskCache::BuildOutput);

let Ok(true) = gradle_project.try_exists() else {
return Ok(AllocatedResource::new(use_case, ByteUnit::from(0)));
};

let total = WalkDir::new(gradle_project)
.into_iter()
.filter_map(|entry| entry.ok())
.filter(ensure_build_output_file)
.map(|entry| size_for_entry(&entry))
.sum::<u64>();

Ok(AllocatedResource::new(use_case, ByteUnit::from(total)))
}

pub fn usage_for_maven_local(maven_local: &Path) -> anyhow::Result<AllocatedResource> {
// We trust that M2 local repository lives under $HOME/.m2
let use_case = UseCase::from(UserLevelDiskCache::MavenLocalRepository);

let Ok(true) = maven_local.try_exists() else {
return Ok(AllocatedResource::new(use_case, ByteUnit::from(0)));
};

let total_amount = WalkDir::new(maven_local)
.into_iter()
.filter_map(|entry| entry.ok())
.filter(ensure_file)
.map(|entry| size_for_entry(&entry))
.sum::<u64>();

Ok(AllocatedResource::new(use_case, ByteUnit::from(total_amount)))
}

pub fn usage_for_gradle_home(gradle_home: &Path) -> anyhow::Result<Vec<AllocatedResource>> {
// We trust that gradle_home == $HOME/.gradle
let Ok(true) = gradle_home.try_exists() else {
return Ok(vec![]);
};

let total_per_use_case = WalkDir::new(gradle_home)
.into_iter()
.filter_map(|entry| entry.ok())
.filter(ensure_file)
.map(|entry| (size_for_entry(&entry), evaluate_use_case_from_gradle_home(&entry)))
.filter(|item| item.1 != UseCase::from(UserLevelDiskCache::GradleOtherCaches))
.group_by(|item| item.1)
.into_iter()
.map(|(use_case, group)| (use_case, group.fold(0, |total, (entry_size, _)| total + entry_size)))
.map(|(use_case, total)| AllocatedResource::new(use_case, ByteUnit::from(total)))
.sorted_by_key(|item| item.use_case)
.collect::<Vec<_>>();

Ok(total_per_use_case)
}

fn size_for_entry(entry: &DirEntry) -> u64 {
entry.metadata().expect("Expecting a valid metadata for entry").len()
}

fn ensure_file(entry: &DirEntry) -> bool {
entry
.metadata()
.expect("Expecting a valid metadata for entry")
.is_file()
}

fn ensure_build_output_file(entry: &DirEntry) -> bool {
let path = entry.path().to_str().expect("Expecting a valid path");
let build_output = path.contains("build/");

let build_output_file = entry
.metadata()
.expect("Expecting a valid metadata for entry")
.is_file();

build_output && build_output_file
}

fn evaluate_use_case_from_gradle_home(entry: &DirEntry) -> UseCase {
let raw_path = entry
.path()
.to_str()
.expect("Should be able to evaluate path as string");

// https://docs.gradle.org/current/userguide/configuration_cache.html
// https://docs.gradle.org/current/userguide/directory_layout.html
let cache_type = match raw_path {
_ if raw_path.contains(".gradle/caches") => UserLevelDiskCache::GradleBuildCaching,
_ if raw_path.contains(".gradle/configuration-cache") => UserLevelDiskCache::GradleConfigurationCaching,
_ if raw_path.contains(".gradle/daemon") => UserLevelDiskCache::GradleDaemonLogs,
_ if raw_path.contains(".gradle/jdks") => UserLevelDiskCache::GradleJDKToolchains,
_ if raw_path.contains(".gradle/wrapper") => UserLevelDiskCache::GradleDistributions,
_ if raw_path.contains(".gradle/.tmp") => UserLevelDiskCache::GradleTemporaryFiles,
_ if raw_path.contains(".gradle/native") => UserLevelDiskCache::GradleNativeFiles,
_ if raw_path.contains(".gradle/build-scan-data") => UserLevelDiskCache::GradleBuildScans,
_ => UserLevelDiskCache::GradleOtherCaches,
};

UseCase::from(cache_type)
}

#[cfg(test)]
mod tests {
use crate::disk::{usage_for_gradle_home, usage_for_gradle_projects, usage_for_maven_local};
use crate::models::{AllocatedResource, ProjectLevelDiskCache, UseCase, UserLevelDiskCache};
use fake::{Fake, StringFaker};
use std::fs;
use std::fs::File;
use std::io::Write;
use temp_dir::TempDir;
use ubyte::{ByteUnit, ToByteUnit};
use uuid::Uuid;

const CHARS: &str = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

fn prepare_fake_gradle_home(dir: &TempDir) {
let folders = vec![
".gradle",
".gradle/caches",
".gradle/caches/build-cache-1",
".gradle/configuration-cache",
".gradle/daemon",
".gradle/daemon/8.7",
];

for folder in folders {
fs::create_dir(dir.path().join(folder)).expect("Cant create temporary fixture folder");
}
}

fn prepare_fake_gradle_projects(dir: &TempDir) {
let folders = vec![
"AndroidStudioProjects",
"AndroidStudioProjects/my-project",
"AndroidStudioProjects/my-project/build",
"AndroidStudioProjects/my-project/.idea",
"AndroidStudioProjects/my-project/.gradle",
];

for folder in folders {
fs::create_dir(dir.path().join(folder)).expect("Cant create temporary fixture folder");
}

let files = vec![
"AndroidStudioProjects/my-project/settings.gradle",
"AndroidStudioProjects/my-project/build.gradle",
"AndroidStudioProjects/my-project/gradlew",
"AndroidStudioProjects/my-project/gradle.properties",
];

for file in files {
fs::write(dir.path().join(file), "foo").expect("Cant create fixture file");
}
}

fn create_fake_1kb_file(gradle_home: &TempDir, folder: &str) {
let file_name = Uuid::new_v4();
let relative_path = format!("{}/{}", folder, file_name);
let complete_path = gradle_home.path().join(relative_path);

let faker = StringFaker::with(Vec::from(CHARS), 1000);
let fake: String = faker.fake();
let mut fake_file = File::create(complete_path).expect("Cannot create temp file");
fake_file
.write_all(fake.as_bytes())
.expect("Cannot write into temp file");
fake_file.sync_all().expect("Cannot sync temp file with FileSystem");
}

#[test]
fn should_compute_no_shared_caches_when_missing_gradle_home() {
let temp_dir = TempDir::new().expect("Cant create temp dir");
let fake_gradle_home_path = temp_dir.path();

let usages = usage_for_gradle_home(fake_gradle_home_path).expect("Cannot compute use cases");

assert!(usages.is_empty())
}

#[test]
fn should_compute_shared_caches_when_gradle_home_has_content() {
let temp_dir = TempDir::new().expect("Cant create temp dir");

prepare_fake_gradle_home(&temp_dir);

for _ in 0..2 {
create_fake_1kb_file(&temp_dir, ".gradle/daemon/8.7");
}

for _ in 0..3 {
create_fake_1kb_file(&temp_dir, ".gradle/caches/build-cache-1");
}

create_fake_1kb_file(&temp_dir, ".gradle/configuration-cache");

let fake_gradle_home_path = temp_dir.path();
let usages = usage_for_gradle_home(fake_gradle_home_path).expect("Cannot compute use cases");

let expected = vec![
AllocatedResource::new(
UseCase::from(UserLevelDiskCache::GradleConfigurationCaching),
1.kilobytes(),
),
AllocatedResource::new(UseCase::from(UserLevelDiskCache::GradleBuildCaching), 3.kilobytes()),
AllocatedResource::new(UseCase::from(UserLevelDiskCache::GradleDaemonLogs), 2.kilobytes()),
];

assert_eq!(usages, expected);
}

#[test]
fn should_compute_shared_caches_when_missing_maven_local() {
let temp_dir = TempDir::new().expect("Cant create temp dir");
let fake_maven_local_path = temp_dir.path();

let usage = usage_for_maven_local(fake_maven_local_path).expect("Cannot compute use cases");

let use_case = UseCase::from(UserLevelDiskCache::MavenLocalRepository);
let expected = AllocatedResource::new(use_case, ByteUnit::from(0));
assert_eq!(usage, expected)
}

#[test]
fn should_compute_shared_caches_when_maven_local_present() {
let temp_dir = TempDir::new().expect("Cant create temp dir");
let fake_maven_local_path = temp_dir.path();

fs::create_dir(temp_dir.path().join(".m2")).expect("Cant create temporary fixture folder");

for _ in 0..2 {
create_fake_1kb_file(&temp_dir, ".m2");
}

let usage = usage_for_maven_local(fake_maven_local_path).expect("Cannot compute use cases");

let use_case = UseCase::from(UserLevelDiskCache::MavenLocalRepository);
let expected = AllocatedResource::new(use_case, 2.kilobytes());
assert_eq!(usage, expected)
}

#[test]
fn should_not_compute_build_output_files_when_missing_gradle_projects() {
let temp_dir = TempDir::new().expect("Cant create temp dir");
let fake_android_studio_projects_path = vec![temp_dir.path().to_path_buf()];

let usage = usage_for_gradle_projects(&fake_android_studio_projects_path).expect("Cannot compute use cases");

let use_case = UseCase::from(ProjectLevelDiskCache::BuildOutput);
let expected = AllocatedResource::new(use_case, ByteUnit::from(0));
assert_eq!(usage, expected)
}

#[test]
fn should_compute_build_output_files_when_gradle_projects_present() {
let temp_dir = TempDir::new().expect("Cant create temp dir");

prepare_fake_gradle_projects(&temp_dir);

for _ in 0..5 {
create_fake_1kb_file(&temp_dir, "AndroidStudioProjects/my-project/build");
}

let fake_android_studio_projects_path = vec![temp_dir.path().to_path_buf()];

let usage = usage_for_gradle_projects(&fake_android_studio_projects_path).expect("Cannot compute use cases");

let use_case = UseCase::from(ProjectLevelDiskCache::BuildOutput);
let expected = AllocatedResource::new(use_case, 5.kilobytes());
assert_eq!(usage, expected)
}
}
mod locations;
mod operations;
mod resources;

pub use locations::find_all_gradle_projects;
pub use locations::find_gradle_home;
pub use locations::find_maven_local_repository;
pub use operations::cleanup;
pub use resources::resources_used_by_gradle_home;
pub use resources::resources_used_by_gradle_projects;
pub use resources::resources_used_by_maven_local_repository;
68 changes: 26 additions & 42 deletions src/filesystem.rs → src/disk/locations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,39 @@
use crate::models::DiskCached;
use anyhow::anyhow;
use directories::BaseDirs;
use std::fs;
use std::path::{Path, PathBuf};
use walkdir::{DirEntry, WalkDir};

pub fn perform_cleanup(resources: &[DiskCached]) {
let paths_to_remove = resources.iter().flat_map(find_paths).collect::<Vec<_>>();
pub fn find_gradle_home() -> anyhow::Result<PathBuf> {
let base_dir = BaseDirs::new().ok_or(anyhow!("Cannot access base directories"))?;
let home_dir = base_dir.home_dir();
Ok(home_dir.join(".gradle"))
}

let errors = paths_to_remove
.iter()
.map(fs::remove_dir_all)
.filter(|deletion| deletion.is_err())
.map(|deletion| deletion.expect_err("Expecting an error").to_string())
.collect::<Vec<String>>();
pub fn find_maven_local_repository() -> anyhow::Result<PathBuf> {
let base_dir = BaseDirs::new().ok_or(anyhow!("Cannot access base directories"))?;
let home_dir = base_dir.home_dir();
Ok(home_dir.join(".m2"))
}

for error in errors {
eprintln!("{}", error);
}
pub fn find_all_gradle_projects() -> anyhow::Result<Vec<PathBuf>> {
let base_dir = BaseDirs::new().ok_or(anyhow!("Cannot access base directories"))?;
let home_dir = base_dir.home_dir();

let android_studio_projects_folder = home_dir.join("AndroidStudioProjects");
let android_projects = find_gradle_projects(android_studio_projects_folder.as_path())?;

let intellij_projects_folder = home_dir.join("IdeaProjects");
let jvm_projects = find_gradle_projects(intellij_projects_folder.as_path())?;

let mut all_projects: Vec<PathBuf> = Vec::new();
all_projects.extend(android_projects);
all_projects.extend(jvm_projects);

Ok(all_projects)
}

fn find_paths(cached: &DiskCached) -> Vec<PathBuf> {
pub(crate) fn find_paths(cached: &DiskCached) -> Vec<PathBuf> {
let base_dir = BaseDirs::new().expect("Cannot access base directories");
let home_dir = base_dir.home_dir();

Expand Down Expand Up @@ -57,35 +70,6 @@ fn find_paths(cached: &DiskCached) -> Vec<PathBuf> {
}
}

pub fn find_gradle_home() -> anyhow::Result<PathBuf> {
let base_dir = BaseDirs::new().ok_or(anyhow!("Cannot access base directories"))?;
let home_dir = base_dir.home_dir();
Ok(home_dir.join(".gradle"))
}

pub fn find_maven_local_repository() -> anyhow::Result<PathBuf> {
let base_dir = BaseDirs::new().ok_or(anyhow!("Cannot access base directories"))?;
let home_dir = base_dir.home_dir();
Ok(home_dir.join(".m2"))
}

pub fn find_all_gradle_projects() -> anyhow::Result<Vec<PathBuf>> {
let base_dir = BaseDirs::new().ok_or(anyhow!("Cannot access base directories"))?;
let home_dir = base_dir.home_dir();

let android_studio_projects_folder = home_dir.join("AndroidStudioProjects");
let android_projects = find_gradle_projects(android_studio_projects_folder.as_path())?;

let intellij_projects_folder = home_dir.join("IdeaProjects");
let jvm_projects = find_gradle_projects(intellij_projects_folder.as_path())?;

let mut all_projects: Vec<PathBuf> = Vec::new();
all_projects.extend(android_projects);
all_projects.extend(jvm_projects);

Ok(all_projects)
}

fn find_gradle_projects(folder: &Path) -> anyhow::Result<Vec<PathBuf>> {
let projects = WalkDir::new(folder)
.into_iter()
Expand Down
Loading