Files
2026-10-28 10:11:18 +01:00

298 lines
12 KiB
Python

# -*- coding: utf-8 -*-
"""Interactive WAPT update_package for multiple cleanmgr profiles.
Uses the same WAPT Console conventions as Tranquil IT's
``tis-waptagent-logo-template``: waptguihelper dialogs, control.inc_build(),
and control.save_control_to_wapt(). The names ask_control_version() and
ask_control_package() in that template are local helpers, not WAPT APIs.
"""
from setuphelpers import *
import json
import os
import re
MAX_PROFILES = 50
# Stable VolumeCaches registry subkey names, NOT translated Windows labels.
# Windows builds expose different subsets of this list.
CATALOG = [
('Temporary Files', 'Fichiers temporaires', 'Standard'),
('Thumbnail Cache', 'Miniatures', 'Standard'),
('D3D Shader Cache', 'Cache des shaders Direct3D', 'Standard'),
('Delivery Optimization Files', 'Optimisation de la distribution', 'Standard'),
('Downloaded Program Files', 'Programmes telecharges', 'Standard'),
('Internet Cache Files', 'Cache Internet de Windows', 'Standard'),
('Windows Defender', 'Fichiers non essentiels Defender', 'Maintenance'),
('Windows Error Reporting Files', 'Rapports erreurs Windows', 'Maintenance'),
('Setup Log Files', 'Journaux installation Windows', 'Maintenance'),
('Windows Upgrade Log Files', 'Journaux mises a niveau Windows', 'Maintenance'),
('Temporary Setup Files', 'Fichiers installation temporaires', 'Maintenance'),
('Old ChkDsk Files', 'Fragments CHKDSK anciens', 'Maintenance'),
('System error memory dump files', 'Dump memoire systeme', 'Diagnostic'),
('System error minidump files', 'Mini dumps systeme', 'Diagnostic'),
('Memory Dump Files', 'Autres dumps memoire', 'Diagnostic'),
('Update Cleanup', 'Nettoyage Windows Update / rollback', 'Sensible'),
('Previous Installations', 'Anciennes installations Windows / rollback', 'Sensible'),
('Recycle Bin', 'Corbeille', 'Sensible'),
('Windows ESD installation files', 'Fichiers de reinitialisation ESD', 'Sensible'),
('Device Driver Packages', 'Anciens packages pilotes', 'Sensible'),
]
PROFILE_META = json.dumps({'columns': [
{'propertyname': 'id', 'datatype': 'Integer'},
{'propertyname': 'name', 'datatype': 'String'},
]})
CATEGORY_META = json.dumps({'columns': [
{'propertyname': 'key', 'datatype': 'String'},
{'propertyname': 'description', 'datatype': 'String'},
{'propertyname': 'risk', 'datatype': 'String'},
{'propertyname': 'enabled', 'datatype': 'Boolean'},
]})
DEFAULT_SAFE_CATEGORIES = [
'Temporary Files', 'Thumbnail Cache',
'D3D Shader Cache', 'Delivery Optimization Files',
]
def _load_config(path):
if not os.path.isfile(path):
return {'schema_version': 1, 'profiles': []}
with open(path, 'r', encoding='utf-8') as stream:
content = json.load(stream)
if not isinstance(content, dict) or content.get('schema_version') != 1:
raise ValueError('profiles.json : schema_version invalide.')
if not isinstance(content.get('profiles'), list):
raise ValueError('profiles.json : profiles doit etre une liste.')
return content
def _as_rows(result, dialog_name):
if result is None:
return None
if isinstance(result, (str, bytes)):
result = json.loads(result)
if not isinstance(result, list) or any(not isinstance(row, dict) for row in result):
raise ValueError('%s : resultat de grille invalide.' % dialog_name)
return result
def _valid_key(key):
return isinstance(key, str) and bool(re.fullmatch(
r'[A-Za-z0-9][A-Za-z0-9 ()_.-]{0,127}', key))
def _normalize_id(value):
if isinstance(value, bool):
raise ValueError('ID Cleanmgr invalide : %r' % value)
if isinstance(value, int):
profile_id = value
elif isinstance(value, str) and value.strip().isdigit():
profile_id = int(value.strip())
else:
raise ValueError('ID Cleanmgr invalide : %r (nombre entier attendu)' % value)
if not 0 <= profile_id <= 9999:
raise ValueError('ID Cleanmgr hors plage 0..9999 : %s' % profile_id)
return profile_id
def _enabled(value):
if isinstance(value, bool):
return value
if isinstance(value, (int, float)):
return value != 0
return isinstance(value, str) and value.strip().lower() in (
'true', 'yes', 'oui', '1')
def _next_id(used):
# Keeps IDs together without overwriting existing IDs already in use.
for candidate in list(range(4101, 10000)) + list(range(0, 4101)):
if candidate not in used:
used.add(candidate)
return candidate
raise ValueError('Plus aucun identifiant Cleanmgr disponible.')
def _profile_rows(old_profiles, count):
rows = [{'id': profile['id'], 'name': profile['name']}
for profile in old_profiles[:count]]
used = {row['id'] for row in rows}
while len(rows) < count:
profile_id = _next_id(used)
rows.append({'id': profile_id, 'name': 'Profil %s' % profile_id})
return rows
def _validate_profile_rows(rows, count):
if len(rows) != count:
raise ValueError('La grille doit contenir exactement %d profils.' % count)
profiles = []
used = set()
for row in rows:
profile_id = _normalize_id(row.get('id'))
if profile_id in used:
raise ValueError('Identifiant Cleanmgr duplique : %d.' % profile_id)
used.add(profile_id)
name = row.get('name')
if not isinstance(name, str) or not 1 <= len(name.strip()) <= 80:
raise ValueError('Le profil %d doit avoir un nom (1 a 80 caracteres).' % profile_id)
profiles.append({'id': profile_id, 'name': name.strip()})
return profiles
def _category_rows(selected):
keys = {key for key, _, _ in CATALOG}
rows = [
{'key': key, 'description': label, 'risk': risk, 'enabled': key in selected}
for key, label, risk in CATALOG
]
for key in sorted(selected - keys):
rows.append({'key': key, 'description': 'Categorie personnalisee existante',
'risk': 'A verifier', 'enabled': True})
return rows
def _parse_categories(rows, allowed_keys):
if rows is None:
return None
if len(rows) != len(allowed_keys):
raise ValueError('Le nombre de categories de la grille a ete modifie.')
result = []
seen = set()
for row in rows:
key = row.get('key')
if key not in allowed_keys or key in seen:
raise ValueError('Cle VolumeCaches inconnue, modifiee ou dupliquee : %r' % key)
seen.add(key)
if _enabled(row.get('enabled')):
result.append(key)
return result
def _write_config(path, config):
temporary = path + '.tmp'
try:
with open(temporary, 'w', encoding='utf-8') as stream:
json.dump(config, stream, indent=2, ensure_ascii=False)
stream.write('\n')
os.replace(temporary, path)
finally:
if os.path.exists(temporary):
os.remove(temporary)
def update_package():
import waptguihelper
# Based on official Store template: LUTI/headless builds must avoid GUI prompts.
current_params = globals().get('params', {}) or {}
if current_params.get('install_with_luti') or current_params.get('running_as_luti'):
print('Build LUTI : conservation de profiles.json sans intervention graphique.')
return 'OK'
config_path = makepath(basedir, 'profiles.json')
existing_config = _load_config(config_path)
old_profiles = existing_config['profiles']
waptguihelper.message_dialog(
'Comitari - Profils Cleanmgr',
'Cette action permet de definir plusieurs profils de nettoyage Windows.\n'
'Pour chaque profil, choisissez un identifiant, un nom et les categories.\n'
'L installation configure les profils mais NE LANCE PAS le nettoyage.\n'
'Les categories disponibles dependent de la version de Windows.',
0 + 64 # MB_OK + MB_ICONINFORMATION (comme le template officiel)
)
count_str = waptguihelper.input_dialog(
'Comitari - Profils Cleanmgr',
'Nombre de profils a configurer (1 a %d) :' % MAX_PROFILES,
str(len(old_profiles) or 2))
if count_str is None:
print('Configuration annulee (aucun fichier modifie).')
return False
try:
count = int(count_str.strip())
except (TypeError, ValueError):
raise ValueError('Veuillez saisir un nombre entier de profils.')
if not 1 <= count <= MAX_PROFILES:
raise ValueError('Le nombre de profils doit etre entre 1 et %d.' % MAX_PROFILES)
# WAPT native editable grid: one dialog for N names and IDs instead of 2*N dialogs.
rows = _as_rows(waptguihelper.grid_dialog(
'Profils Cleanmgr : modifiez les identifiants et noms',
_profile_rows(old_profiles, count),
waptguihelper.GRT_ALL,
PROFILE_META), 'Profils Cleanmgr')
if rows is None:
return False
new_profiles = _validate_profile_rows(rows, count)
by_id = {p['id']: p for p in old_profiles}
for index, profile in enumerate(new_profiles):
# When an ID is changed, retain selections from its previous row.
prior = by_id.get(profile['id'])
if prior is None and index < len(old_profiles):
prior = old_profiles[index]
previous = set(prior.get('categories', [])) if prior else set(DEFAULT_SAFE_CATEGORIES)
possible = _category_rows(previous)
possible_keys = {row['key'] for row in possible}
title = 'Profil %d/%d : %d - %s' % (
index + 1, count, profile['id'], profile['name'])
returned = _as_rows(waptguihelper.grid_dialog(
title + ' - cochez les categories',
possible,
waptguihelper.GRT_ALL,
CATEGORY_META), 'Categories Cleanmgr')
if returned is None:
return False
categories = _parse_categories(returned, possible_keys)
# Add optional custom registry subkeys, when needed on specific Windows versions.
custom = waptguihelper.input_dialog(
title, 'Autres sous-cles VolumeCaches a ajouter (virgules, optionnel) :', '')
if custom is None:
return False
for candidate in custom.split(','):
category = candidate.strip()
if category and category not in categories:
categories.append(category)
if not categories:
raise ValueError('Le profil %d doit contenir au moins une categorie.' % profile['id'])
if len(categories) > 200 or any(not _valid_key(key) for key in categories):
raise ValueError('Categorie VolumeCaches invalide ou liste trop longue.')
profile['categories'] = categories
overview = '\n'.join('%d - %s : %d categories' % (
p['id'], p['name'], len(p['categories'])) for p in new_profiles)
risky = [p['name'] for p in new_profiles if
set(p['categories']) & {'Previous Installations', 'Recycle Bin',
'Windows ESD installation files', 'Update Cleanup'}]
if risky:
overview += ('\n\nATTENTION : categories sensibles dans %s.\n'
'Certaines suppressions empechent un retour a la version '
'precedente de Windows ou vident la corbeille.' % ', '.join(risky))
overview += '\n\nEnregistrer les profils dans ce paquet ?'
confirmed = waptguihelper.message_dialog(
'Validation des profils', overview,
4 + 32) # MB_YESNO + MB_ICONQUESTION
if confirmed != waptguihelper.ID_YES:
print('Configuration non validee (aucun fichier modifie).')
return False
new_config = {'schema_version': 1, 'profiles': new_profiles}
if new_config == existing_config:
waptguihelper.message_dialog(
'Profils inchanges', 'Aucun changement detecte dans profiles.json.', 0 + 64)
return False
# WAPT-native control API instead of manually parsing the version string.
_write_config(config_path, new_config)
control.inc_build()
control.save_control_to_wapt()
print('Profils enregistres : %s (%d profils).' % (config_path, len(new_profiles)))
print('Version du paquet WAPT : %s' % control.version)
return True