mirror of
https://github.com/tw93/Mole.git
synced 2026-03-24 11:10:08 +00:00
feat(clean): add Group Containers logs/caches cleanup
Add clean_group_container_caches() to safely clean Group Containers: - Skip Apple-owned containers (com.apple.*, group.com.apple.*, systemgroup.com.apple.*) - For protected apps: only clean Logs directories - For non-protected apps: clean Logs, tmp, and Caches - Add symlink checks to prevent path traversal - Respect whitelist and should_protect_path checks - Integrate with clean_sandboxed_app_caches flow Also add symlink checks in process_container_cache() for consistency. Includes 4 BATS tests covering protected apps, whitelist, system containers, and empty results handling.
This commit is contained in:
@@ -436,11 +436,14 @@ clean_sandboxed_app_caches() {
|
|||||||
((total_items++))
|
((total_items++))
|
||||||
note_activity
|
note_activity
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
clean_group_container_caches
|
||||||
}
|
}
|
||||||
# Process a single container cache directory.
|
# Process a single container cache directory.
|
||||||
process_container_cache() {
|
process_container_cache() {
|
||||||
local container_dir="$1"
|
local container_dir="$1"
|
||||||
[[ -d "$container_dir" ]] || return 0
|
[[ -d "$container_dir" ]] || return 0
|
||||||
|
[[ -L "$container_dir" ]] && return 0
|
||||||
local bundle_id=$(basename "$container_dir")
|
local bundle_id=$(basename "$container_dir")
|
||||||
if is_critical_system_component "$bundle_id"; then
|
if is_critical_system_component "$bundle_id"; then
|
||||||
return 0
|
return 0
|
||||||
@@ -450,6 +453,7 @@ process_container_cache() {
|
|||||||
fi
|
fi
|
||||||
local cache_dir="$container_dir/Data/Library/Caches"
|
local cache_dir="$container_dir/Data/Library/Caches"
|
||||||
[[ -d "$cache_dir" ]] || return 0
|
[[ -d "$cache_dir" ]] || return 0
|
||||||
|
[[ -L "$cache_dir" ]] && return 0
|
||||||
# Fast non-empty check.
|
# Fast non-empty check.
|
||||||
if find "$cache_dir" -mindepth 1 -maxdepth 1 -print -quit 2> /dev/null | grep -q .; then
|
if find "$cache_dir" -mindepth 1 -maxdepth 1 -print -quit 2> /dev/null | grep -q .; then
|
||||||
local size=$(get_path_size_kb "$cache_dir")
|
local size=$(get_path_size_kb "$cache_dir")
|
||||||
@@ -472,6 +476,120 @@ process_container_cache() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Group Containers safe cleanup (logs for protected apps, caches/tmp for non-protected apps).
|
||||||
|
clean_group_container_caches() {
|
||||||
|
local group_containers_dir="$HOME/Library/Group Containers"
|
||||||
|
[[ -d "$group_containers_dir" ]] || return 0
|
||||||
|
if ! ls "$group_containers_dir" > /dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
start_section_spinner "Scanning Group Containers..."
|
||||||
|
local total_size=0
|
||||||
|
local cleaned_count=0
|
||||||
|
local found_any=false
|
||||||
|
local _ng_state
|
||||||
|
_ng_state=$(shopt -p nullglob || true)
|
||||||
|
shopt -s nullglob
|
||||||
|
|
||||||
|
local container_dir
|
||||||
|
for container_dir in "$group_containers_dir"/*; do
|
||||||
|
[[ -d "$container_dir" ]] || continue
|
||||||
|
[[ -L "$container_dir" ]] && continue
|
||||||
|
local container_id
|
||||||
|
container_id=$(basename "$container_dir")
|
||||||
|
|
||||||
|
# Skip Apple-owned shared containers entirely.
|
||||||
|
case "$container_id" in
|
||||||
|
com.apple.* | group.com.apple.* | systemgroup.com.apple.*)
|
||||||
|
continue
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
local normalized_id="$container_id"
|
||||||
|
[[ "$normalized_id" == group.* ]] && normalized_id="${normalized_id#group.}"
|
||||||
|
|
||||||
|
local protected_container=false
|
||||||
|
if should_protect_data "$container_id" || should_protect_data "$normalized_id"; then
|
||||||
|
protected_container=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
local -a candidates=(
|
||||||
|
"$container_dir/Logs"
|
||||||
|
"$container_dir/Library/Logs"
|
||||||
|
)
|
||||||
|
if [[ "$protected_container" != "true" ]]; then
|
||||||
|
candidates+=(
|
||||||
|
"$container_dir/tmp"
|
||||||
|
"$container_dir/Library/tmp"
|
||||||
|
"$container_dir/Caches"
|
||||||
|
"$container_dir/Library/Caches"
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
local candidate
|
||||||
|
for candidate in "${candidates[@]}"; do
|
||||||
|
[[ -d "$candidate" ]] || continue
|
||||||
|
[[ -L "$candidate" ]] && continue
|
||||||
|
if is_path_whitelisted "$candidate"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if ! find "$candidate" -mindepth 1 -maxdepth 1 -print -quit 2> /dev/null | grep -q .; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
local candidate_size_kb=0
|
||||||
|
local candidate_changed=false
|
||||||
|
local item
|
||||||
|
while IFS= read -r -d '' item; do
|
||||||
|
[[ -e "$item" ]] || continue
|
||||||
|
[[ -L "$item" ]] && continue
|
||||||
|
if should_protect_path "$item" || is_path_whitelisted "$item"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
local item_size_kb
|
||||||
|
item_size_kb=$(get_path_size_kb "$item")
|
||||||
|
[[ "$item_size_kb" =~ ^[0-9]+$ ]] || item_size_kb=0
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == "true" ]]; then
|
||||||
|
candidate_changed=true
|
||||||
|
((candidate_size_kb += item_size_kb))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if safe_remove "$item" true; then
|
||||||
|
candidate_changed=true
|
||||||
|
((candidate_size_kb += item_size_kb))
|
||||||
|
fi
|
||||||
|
done < <(command find "$candidate" -mindepth 1 -maxdepth 1 -print0 2> /dev/null || true)
|
||||||
|
|
||||||
|
if [[ "$candidate_changed" == "true" ]]; then
|
||||||
|
((total_size += candidate_size_kb))
|
||||||
|
((cleaned_count++))
|
||||||
|
found_any=true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
eval "$_ng_state"
|
||||||
|
stop_section_spinner
|
||||||
|
|
||||||
|
if [[ "$found_any" == "true" ]]; then
|
||||||
|
local size_human
|
||||||
|
size_human=$(bytes_to_human "$((total_size * 1024))")
|
||||||
|
if [[ "$DRY_RUN" == "true" ]]; then
|
||||||
|
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Group Containers logs/caches${NC}, ${YELLOW}$size_human dry${NC}"
|
||||||
|
else
|
||||||
|
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Group Containers logs/caches${NC}, ${GREEN}$size_human${NC}"
|
||||||
|
fi
|
||||||
|
((files_cleaned += cleaned_count))
|
||||||
|
((total_size_cleaned += total_size))
|
||||||
|
((total_items++))
|
||||||
|
note_activity
|
||||||
|
fi
|
||||||
|
}
|
||||||
# Browser caches (Safari/Chrome/Edge/Firefox).
|
# Browser caches (Safari/Chrome/Edge/Firefox).
|
||||||
clean_browsers() {
|
clean_browsers() {
|
||||||
safe_clean ~/Library/Caches/com.apple.Safari/* "Safari cache"
|
safe_clean ~/Library/Caches/com.apple.Safari/* "Safari cache"
|
||||||
@@ -755,6 +873,7 @@ check_large_file_candidates() {
|
|||||||
note_activity
|
note_activity
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# Apple Silicon specific caches (IS_M_SERIES).
|
# Apple Silicon specific caches (IS_M_SERIES).
|
||||||
clean_apple_silicon_caches() {
|
clean_apple_silicon_caches() {
|
||||||
if [[ "${IS_M_SERIES:-false}" != "true" ]]; then
|
if [[ "${IS_M_SERIES:-false}" != "true" ]]; then
|
||||||
|
|||||||
@@ -77,6 +77,144 @@ EOF
|
|||||||
[[ "$output" != *"Sandboxed app caches"* ]]
|
[[ "$output" != *"Sandboxed app caches"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test "clean_group_container_caches keeps protected caches and cleans non-protected caches" {
|
||||||
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" DRY_RUN=false /bin/bash --noprofile --norc <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
|
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||||
|
source "$PROJECT_ROOT/lib/clean/user.sh"
|
||||||
|
start_section_spinner() { :; }
|
||||||
|
stop_section_spinner() { :; }
|
||||||
|
bytes_to_human() { echo "0B"; }
|
||||||
|
note_activity() { :; }
|
||||||
|
files_cleaned=0
|
||||||
|
total_size_cleaned=0
|
||||||
|
total_items=0
|
||||||
|
|
||||||
|
mkdir -p "$HOME/Library/Group Containers/group.com.microsoft.teams/Library/Logs"
|
||||||
|
mkdir -p "$HOME/Library/Group Containers/group.com.microsoft.teams/Library/Caches"
|
||||||
|
mkdir -p "$HOME/Library/Group Containers/group.com.example.tool/Library/Caches"
|
||||||
|
echo "log" > "$HOME/Library/Group Containers/group.com.microsoft.teams/Library/Logs/log.txt"
|
||||||
|
echo "cache" > "$HOME/Library/Group Containers/group.com.microsoft.teams/Library/Caches/cache.db"
|
||||||
|
echo "cache" > "$HOME/Library/Group Containers/group.com.example.tool/Library/Caches/cache.db"
|
||||||
|
|
||||||
|
clean_group_container_caches
|
||||||
|
|
||||||
|
if [[ ! -e "$HOME/Library/Group Containers/group.com.microsoft.teams/Library/Logs/log.txt" ]] \
|
||||||
|
&& [[ -e "$HOME/Library/Group Containers/group.com.microsoft.teams/Library/Caches/cache.db" ]] \
|
||||||
|
&& [[ ! -e "$HOME/Library/Group Containers/group.com.example.tool/Library/Caches/cache.db" ]]; then
|
||||||
|
echo "PASS"
|
||||||
|
else
|
||||||
|
echo "FAIL"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
EOF
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"Group Containers logs/caches"* ]]
|
||||||
|
[[ "$output" == *"PASS"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "clean_group_container_caches respects whitelist entries" {
|
||||||
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" DRY_RUN=false /bin/bash --noprofile --norc <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
|
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||||
|
source "$PROJECT_ROOT/lib/clean/user.sh"
|
||||||
|
start_section_spinner() { :; }
|
||||||
|
stop_section_spinner() { :; }
|
||||||
|
bytes_to_human() { echo "0B"; }
|
||||||
|
note_activity() { :; }
|
||||||
|
files_cleaned=0
|
||||||
|
total_size_cleaned=0
|
||||||
|
total_items=0
|
||||||
|
|
||||||
|
mkdir -p "$HOME/Library/Group Containers/group.com.example.tool/Library/Caches"
|
||||||
|
echo "protected" > "$HOME/Library/Group Containers/group.com.example.tool/Library/Caches/keep.db"
|
||||||
|
echo "remove" > "$HOME/Library/Group Containers/group.com.example.tool/Library/Caches/drop.db"
|
||||||
|
|
||||||
|
is_path_whitelisted() {
|
||||||
|
[[ "$1" == *"/group.com.example.tool/Library/Caches/keep.db" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
clean_group_container_caches
|
||||||
|
|
||||||
|
if [[ -e "$HOME/Library/Group Containers/group.com.example.tool/Library/Caches/keep.db" ]] \
|
||||||
|
&& [[ ! -e "$HOME/Library/Group Containers/group.com.example.tool/Library/Caches/drop.db" ]]; then
|
||||||
|
echo "PASS"
|
||||||
|
else
|
||||||
|
echo "FAIL"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
EOF
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"PASS"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "clean_group_container_caches skips systemgroup apple containers" {
|
||||||
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" DRY_RUN=false /bin/bash --noprofile --norc <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
|
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||||
|
source "$PROJECT_ROOT/lib/clean/user.sh"
|
||||||
|
start_section_spinner() { :; }
|
||||||
|
stop_section_spinner() { :; }
|
||||||
|
bytes_to_human() { echo "0B"; }
|
||||||
|
note_activity() { :; }
|
||||||
|
files_cleaned=0
|
||||||
|
total_size_cleaned=0
|
||||||
|
total_items=0
|
||||||
|
|
||||||
|
mkdir -p "$HOME/Library/Group Containers/systemgroup.com.apple.example/Library/Caches"
|
||||||
|
echo "system-data" > "$HOME/Library/Group Containers/systemgroup.com.apple.example/Library/Caches/cache.db"
|
||||||
|
|
||||||
|
clean_group_container_caches
|
||||||
|
|
||||||
|
if [[ -e "$HOME/Library/Group Containers/systemgroup.com.apple.example/Library/Caches/cache.db" ]]; then
|
||||||
|
echo "PASS"
|
||||||
|
else
|
||||||
|
echo "FAIL"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
EOF
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"PASS"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "clean_group_container_caches does not report when only whitelisted items exist" {
|
||||||
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" DRY_RUN=false /bin/bash --noprofile --norc <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
|
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||||
|
source "$PROJECT_ROOT/lib/clean/user.sh"
|
||||||
|
start_section_spinner() { :; }
|
||||||
|
stop_section_spinner() { :; }
|
||||||
|
bytes_to_human() { echo "0B"; }
|
||||||
|
note_activity() { :; }
|
||||||
|
files_cleaned=0
|
||||||
|
total_size_cleaned=0
|
||||||
|
total_items=0
|
||||||
|
|
||||||
|
mkdir -p "$HOME/Library/Group Containers/group.com.example.onlywhite/Library/Caches"
|
||||||
|
echo "whitelisted" > "$HOME/Library/Group Containers/group.com.example.onlywhite/Library/Caches/keep.db"
|
||||||
|
|
||||||
|
is_path_whitelisted() {
|
||||||
|
[[ "$1" == *"/group.com.example.onlywhite/Library/Caches/keep.db" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
clean_group_container_caches
|
||||||
|
|
||||||
|
if [[ -e "$HOME/Library/Group Containers/group.com.example.onlywhite/Library/Caches/keep.db" ]]; then
|
||||||
|
echo "PASS"
|
||||||
|
else
|
||||||
|
echo "FAIL"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
EOF
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"PASS"* ]]
|
||||||
|
[[ "$output" != *"Group Containers logs/caches"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
@test "clean_finder_metadata respects protection flag" {
|
@test "clean_finder_metadata respects protection flag" {
|
||||||
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" PROTECT_FINDER_METADATA=true /bin/bash --noprofile --norc <<'EOF'
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" PROTECT_FINDER_METADATA=true /bin/bash --noprofile --norc <<'EOF'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -104,11 +242,15 @@ EOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
@test "clean_browsers calls expected cache paths" {
|
@test "clean_browsers calls expected cache paths" {
|
||||||
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" DRY_RUN=true bash --noprofile --norc <<'EOF'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$PROJECT_ROOT/lib/core/common.sh"
|
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||||
source "$PROJECT_ROOT/lib/clean/user.sh"
|
source "$PROJECT_ROOT/lib/clean/user.sh"
|
||||||
safe_clean() { echo "$2"; }
|
safe_clean() { echo "$2"; }
|
||||||
|
note_activity() { :; }
|
||||||
|
files_cleaned=0
|
||||||
|
total_size_cleaned=0
|
||||||
|
total_items=0
|
||||||
clean_browsers
|
clean_browsers
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user