• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bleachbit / bleachbit / 25300670692

04 May 2026 03:50AM UTC coverage: 74.001% (+0.2%) from 73.851%
25300670692

push

github

az0
Clean node_modules directories

Node.js/npm package dependencies

7392 of 9989 relevant lines covered (74.0%)

0.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.31
/bleachbit/DeepScan.py
1
# vim: ts=4:sw=4:expandtab
2
# -*- coding: UTF-8 -*-
3

4
# BleachBit
5
# Copyright (C) 2008-2025 Andrew Ziem
6
# https://www.bleachbit.org
7
#
8
# This program is free software: you can redistribute it and/or modify
9
# it under the terms of the GNU General Public License as published by
10
# the Free Software Foundation, either version 3 of the License, or
11
# (at your option) any later version.
12
#
13
# This program is distributed in the hope that it will be useful,
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
# GNU General Public License for more details.
17
#
18
# You should have received a copy of the GNU General Public License
19
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
20

21

22
"""
23
Scan directory tree for files to delete
24
"""
25

26
import logging
1✔
27
import os
1✔
28
import platform
1✔
29
import re
1✔
30
import time
1✔
31
import unicodedata
1✔
32
from collections import namedtuple
1✔
33
from bleachbit import FS_SCAN_RE_FLAGS
1✔
34
from . import Command
1✔
35
from .FileUtilities import whitelisted
1✔
36

37

38
def normalized_walk(top, **kwargs):
1✔
39
    """
40
    macOS uses decomposed UTF-8 to store filenames. This functions
41
    is like `os.walk` but recomposes those decomposed filenames on
42
    macOS
43
    """
44
    try:
1✔
45
        from scandir import walk
1✔
46
    except:
1✔
47
        # there is a warning in FileUtilities, so don't warn again here
48
        from os import walk
1✔
49
    if 'Darwin' == platform.system():
1✔
50
        for dirpath, dirnames, filenames in walk(top, **kwargs):
×
51
            yield dirpath, dirnames, [
×
52
                unicodedata.normalize('NFC', fn)
53
                for fn in filenames
54
            ]
55
    else:
56
        yield from walk(top, **kwargs)
1✔
57

58

59
Search = namedtuple(
1✔
60
    'Search', ['command', 'regex', 'nregex', 'wholeregex', 'nwholeregex'])
61
Search.__new__.__defaults__ = (None,) * len(Search._fields)
1✔
62

63

64
class CompiledSearch:
1✔
65
    """Compiled search condition"""
66

67
    def __init__(self, search):
1✔
68
        self.command = search.command
1✔
69

70
        def re_compile(regex):
1✔
71
            return re.compile(regex, FS_SCAN_RE_FLAGS) if regex else None
1✔
72

73
        self.regex = re_compile(search.regex)
1✔
74
        self.nregex = re_compile(search.nregex)
1✔
75
        self.wholeregex = re_compile(search.wholeregex)
1✔
76
        self.nwholeregex = re_compile(search.nwholeregex)
1✔
77

78
    def match(self, dirpath, filename):
1✔
79
        full_path = os.path.join(dirpath, filename)
1✔
80

81
        if self.regex and not self.regex.search(filename):
1✔
82
            return None
1✔
83

84
        if self.nregex and self.nregex.search(filename):
1✔
85
            return None
×
86

87
        if self.wholeregex and not self.wholeregex.search(full_path):
1✔
88
            return None
1✔
89

90
        if self.nwholeregex and self.nwholeregex.search(full_path):
1✔
91
            return None
×
92

93
        return full_path
1✔
94

95

96
class DeepScan:
1✔
97

98
    """Advanced directory tree scan"""
99

100
    def __init__(self, searches):
1✔
101
        self.roots = []
1✔
102
        self.searches = searches
1✔
103

104
    def scan(self):
1✔
105
        """Perform requested searches and yield each match"""
106
        logging.getLogger(__name__).debug(
1✔
107
            'DeepScan.scan: searches=%s', str(self.searches))
108
        yield_time = time.time()
1✔
109

110
        for (top, searches) in self.searches.items():
1✔
111
            # This skips top-level directories that are in the keep list
112
            # to reduce unnecessary work.
113
            if whitelisted(top):
1✔
114
                continue
×
115
            compiled_searches = [CompiledSearch(s) for s in searches]
1✔
116
            for (dirpath, dirnames, filenames) in normalized_walk(top):
1✔
117
                # This filters out subdirectories that are in the keep list
118
                # to reduce unnecessary work.
119
                dirnames[:] = [
1✔
120
                    dirname
121
                    for dirname in dirnames
122
                    if not whitelisted(os.path.join(dirpath, dirname))
123
                ]
124
                for c in compiled_searches:
1✔
125
                    # fixme, don't match filename twice
126
                    for filename in filenames:
1✔
127
                        full_name = c.match(dirpath, filename)
1✔
128
                        if full_name is not None:
1✔
129
                            # fixme: support other commands
130
                            if c.command == 'delete':
1✔
131
                                yield Command.Delete(full_name)
1✔
132
                            elif c.command == 'shred':
1✔
133
                                yield Command.Shred(full_name)
1✔
134

135
                if time.time() - yield_time > 0.25:
1✔
136
                    # allow GTK+ to process the idle loop
137
                    yield True
1✔
138
                    yield_time = time.time()
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc