# Drupal Configuration Selector A Python utility to selectively copy configuration files from a source directory to a target directory based on file prefixes. ## Overview This script helps you manage and transfer specific Drupal configuration files by: - Reading a list of configuration file prefixes from a JSON file - Identifying matching files in a source directory - Copying selected files to a target directory - Providing a TUI (Text User Interface) to manage the selection process ## Requirements - Python 3.6+ - curses library (built into most Python installations on Linux/Mac) - PyYAML library (install with: `pip install pyyaml`) ## Installation and Setup The script operates in the current working directory where you run it. It will create the following subdirectories if they don't exist: - `ingest/` - Drop your exported Drupal config archive here (named `config*.tar.gz`) - `original_config/` - YAML files are extracted here automatically from the ingest archive - `new_recipe_config/` - Matched files will be copied here (with UUID and _core removed) - `changed_files/` - Files that changed between versions will be placed here - `ignored_files.yml` - Optional: List files to exclude from change tracking - `deleted_files.csv` - Tracks files that should be deleted ## Usage ### Ingest: export from Drupal into `ingest/` 1. In the Drupal admin UI, open the **full configuration export** page: **Configuration → Development → Configuration synchronization → Export**, then use the **Full archive** export (direct URL path: `admin/config/development/configuration/full/export`). 2. Download the generated archive (a `.tar.gz` of all site configuration). 3. Place that file in the `ingest/` directory under this tool’s working directory. The filename must match `config*.tar.gz` (for example `config.tar.gz` as downloaded, or a descriptive name like `config-localhost_3001-2026-04-09.tar.gz`). 4. Run `python3 config_selector.py` (or `python3 config_selector.py --force` if `original_config/` already has files and you want to replace them from the new archive). ### Standard run (skip extraction if original_config/ already has files) ```bash python3 config_selector.py ``` ### Force re-extraction from the ingest archive Use `--force` when you have placed a new archive in `ingest/` and want to replace the current `original_config/` contents: ```bash python3 config_selector.py --force ``` ### Workflow 1. Follow **Ingest** above to place a full export `config*.tar.gz` in `ingest/`. 2. Run the script. It extracts the archive into `original_config/` automatically (skipped on subsequent runs unless `--force` is passed). 3. The script will create a default `config_prefixes.json` in the current directory if it doesn't exist. 4. Use the TUI to manage your file selections and copy files. ## Hierarchical Navigation The script organizes configuration files in a tree-like structure based on dot-separated prefixes: - Files like `field.storage.node.comment.yml` are split into segments: `field` > `field.storage` > `field.storage.node` > etc. - Use **RIGHT ARROW** to navigate deeper into a selected prefix - Use **LEFT ARROW** to go back up one level in the hierarchy - At each level, you see only the unique segments available at that level - Each segment shows the total count of files under it - Segments with children have a "▶" indicator - You can perform actions (add, delete, select files) at any level in the hierarchy ## TUI Navigation The Text User Interface provides the following controls: - **UP/DOWN**: Navigate through the list of prefixes - **LEFT/RIGHT**: Navigate through the hierarchy tree (drill down/go back) - **SPACE**: Toggle between matched and unmatched prefixes view - **ENTER**: Select individual files from the selected prefix - **A**: Add a new prefix (press ESC or Enter with empty input to cancel) - **O**: Choose a prefix from the list of unmatched prefixes - **D**: Delete the selected prefix (in matched view) or add the selected prefix (in unmatched view) - **C**: Copy all matched files to the new_recipe_config directory (with UUID and _core removal) - **M**: Compare committed recipe vs `new_recipe_config`, copy new/modified to `changed_files/`, list deletions - **S**: Save the current list of prefixes to the JSON file - **Q**: Quit the application ## Individual File Selection When you press **ENTER** on a prefix, a dialog opens that allows you to: - In matched view: Select individual files to remove from the matching set - In unmatched view: Select individual files to add to a matching prefix Within the file selection dialog: - Use **UP/DOWN** to navigate between files - Press **SPACE** to toggle selection of a file - Press **ENTER** to confirm your selection - Press **ESC** to cancel ## Configuration File The script uses a JSON file to store prefixes. The default file is created at first run: ```json { "prefixes": [ "put.your.prefixes.here" ] } ``` You can modify this file directly or use the TUI to manage the prefixes. ## File Comparison and Change Tracking The script can compare configuration files between recipe versions: - **M**: Compare committed recipe to `new_recipe_config/` - Baseline: `recipes/wisski_default_data_model/config/` (committed recipe) - **New** files: present in `new_recipe_config/` but not in the committed recipe → copied to `changed_files/` (same layout, including `language//…`) - **Modified** files: same relative path in both, different content → copied to `changed_files/` - **Deleted** files: present in the committed recipe but missing from `new_recipe_config/` → listed in `deleted_files.csv` - Each compare run **clears** `changed_files/` first, then repopulates it - `changed_files_manifest.csv` lists every copied path with `kind` = `new` or `modified` Files matched in `ignored_files.yml` are not copied to `changed_files/` (whether new or modified). This feature helps you track additions, updates, and removals relative to the committed recipe. ## Ignoring Files from Change Tracking You can create an `ignored_files.yml` file to prevent specific configuration files from being copied to the `changed_files/` folder during comparison (for both **new** and **modified** paths). This is useful when certain files have intentional differences or should not be promoted from the export. Example `ignored_files.yml`: ```yaml field: name: field.field.wisski_individual.bb48a22d36f1c15cd16b143d23466812.field__vf__title reason: because we need target id instead of uuid another_category: name: some.other.config.file.yml reason: custom modification required ``` Files listed in this YAML will be excluded from the `changed_files/` directory even if they are new or changed relative to the committed recipe. ## Directory Structure ``` /your/working/directory/ ├── config_selector.py # Main script ├── config_prefixes.json # Configuration prefixes list ├── ignored_files.yml # Optional: List of files to ignore from change tracking ├── ingest/ # Drop config*.tar.gz archives here ├── original_config/ # Extracted source YAML files (auto-populated from ingest/) ├── new_recipe_config/ # Target directory where matched files will be copied (cleaned) ├── recipes/wisski_default_data_model/config/ # Committed recipe config (used as baseline for comparison) ├── changed_files/ # New + modified YAML vs committed recipe (cleared each compare) ├── changed_files_manifest.csv # relative_path + kind (new|modified) for changed_files/ ├── new_recipe_not_in_original.csv # Paths in new_recipe_config missing from original export └── deleted_files.csv # Paths in committed recipe missing from new_recipe_config ```