├── .gitignore ├── README.md ├── analyze_process_monitor_events.py ├── bleachbit.svg ├── build_web_download.py ├── credit_translations.py ├── debuild_bleachbit.sh ├── diff_msgid.sh ├── duplicate_cleaner_scan.sh ├── extract_desktop.py ├── gource ├── 1-caption.py ├── 1-email.sh ├── 1-log.sh ├── 2-avatar.py ├── 3-raw-video.sh ├── 4-final-video.sh ├── README.md ├── caption-yymmdd.csv └── caption.psv ├── httpmail ├── LICENSE.txt ├── README ├── httpmail.info ├── httpmail.install └── httpmail.module ├── iso639_langcodes.py ├── list_cleaners.py ├── lp2git.py ├── opensuse_build_service_meta.xml ├── purge_old_ci_builds.py ├── read_windows_exe_metadata.py ├── sdist.sh ├── summarize_translations.py ├── summarize_translations.sh ├── svg2ico.sh ├── svn2git.sh └── test_check_update.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bleachbit-misc 2 | These are files related to the development of BleachBit, but they are not required by most users. 3 | 4 | ## Running from code 5 | For general BleachBit development information, see [BleachBit: running from code](https://docs.bleachbit.org/dev/running-from-source-code.html). 6 | 7 | ## Related repositories 8 | * [BleachBit, the application](https://github.com/bleachbit/bleachbit) 9 | * [BleachBit documentation](https://github.com/bleachbit/docs) 10 | * [CleanerML, cleaners](https://github.com/az0/cleanerml) 11 | * [Winapp2.ini Repository](https://github.com/bleachbit/winapp2.ini) 12 | -------------------------------------------------------------------------------- /analyze_process_monitor_events.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # vim: ts=4:sw=4:expandtab 3 | 4 | # Copyright (C) 2016 by Andrew Ziem. All rights reserved. 5 | # License GPLv3+: GNU GPL version 3 or later . 6 | # This is free software: you are free to change and redistribute it. 7 | # There is NO WARRANTY, to the extent permitted by law. 8 | # 9 | # 10 | 11 | """ 12 | 13 | BleachBit's Windows build some files that are unnecessary to the application, 14 | so this program helps analyze which files are actually needed to inform a 15 | delete-list or whitelist. 16 | 17 | Instructions: 18 | 19 | 1. Build BleachBit without deleting any of the files from the runtime, 20 | or at least be sure not to delete any important files. 21 | 22 | 2. Run Microsoft Process Monitor and set up a filter. 23 | 24 | 3. Run BleachBit, and exercise all its functions. 25 | 26 | 4. From Process Monitor save the events to a CSV file. 27 | 28 | 5. Run this program like follows. 29 | 30 | python3 analyze_process_monitor_events.py event_log.csv C:\\Users\\username\\Desktop\\BleachBit-portable\\ ~/bleachbit/dist 31 | 32 | The arguments are 33 | 1. The trace file 34 | 2. The base directory of the BleachBit application when it was being traced 35 | 3. The directory containing the BleachBit files for comparison 36 | 37 | Notice the Windows path has double slashes, and notice by the third argument 38 | that this program can run on Unix-style systems. 39 | 40 | """ 41 | 42 | 43 | 44 | def list_to_file(l, fn): 45 | """Write the list to a file""" 46 | with open(fn, 'w') as f: 47 | for item in sorted(l): 48 | f.write('%s\n' % item) 49 | 50 | def clean_filename(fn, base_path): 51 | """Standardize the filename""" 52 | ret = fn 53 | if '/' == fn[0]: 54 | # running on Unix-like system 55 | ret = ret.replace('/', '\\') 56 | 57 | ret = ret.lower() # make lowercase 58 | ret = ret.replace(base_path.lower(), '') # strip off the base path 59 | ret = ret.replace(base_path.replace('/', '\\').lower(), '') # Unix variation 60 | if ret.endswith(r'\*'): 61 | # remove wildcard seen with QueryDirectory 62 | ret = ret[:-1] 63 | 64 | return ret 65 | 66 | def is_filtered(path): 67 | if path.lower().startswith('hk'): 68 | # registry keys: HKCU, HKLM 69 | return True 70 | if path.lower().startswith('c:\\windows\\'): 71 | return True 72 | return False 73 | 74 | def get_whitelist(csv_path, base_path): 75 | whitelist = set() 76 | 77 | import csv 78 | with open(csv_path, newline='') as f: 79 | reader = csv.reader(f) 80 | for row in reader: 81 | if not 'SUCCESS' == row[5]: 82 | continue 83 | fn_raw = row[4] 84 | if is_filtered(fn_raw): 85 | continue 86 | fn_clean = clean_filename(fn_raw, base_path) 87 | whitelist.add(fn_clean) 88 | return whitelist 89 | 90 | 91 | 92 | def walk_compare_path(compare_path, whitelist): 93 | keep = set() 94 | remove = set() 95 | 96 | import os 97 | for root, dirs, files in os.walk(compare_path, topdown=False): 98 | for name in files: 99 | full_path = os.path.join(root, name) 100 | cleaned_path = clean_filename(full_path, compare_path) 101 | if cleaned_path in whitelist: 102 | keep.add(cleaned_path) 103 | else: 104 | remove.add(cleaned_path) 105 | return (keep, remove) 106 | 107 | def go(): 108 | import argparse 109 | parser = argparse.ArgumentParser() 110 | parser.add_argument('csv_path') 111 | parser.add_argument('base_path') 112 | parser.add_argument('compare_path') 113 | args = parser.parse_args() 114 | 115 | whitelist = get_whitelist(args.csv_path, args.base_path) 116 | list_to_file(whitelist, 'whitelist.txt') 117 | 118 | (keep, remove) = walk_compare_path(args.compare_path, whitelist) 119 | list_to_file(keep, 'keep.txt') 120 | list_to_file(remove, 'remove.txt') 121 | 122 | 123 | go() 124 | -------------------------------------------------------------------------------- /bleachbit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 30 | 39 | 50 | 59 | 70 | 79 | 88 | 98 | 100 | 104 | 108 | 109 | 111 | 115 | 119 | 120 | 123 | 127 | 131 | 132 | 134 | 138 | 142 | 143 | 146 | 150 | 154 | 155 | 158 | 162 | 166 | 167 | 169 | 173 | 177 | 178 | 180 | 184 | 188 | 192 | 196 | 200 | 201 | 211 | 213 | 217 | 221 | 225 | 226 | 237 | 240 | 244 | 248 | 249 | 260 | 267 | 276 | 287 | 297 | 307 | 317 | 327 | 337 | 347 | 357 | 367 | 370 | 374 | 378 | 379 | 381 | 385 | 389 | 390 | 392 | 396 | 400 | 404 | 405 | 416 | 418 | 422 | 426 | 427 | 429 | 433 | 437 | 441 | 442 | 444 | 448 | 452 | 456 | 457 | 460 | 464 | 468 | 469 | 476 | 477 | 499 | 501 | 502 | 504 | image/svg+xml 505 | 507 | 508 | 509 | 510 | 511 | 516 | 520 | 523 | 530 | 536 | 542 | 543 | 549 | 555 | 561 | 568 | 574 | 580 | 585 | 595 | 605 | 611 | 616 | 621 | 626 | 631 | 636 | 641 | 646 | 651 | 656 | 661 | 666 | 671 | 681 | 682 | 686 | 696 | 702 | 708 | 714 | 720 | 726 | 732 | 738 | 744 | 750 | 756 | 762 | 772 | 782 | 788 | 794 | 800 | 806 | 812 | 818 | 819 | 820 | 821 | -------------------------------------------------------------------------------- /build_web_download.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # vim: ts=4:sw=4:expandtab 3 | 4 | # Copyright (C) 2008-2024 by Andrew Ziem. All rights reserved. 5 | # License GPLv3+: GNU GPL version 3 or later . 6 | # This is free software: you are free to change and redistribute it. 7 | # There is NO WARRANTY, to the extent permitted by law. 8 | # 9 | # 10 | 11 | """ 12 | Build a shell script to download packages from OpenSUSE Build Service (OBS) and make 13 | an HTML snippet of download links 14 | """ 15 | 16 | 17 | import os 18 | import subprocess 19 | import urllib.request, urllib.error, urllib.parse 20 | import sys 21 | import re 22 | import traceback 23 | 24 | 25 | # https://en.wikipedia.org/wiki/Linux_Mint_version_history 26 | UBUNTU_TO_MINT = { 27 | 'Ubuntu 24.04 LTS (Noble Numbat)': ['Linux Mint 22 - 22.1 (Wilma - Xia)'], 28 | 'Ubuntu 22.04 LTS (Jammy Jellyfish)': ['Linux Mint 21 - 21.3 (Vanessa - Virginia)'], 29 | 'Ubuntu 20.04 LTS (Focal Fossa)': ['Linux Mint 20 - 20.3 (Ulyana - Una)'], 30 | 'Ubuntu 18.04 LTS (Bionic Beaver)': ['Linux Mint 19 - 19.2 (Tara - Tina)'] 31 | } 32 | 33 | DISTRO_CODE_TO_NAME = { 34 | 'centos9': 'CentOS 9 Stream', 35 | 'fc40': 'Fedora 40', 36 | 'fc41': 'Fedora 41', 37 | 'fc42': 'Fedora 42', 38 | 'ubuntu1804': 'Ubuntu 18.04 LTS (Bionic Beaver)', 39 | 'ubuntu2004': 'Ubuntu 20.04 LTS (Focal Fossa)', 40 | 'ubuntu2204': 'Ubuntu 22.04 LTS (Jammy Jellyfish)', 41 | 'ubuntu2310': 'Ubuntu 23.10 (Mantic Minitaur)', 42 | 'ubuntu2404': 'Ubuntu 24.04 LTS (Noble Numbat)', 43 | 'ubuntu2410': 'Ubuntu 24.10 (Oracular Oriole)', 44 | 'ubuntu2504': 'Ubuntu 25.04 (Plucky Puffin)', 45 | 'debian11': 'Debian 11 (Bullseye)', 46 | 'debian12': 'Debian 12 (Bookworm)' 47 | } 48 | 49 | def url_to_distro(url : str) -> (str, str): 50 | """Given a URL, return the distribution and version""" 51 | assert url.startswith('https://download.opensuse.org'), f"not an OBS URL: {url}" 52 | distro_ver = url.split("/")[6] 53 | if re.match(r'CentOS_\d+_Stream', distro_ver): 54 | distro_ver = re.sub('_Stream$', '', distro_ver) 55 | if distro_ver.find('RedHat_') == 0: 56 | # example: RedHat_RHEL-6 57 | (dummy, distrover) = distro_ver.split("_") 58 | (distro, ver) = distrover.split("-") 59 | else: 60 | (distro, ver) = distro_ver.split("_") 61 | if distro == "xUbuntu": 62 | distro = "Ubuntu" 63 | return (distro, ver) 64 | 65 | 66 | def make_tag(distro, ver): 67 | """Given a distribution, return a short suffix for the package filename""" 68 | ver = ver.replace(".", "") 69 | if distro == 'Fedora': 70 | # official 71 | return 'fc' + ver 72 | if distro in ('CentOS', 'Ubuntu'): 73 | return distro.lower() + ver 74 | if distro == 'Debian': 75 | # unofficial 76 | if not ver in ('11','12'): 77 | raise Exception("Unknown debian ver %s" % (ver,)) 78 | return 'debian' + ver 79 | if distro == 'openSUSE': 80 | # unofficial 81 | return 'opensuse' + ver 82 | raise Exception("Unknown distro %s" % (distro,)) 83 | 84 | 85 | def filename_to_distro(filename): 86 | """Given a filename, return a pretty distribution name""" 87 | if 'opensuseTumbleweed' in filename: 88 | # example: bleachbit-3.9.0-5.1.opensuseTumbleweed.noarch.rpm 89 | return 'openSUSE Tumbleweed' 90 | if 'opensuseSlowroll' in filename: 91 | return 'openSUSE Slowroll' 92 | tag = re.findall(r"\.([a-z]*[0-9]*)\.noarch.rpm$", filename) 93 | if len(tag) == 1: 94 | return DISTRO_CODE_TO_NAME[tag[0]] 95 | tag = re.findall(r"_([a-z]*[0-9]*)\.deb$", filename) 96 | if len(tag) == 1: 97 | return DISTRO_CODE_TO_NAME[tag[0]] 98 | 99 | if filename.endswith('.exe'): 100 | return 'Microsoft Windows' 101 | 102 | raise Exception("unknown distro for '%s'" % filename) 103 | 104 | 105 | def url_to_filename(url): 106 | """Given a URL of a package on OBS, return a filename that adds the distribution""" 107 | (distro, ver) = url_to_distro(url) 108 | try: 109 | tag = make_tag(distro, ver) 110 | except: 111 | traceback.print_exc() 112 | sys.stderr.flush() 113 | sys.stderr.write( 114 | "url = '%s', distro = '%s', ver=%s\n" % (url, distro, ver)) 115 | raise 116 | old_fn = url[url.rfind("/") + 1:] 117 | if old_fn.find("noarch") >= 0: 118 | return old_fn.split(".noarch")[0] + "." + tag + ".noarch.rpm" 119 | if old_fn.endswith(".deb"): 120 | return old_fn.replace(".deb", "") + "_" + tag + ".deb" 121 | raise Exception(f"Unexpected filename '{old_fn}'") 122 | 123 | 124 | def get_repo_urls(osc_dir): 125 | """Return repository URLs returned by "osc repourls" """ 126 | print("* Getting list of URLs") 127 | old_cwd = os.getcwd() 128 | os.chdir(osc_dir) 129 | repourls = subprocess.Popen( 130 | ["osc", "repourls"], stdout=subprocess.PIPE).communicate()[0] 131 | repourls_txt = repourls.decode() 132 | repourls = re.findall(r"https?://[^\s]+", repourls_txt) 133 | os.chdir(old_cwd) 134 | return repourls 135 | 136 | 137 | def get_files_in_repo_sub(url): 138 | """Return a list of files in an OBS repository sub-directory""" 139 | if not url.startswith('http'): 140 | raise RuntimeError(f'not a valid url {url}') 141 | print(f"opening url '{url}'") 142 | try: 143 | dir = urllib.request.urlopen(url).read(100000) 144 | except: 145 | print(str(sys.exc_info()[1])) 146 | return [] 147 | files = [] 148 | # ignore https://download.opensuse.org/repositories/home:/andrew_z/xUbuntu_25.04/all/769d957dfb780abf11d2a6a4491de28296cdeb 149 | for (file, ext) in re.findall(r"(bleachbit[a-z0-9_.-]+\.)(rpm|deb)", dir.decode()): 150 | fn = file + ext 151 | fileurl = url + fn 152 | print(f"found fileurl '{fileurl}'") 153 | files.append(fileurl) 154 | # make the list unique 155 | files = list(set(files)) 156 | if not files: 157 | print(f'WARNING: no files found in {url}') 158 | return files 159 | 160 | 161 | def get_files_in_repo(repourl): 162 | """Return a list of files in an OBS repository directory""" 163 | # strip off the filename 164 | pos = repourl.rfind("/") 165 | baseurl = repourl[0:pos + 1] 166 | if repourl.find("buntu") >= 0 or repourl.find("ebian") >= 0: 167 | return get_files_in_repo_sub(baseurl + "all/") 168 | else: 169 | return get_files_in_repo_sub(baseurl + "noarch/") 170 | 171 | 172 | def get_files_in_repos(repourls): 173 | """Return a list of files in an OBS repository""" 174 | print("* Getting files in repos") 175 | files = [] 176 | for repourl in repourls: 177 | if len(repourl) < 3: 178 | break 179 | files += get_files_in_repo(repourl) 180 | print(files) 181 | return files 182 | 183 | 184 | def strip_tags(value): 185 | """Return the given HTML with all tags stripped.""" 186 | # http://smitbones.blogspot.com/2008/01/python-strip-html-tags-function.html 187 | return re.sub(r'<[^>]*?>', '', value) 188 | 189 | def add_package(distro, url, filename): 190 | """Add package to HTML snippet 191 | 192 | distro: human name of Linux distribution 193 | 194 | returns a list of tuples (distro_txt, distro, url, filename) 195 | """ 196 | distro_txt = strip_tags(distro) # for sorting 197 | ret = [(distro_txt, distro, url, filename)] 198 | # Users often ask for Mint packages, so for convenience provide 199 | # a link to the compatible Ubuntu package. 200 | if distro in UBUNTU_TO_MINT: 201 | for d in UBUNTU_TO_MINT[distro]: 202 | ret.append((strip_tags(d), d, url, filename)) 203 | if 'Ubuntu' in distro and ' LTS ' in distro and len(ret) == 1: 204 | raise RuntimeError(f'Add {distro} to add_package()') 205 | return ret 206 | 207 | def create_html_snippet(filenames, header): 208 | """Create an HTML snippet with links to download packages""" 209 | print("* Creating HTML snippet") 210 | 211 | # collect list of download packages 212 | records = [] 213 | 214 | for filename in filenames: 215 | if len(filename) < 5 \ 216 | or re.search(r'((tar.(bz2|lzma|gz)|zip|txt|txt.asc|html|sh)$)', filename): 217 | continue 218 | distro = filename_to_distro(filename) 219 | # this url works as of 9/14/2010 220 | #url = "http://sourceforge.net/projects/bleachbit/files/%s" % filename 221 | url = "/download/file/t?file=%s" % filename 222 | records.extend(add_package(distro, url, filename)) 223 | 224 | # sort by distribution name 225 | import operator 226 | records = sorted(records, key=operator.itemgetter(0)) 227 | 228 | # write to file 229 | f = open("snippet_%s.html" % (header.lower().replace(" ", "_"),), "w") 230 | f.write("\n") 236 | 237 | 238 | def write_download_urls(urls): 239 | """"Build a shell script that downloads URLs and renames the files""" 240 | with open('download_from_obs.sh', 'w') as f: 241 | assert len(urls) > 0 242 | for url in urls: 243 | assert url.startswith('http') 244 | local_fn = url_to_filename(url) 245 | cmd = 'wget -nv -nc -O %s %s' % (local_fn, url) 246 | f.write('%s\n' % cmd) 247 | 248 | 249 | def main(): 250 | if len(sys.argv) == 1: 251 | print('invoke with either --make-download (OSC directory) or --make-html') 252 | sys.exit(1) 253 | elif sys.argv[1] == '--make-download': 254 | print("getting URLs from OpenSUSE Build Service") 255 | repourls = get_repo_urls(sys.argv[2]) 256 | fileurls = get_files_in_repos(repourls) 257 | write_download_urls(fileurls) 258 | elif sys.argv[1] == '--make-html': 259 | filenames = sorted(os.listdir('files')) 260 | create_html_snippet(filenames, "Installation package") 261 | else: 262 | raise RuntimeError('Unknown command line' + sys.argv[1]) 263 | 264 | 265 | if __name__ == '__main__': 266 | main() 267 | -------------------------------------------------------------------------------- /credit_translations.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # vim: ts=4:sw=4:expandtab 3 | 4 | # Copyright (C) 2019 by Andrew Ziem. All rights reserved. 5 | # License GPLv3+: GNU GPL version 3 or later . 6 | # This is free software: you are free to change and redistribute it. 7 | # There is NO WARRANTY, to the extent permitted by law. 8 | # 9 | # 10 | 11 | """ 12 | Transform Git log into translation credits 13 | """ 14 | 15 | import os 16 | import subprocess 17 | import sys 18 | 19 | 20 | def process_line(line): 21 | """Process a single entry of the Git log""" 22 | import re 23 | groups = re.split(r'^\w{8} Update (.*) translation thanks to (\.*)', line) 24 | if len(groups) > 2: 25 | lang = groups[1] 26 | authors = groups[3].split(',') # turn string into list 27 | authors = [a.strip(' ') for a in authors] # clean up whitespace 28 | return (lang, authors) 29 | else: 30 | return None 31 | 32 | 33 | def make_html_snippet(credit): 34 | """Make an HTML snippet to show credit 35 | 36 | Language list is sorted. 37 | 38 | Authors list is unique and sorted. 39 | """ 40 | for lang in sorted(credit.keys()): 41 | authors = credit[lang] 42 | authors = sorted(set(authors)) 43 | print('
  • Update %s translation thanks to %s
  • ' % 44 | (lang, ', '.join(authors))) 45 | 46 | 47 | def usage(): 48 | print('usage:') 49 | print('argument 1: path to BleachBit repository') 50 | print('argument 2: Git revision range such as v2.2...v2.3') 51 | sys.exit(1) 52 | 53 | 54 | def go(): 55 | """Main""" 56 | if not len(sys.argv) == 3: 57 | usage() 58 | root_dir = sys.argv[1] 59 | revision_range = sys.argv[2] 60 | os.chdir(root_dir) 61 | cmd = ['git', 'log', '--oneline', revision_range, './po'] 62 | cp = subprocess.run(cmd, capture_output=True, encoding='utf8') 63 | lines = cp.stdout.split('\n') 64 | credit = {} 65 | unrecognized_lines = [] 66 | for line in lines: 67 | ret = process_line(line) 68 | if ret: 69 | # Found a translation update 70 | lang = ret[0] 71 | authors = ret[1] 72 | if lang in credit: 73 | credit[lang] = credit[lang] + authors 74 | else: 75 | credit[lang] = authors 76 | else: 77 | unrecognized_lines.append(line) 78 | make_html_snippet(credit) 79 | for line in sorted(unrecognized_lines): 80 | print('
  • %s
  • ' % line) 81 | 82 | 83 | go() 84 | -------------------------------------------------------------------------------- /debuild_bleachbit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (C) 2009 by Andrew Ziem. All rights reserved. 4 | # License GPLv3+: GNU GPL version 3 or later . 5 | # This is free software: you are free to change and redistribute it. 6 | # There is NO WARRANTY, to the extent permitted by law. 7 | # 8 | # http://bleachbit.blogspot.com/2009/02/building-virtual-ubuntu.html 9 | # 10 | 11 | # Handle fatal errors. 12 | function fail { 13 | echo $1 14 | notify-send -u critical $1 15 | exit 1 16 | } 17 | 18 | # Check which version of Ubuntu is running. 19 | RELEASE=`grep DISTRIB_RELEASE /etc/lsb-release | cut -b17-` 20 | echo "Detected version distribution version $RELEASE" 21 | 22 | mkdir ~/bleachbit 23 | cd ~/bleachbit 24 | # Extract the source code tarball. 25 | tar xzvf ~/tmp/vm/bleachbit*.tar.gz || fail "tar failed" 26 | cd ~/bleachbit/bleachbit-[0-9].[0-9].[0-9]* || fail "'cd' failed" 27 | # Create .deb packaging directory. 28 | mkdir debian 29 | cd debian 30 | # Copy .deb packaging files. 31 | cp ~/tmp/vm/debian/* . 32 | # Create links because openSUSE Build Service and dpkg like different names. 33 | ln -s debian.control control 34 | ln -s debian.rules rules 35 | ln -s debian.changelog changelog 36 | cd ~/bleachbit/bleachbit-[0-9].[0-9].[0-9]* 37 | # Ubuntu 6.06 doesn't have Python central, so remove it. 38 | if [[ "x$RELEASE" == "x6.06" ]]; 39 | then 40 | echo "Applying Ubuntu 6.06 changes" 41 | cd debian 42 | sed -i -e 's/, python-central//g' control 43 | sed -i -e 's/, python-central//g' bleachbit.dsc 44 | sed -i -e 's/dh_pycentral//g' rules 45 | cd .. 46 | 47 | fi 48 | # Build. 49 | debuild 50 | # Check build. 51 | cd ~/bleachbit 52 | [[ ! -e *deb ]] || fail "no .deb files" 53 | # Lintian performs checks against a database of packaging rules. 54 | lintian *deb 55 | -------------------------------------------------------------------------------- /diff_msgid.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (C) 2009 by Andrew Ziem. All rights reserved. 4 | # License GPLv3+: GNU GPL version 3 or later . 5 | # This is free software: you are free to change and redistribute it. 6 | # There is NO WARRANTY, to the extent permitted by law. 7 | # 8 | # Compare change .pot strings from trunk to release version given on command line 9 | # 10 | 11 | 12 | OLDPO=../releases/$1/po 13 | [ -d $OLDPO ] || echo "$OLDPO does not exist" 14 | [ -d $OLDPO ] || exit 15 | 16 | cd ../trunk/po 17 | make bleachbit.pot 18 | cd - 19 | 20 | cd ../releases/$1/po 21 | make bleachbit.pot 22 | cd - 23 | 24 | msggrep -Kie . --no-wrap ../trunk/po/bleachbit.pot | grep ^msgid | cut -c 8- | sort > /tmp/msgid-trunk 25 | msggrep -Kie . --no-wrap $OLDPO/bleachbit.pot | grep ^msgid | cut -c 8- | sort > /tmp/msgid-release-$1 26 | 27 | diff /tmp/msgid-{release-$1,trunk} 28 | 29 | rm -f /tmp/msgid-trunk 30 | rm -f /tmp/msgid-release-$1 31 | 32 | cd $OLDPO 33 | make clean 34 | cd - 35 | -------------------------------------------------------------------------------- /duplicate_cleaner_scan.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (C) 2014 by Andrew Ziem. All rights reserved. 4 | # License GPLv3+: GNU GPL version 3 or later . 5 | # This is free software: you are free to change and redistribute it. 6 | # There is NO WARRANTY, to the extent permitted by law. 7 | # 8 | # Some material from the Automatic Build System Setup for Chakra (GPLv3+) 9 | # Some material from makepkg (GPLv2+) 10 | # 11 | # Helps automate finding duplicate cleaners in the bleachbit repositories via FSlint. 12 | # To use, you will need: bash, git, and fslint installed. 13 | # 14 | # 15 | 16 | # Workspace name (safe to change so long as it's a unique name; this 17 | # directory gets deleted if it already exists.) 18 | WORKSPACENAME="duplicate-scan-workspace" 19 | 20 | # Directory variables 21 | CURDIR="${PWD}" 22 | WORKSPACE="${CURDIR}/${WORKSPACENAME}" 23 | 24 | # Formatted output functions 25 | msg() { 26 | local mesg=$1; shift 27 | printf "${GREEN}==>${ALL_OFF}${BOLD} ${mesg}${ALL_OFF}\n" "$@" >&2 28 | } 29 | 30 | msg2() { 31 | local mesg=$1; shift 32 | printf "${BLUE} ->${ALL_OFF}${BOLD} ${mesg}${ALL_OFF}\n" "$@" >&2 33 | } 34 | 35 | # Delete old workspace if one exists 36 | if [ -d "${WORKSPACE}" ]; then 37 | msg "Deleting old workspace..." 38 | 39 | # Delete write-protected .git files 40 | for i in bleachbit cleanerml 41 | do 42 | sudo rm "${WORKSPACE}"/$i/.git/objects/pack/pack-*.idx 43 | sudo rm "${WORKSPACE}"/$i/.git/objects/pack/pack-*.pack 44 | done 45 | 46 | # Delete regular files 47 | rm -rd "${WORKSPACE}" 48 | fi 49 | 50 | # Create a new workspace 51 | msg "Creating new workspace..." 52 | msg2 "Cloning az0/bleachbit..." 53 | git clone https://github.com/az0/bleachbit.git "${WORKSPACE}"/bleachbit 54 | msg2 "Cloning az0/cleanerml..." 55 | git clone https://github.com/az0/cleanerml.git "${WORKSPACE}"/cleanerml 56 | 57 | # Launch fslint targeting the workspace while hiding standard output 58 | msg "Launching FSlint..." 59 | fslint-gui "${WORKSPACE}" > /dev/null 2>&1 60 | -------------------------------------------------------------------------------- /extract_desktop.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # vim: ts=4:sw=4:expandtab 3 | 4 | # Copyright (C) 2014-2024 by Andrew Ziem. All rights reserved. 5 | # License GPLv3+: GNU GPL version 3 or later . 6 | # This is free software: you are free to change and redistribute it. 7 | # There is NO WARRANTY, to the extent permitted by law. 8 | # 9 | # 10 | 11 | 12 | """ 13 | This program helps manage the translations in the bleachbit.desktop file. 14 | 15 | Here is how it works 16 | 1. The bleachbit/po/Makefile adds strings from bleachbit.desktop to bleachbit.pot 17 | 2. Weblate syncs translation strings from GitHub to hosted.weblate.org 18 | 3. Weblate asks users to translate the strings 19 | 4. Weblate pushes the .po files to the GitHub repository 20 | 5. This program extracts the two strings 21 | 6. I copy the output of this program and paste it into bleachbit.desktop 22 | """ 23 | 24 | 25 | import gettext 26 | import os 27 | import sys 28 | 29 | bleachbit_repo_dir = '../bleachbit' 30 | if not os.path.exists(bleachbit_repo_dir): 31 | print('The bleachbit repository does not exist in ', bleachbit_repo_dir) 32 | sys.exit(1) 33 | os.chdir(bleachbit_repo_dir) 34 | sys.path.append(".") 35 | 36 | import setup 37 | 38 | 39 | locale_dir = 'locale' 40 | 41 | gettext.bindtextdomain('bleachbit', locale_dir) 42 | gettext.textdomain('bleachbit') 43 | gettext.install('bleachbit', locale_dir) 44 | 45 | 46 | def get_lang_str(langid, str): 47 | lang = gettext.translation( 48 | 'bleachbit', localedir=locale_dir, languages=[langid]) 49 | lang.install() 50 | return lang.gettext(str) 51 | 52 | 53 | def update_desktop(parser, key, value): 54 | changes = 0 55 | for langid in sorted(setup.supported_languages()): 56 | if '_new' in langid: 57 | raise ValueError('langid=_new') 58 | translated = get_lang_str(langid, value) 59 | if translated != value: 60 | key_lang = f"{key}[{langid}]" 61 | if key_lang in parser['Desktop Entry']: 62 | old_string = parser['Desktop Entry'][key_lang] 63 | else: 64 | old_string = '' 65 | if old_string != translated: 66 | changes += 1 67 | parser['Desktop Entry'][key_lang] = translated 68 | return changes 69 | 70 | def process_desktop_file(): 71 | from configparser import ConfigParser 72 | parser = ConfigParser() 73 | parser.optionxform = str # preserve case 74 | parser.read('org.bleachbit.BleachBit.desktop', encoding='utf-8') 75 | changes = update_desktop( 76 | parser, 'Comment', 'Free space and maintain privacy') 77 | changes += update_desktop( 78 | parser, 'GenericName', 'Unnecessary file cleaner') 79 | print(f"Made {changes} change(s) to org.bleachbit.BleachBit.desktop") 80 | with open('org.bleachbit.BleachBit.desktop', 'w', encoding='utf-8') as f: 81 | parser.write(f, space_around_delimiters=False) 82 | 83 | def main(): 84 | if not os.path.exists('po/es.po'): 85 | print('ERROR: po/es.po does not exist.') 86 | print('Tip: Verify you are in the root directory of the BleachBit repository.') 87 | sys.exit(1) 88 | if not os.path.exists('po/es.mo'): 89 | print('ERROR: po/es.mo does not exist, so it seems translations are not compiled.') 90 | print('Tip: try running "make -C po local" from the BleachBit repo.') 91 | print('Running the command for you.') 92 | ret = subprocess.run(['make', '-C', 'po', 'local']) 93 | if ret.returncode != 0: 94 | print('ERROR: running "make -C po local" failed') 95 | sys.exit(1) 96 | if not os.path.exists('po/es.mo'): 97 | print('ERROR: po/es.mo does not exist even after running "make -C po local"') 98 | sys.exit(1) 99 | process_desktop_file() 100 | 101 | 102 | 103 | main() 104 | -------------------------------------------------------------------------------- /gource/1-caption.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # vim: ts=4:sw=4:expandtab 3 | 4 | # Copyright (C) 2018 by Andrew Ziem. All rights reserved. 5 | # License GPLv3+: GNU GPL version 3 or later . 6 | # This is free software: you are free to change and redistribute it. 7 | # There is NO WARRANTY, to the extent permitted by law. 8 | # 9 | # 10 | 11 | """ 12 | Convert the caption CSV file into a PSV file 13 | 14 | The CSV is comma delimited with date in YYYY-MM-DD format 15 | 16 | The PSV is pipe delimited with date-time as Unix time 17 | """ 18 | 19 | import csv 20 | import datetime 21 | 22 | with open('caption-yymmdd.csv') as infile, open('caption.psv', 'w') as outfile: 23 | csvr = csv.reader(infile, delimiter=',') 24 | for row in csvr: 25 | ymd = row[0] 26 | y = int(ymd[0:4]) 27 | m = int(ymd[5:7]) 28 | d = int(ymd[8:10]) 29 | dt = datetime.datetime(y, m, d) 30 | unixtime = dt.strftime('%s') 31 | caption = row[1] 32 | outfile.write('%s|%s\n' % (unixtime, caption)) 33 | -------------------------------------------------------------------------------- /gource/1-email.sh: -------------------------------------------------------------------------------- 1 | # vim: ts=4:sw=4:expandtab 2 | 3 | # Copyright (C) 2018 by Andrew Ziem. All rights reserved. 4 | # License GPLv3+: GNU GPL version 3 or later . 5 | # This is free software: you are free to change and redistribute it. 6 | # There is NO WARRANTY, to the extent permitted by law. 7 | # 8 | # 9 | 10 | 11 | # 12 | # Collect email addresses from Git repositories for use with 2-avatar.py 13 | # 14 | 15 | WORKDIR=${WORKDIR:-/tmp/gource} 16 | GITROOT=${GITROOT:-~/repos} 17 | 18 | 19 | rm -f $WORKDIR/committer1 20 | 21 | mkdir -p $WORKDIR 22 | 23 | touch $WORKDIR/committer1 24 | 25 | git_log () { 26 | if [ ! -d "$GITROOT/$1" ];then 27 | echo "ERROR: Repository does not exist: $GITROOT/$1" 28 | exit 1 29 | fi 30 | echo Repository: $1 31 | cd $GITROOT/$1 32 | git log "--pretty=format:%ae|%an" >> $WORKDIR/committer1 33 | echo " " >> $WORKDIR/committer1 34 | } 35 | 36 | git_log bleachbit 37 | git_log bleachbit-misc 38 | git_log cleanerml 39 | git_log winapp2.ini 40 | 41 | sort $WORKDIR/committer1 | sort | uniq | grep @ > $WORKDIR/committer2 42 | -------------------------------------------------------------------------------- /gource/1-log.sh: -------------------------------------------------------------------------------- 1 | # vim: ts=4:sw=4:expandtab 2 | 3 | # Copyright (C) 2018 by Andrew Ziem. All rights reserved. 4 | # License GPLv3+: GNU GPL version 3 or later . 5 | # This is free software: you are free to change and redistribute it. 6 | # There is NO WARRANTY, to the extent permitted by law. 7 | # 8 | # 9 | 10 | # 11 | # ETL Git logs from multiple repositories for use in Gource 12 | # 13 | 14 | WORKDIR=${WORKDIR:-/tmp/gource} 15 | GITROOT=${GITROOT:-~/repos} 16 | 17 | mkdir -p $WORKDIR 18 | 19 | echo Clearing old logs 20 | rm -f $WORKDIR/*.log 21 | 22 | git_log () { 23 | # parameters 24 | # 1: repository directory 25 | # 2: log name 26 | # 3: display name 27 | if [ ! -d "$GITROOT/$1" ];then 28 | echo ERROR: Repository does not exist: $GITROOT/$1 29 | exit 1 30 | fi 31 | echo Repository: $1 32 | gource --output-custom-log $WORKDIR/$2.log $GITROOT/$1 || exit 1 33 | sed -i -r "s#(.+)\|#\1|/$3#" $WORKDIR/core.log 34 | } 35 | 36 | echo Running Gource to extract logs from Git repositories 37 | git_log bleachbit core Core 38 | git_log bleachbit-misc misc misc 39 | git_log cleanerml cleanerml CleanerML 40 | git_log winapp2.ini winapp2 Winapp2.ini 41 | 42 | echo Combining logs 43 | rm -f $WORKDIR/combined.log 44 | cat $WORKDIR/*log | sort -n > $WORKDIR/combined.log 45 | 46 | echo Changing names 47 | sed -i -E 's/\|([^|]*,[^|]*)\|/|Multiple translators|/g' $WORKDIR/combined.log 48 | # combine two names for same person 49 | sed -i -E 's/\|z\|/|Andrew Ziem|/g' $WORKDIR/combined.log 50 | sed -i -E 's/\|rob\|/|Rob|/g' $WORKDIR/combined.log 51 | sed -i -E 's/\|Jean Marc\|/|Jean-Marc|/g' $WORKDIR/combined.log 52 | sed -i -E 's/\|mbrandis\|/|Mark Brandis|/g' $WORKDIR/combined.log 53 | # unsupported characters 54 | sed -i -E 's/அவினாஷ் Avinash/Avinash/g' $WORKDIR/combined.log 55 | sed -i -E 's/☠//g' $WORKDIR/combined.log 56 | 57 | echo Here are the names of contributors 58 | cat $WORKDIR/combined.log | awk -F\| {'print $2'} | sort | uniq | less 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /gource/2-avatar.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # vim: ts=4:sw=4:expandtab 4 | # 5 | # 6 | # Copyright (C) 2018 by Andrew Ziem. All rights reserved. 7 | # License GPLv3+: GNU GPL version 3 or later . 8 | # This is free software: you are free to change and redistribute it. 9 | # There is NO WARRANTY, to the extent permitted by law. 10 | # 11 | """ 12 | 13 | Download avatars 14 | 15 | Based on: 16 | https://code.google.com/p/gource/wiki/GravatarExample 17 | https://gist.github.com/macagua/5c2f5e4e38df92aae7fe 18 | https://gist.github.com/openp2pdesign/15db406825a4b35783e2 19 | 20 | Usage with Gource: gource --user-image-dir .git/avatar/ 21 |   22 | Get list of authors + email with git log 23 | git log --format='%aN|%aE' | sort -u 24 | 25 | Get list of authors + email with hg log (todo) 26 | hg log --template 'author: {author}\n' 27 | """ 28 | 29 | 30 | import requests 31 | import getpass 32 | import os 33 | import subprocess 34 | import hashlib 35 | from time import sleep 36 | import sys 37 | 38 | username = "" 39 | password = "" 40 | 41 | work_dir = os.path.expandvars('~/tmp/gource') 42 | avatar_dir = os.path.join(work_dir, 'avatar') 43 | committer_filename = os.path.join(work_dir, 'committer2') 44 | 45 | 46 | def md5_hex(text): 47 | m = hashlib.md5() 48 | m.update(text.encode('ascii', errors='ignore')) 49 | return m.hexdigest() 50 | 51 | 52 | def get_data(api_request): 53 | global username 54 | global password 55 | 56 | r = requests.get(api_request, auth=(username, password)) 57 | data = r.json() 58 | 59 | if "message" in data.keys(): 60 | print data['message'] 61 | if data['message'] == 'Must specify two-factor authentication OTP code.': 62 | sys.exit(1) 63 | # Countdown 64 | # http://stackoverflow.com/questions/3249524/print-in-one-line-dynamically-python 65 | for k in range(1, 60 * 15): 66 | remaining = 60 * 15 - k 67 | sys.stdout.write("\r%d seconds remaining " % remaining) 68 | sys.stdout.flush() 69 | sleep(1) 70 | sys.stdout.write("\n") 71 | # Another request 72 | r = requests.get(api_request, auth=(username, password)) 73 | data = r.json() 74 | else: 75 | pass 76 | 77 | # Return data 78 | return data 79 | 80 | if __name__ == "__main__": 81 | print 'starting' 82 | global username 83 | global password 84 | 85 | # Clear screen 86 | #os.system('cls' if os.name == 'nt' else 'clear') 87 | 88 | # Login to the GitHub API 89 | username = raw_input("Enter your GitHub username: ") 90 | password = getpass.getpass("Enter your GitHub password: ") 91 | 92 | # Create the folder for storing the images. It's in the .git folder, so it won't be tracked by git 93 | output_dir = os.path.expanduser(avatar_dir) 94 | if not os.path.exists(output_dir): 95 | os.makedirs(output_dir) 96 | 97 | # Get the authors from the Git log 98 | committer_filename = os.path.expanduser(log_path) 99 | print 'Committer filename', committer_filename 100 | authors = [] 101 | with open(committer_filename) as cf: 102 | for line in cf: 103 | authors.append(line.replace('\n', '')) 104 | print "" 105 | print "USERS:" 106 | print(authors) 107 | 108 | # Check each author 109 | for author in authors: 110 | # Get e-mail and name from log 111 | email, name = author.split('|') 112 | print "" 113 | print "Checking", name, email 114 | # Try to find the user on GitHub with the e-mail 115 | api_request = "https://api.github.com/search/users?utf8=%E2%9C%93&q=" + \ 116 | email + "+in%3Aemail&type=Users" 117 | data = get_data(api_request) 118 | 119 | # Check if the user was found 120 | if "items" in data.keys(): 121 | if len(data["items"]) == 1: 122 | url = data["items"][0]["avatar_url"] 123 | print "Avatar url:", url 124 | else: 125 | # Try to find the user on GitHub with the name 126 | api_request = "https://api.github.com/search/users?utf8=%E2%9C%93&q=" + \ 127 | name + "+in%3Aname&type=Users" 128 | data = get_data(api_request) 129 | 130 | # Check if the user was found 131 | if "items" in data.keys(): 132 | if len(data["items"]) == 1: 133 | url = data["items"][0]["avatar_url"] 134 | print "Avatar url:", url 135 | # Eventually try to find the user with Gravatar 136 | else: 137 | # d=404 returns no file if user not found 138 | url = "http://www.gravatar.com/avatar/" + \ 139 | md5_hex(email) + "?d=404&s=" + str(90) 140 | print "Avatar url:", url 141 | 142 | # Finally retrieve the image 143 | try: 144 | output_file = os.path.join(output_dir, name + '.png') 145 | if not os.path.exists(output_file): 146 | r = requests.get(url) 147 | if r.ok: 148 | with open(output_file, 'wb') as img: 149 | img.write(r.content) 150 | except: 151 | print "There was an error with", name, email 152 | -------------------------------------------------------------------------------- /gource/3-raw-video.sh: -------------------------------------------------------------------------------- 1 | # vim: ts=4:sw=4:expandtab 2 | 3 | # Copyright (C) 2018 by Andrew Ziem. All rights reserved. 4 | # License GPLv3+: GNU GPL version 3 or later . 5 | # This is free software: you are free to change and redistribute it. 6 | # There is NO WARRANTY, to the extent permitted by law. 7 | # 8 | 9 | # 10 | # Invoke Gource to produce the video 11 | # 12 | 13 | WORKDIR=${WORKDIR:-/tmp/gource} 14 | GITROOT=${GITROOT:-~/repos} 15 | 16 | rm -f $WORKDIR/video.ppm 17 | 18 | gource \ 19 | --title "History of BleachBit code (2008 to 2018)" \ 20 | --date-format "%Y-%m-%d" \ 21 | --font-size 20 \ 22 | --caption-file caption.psv \ 23 | --caption-duration 5 \ 24 | --auto-skip-seconds 1 \ 25 | --seconds-per-day 0.04 \ 26 | --highlight-dirs \ 27 | --hide mouse,progress \ 28 | --key \ 29 | --stop-at-end \ 30 | --hide-root \ 31 | --logo $GITROOT/bleachbit/bleachbit.png \ 32 | --user-image-dir $WORKDIR/avatar/ \ 33 | --multi-sampling \ 34 | -1920x1080 \ 35 | --output-framerate 60 \ 36 | --output-ppm-stream $WORKDIR/video.ppm \ 37 | $WORKDIR/combined.log 38 | -------------------------------------------------------------------------------- /gource/4-final-video.sh: -------------------------------------------------------------------------------- 1 | # vim: ts=4:sw=4:expandtab 2 | 3 | # Copyright (C) 2018 by Andrew Ziem. All rights reserved. 4 | # License GPLv3+: GNU GPL version 3 or later . 5 | # This is free software: you are free to change and redistribute it. 6 | # There is NO WARRANTY, to the extent permitted by law. 7 | # 8 | 9 | # 10 | # Add music, encode, and trim the video 11 | # 12 | 13 | WORKDIR=${WORKDIR:-/tmp/gource} 14 | 15 | # Music: http://freemusicarchive.org/music/Chris_Zabriskie 16 | # http://freemusicarchive.org/music/Chris_Zabriskie/Vendaface/05_-_Air_Hockey_Saloon 17 | 18 | echo Download music 19 | if [ ! -e "$WORKDIR/music.mp3" ]; then 20 | wget https://freemusicarchive.org/music/download/95af9859ee88ffce9a446e5896c4db8128b3d6a9 -O $WORKDIR/music.mp3 21 | fi 22 | 23 | echo Convert to mp4 24 | rm -f $WORKDIR/video.mp4 25 | time nice ffmpeg -y -r 60 -f image2pipe -vcodec ppm -i $WORKDIR/video.ppm -i $WORKDIR/music.mp3 -vcodec libx264 -acodec copy -preset ultrafast -pix_fmt yuv420p -crf 1 -threads 0 -bf 0 $WORKDIR/video.mp4 26 | 27 | echo Trim 28 | rm -f $WORKDIR/video2.mp4 29 | ffmpeg -i $WORKDIR/video.mp4 -to 03:30 -c copy $WORKDIR/video2.mp4 30 | 31 | -------------------------------------------------------------------------------- /gource/README.md: -------------------------------------------------------------------------------- 1 | # Visualization of BleachBit 2 | 3 | This is a set of scripts that visualizes the development of BleachBit code over time. 4 | 5 | You can [watch the video now](https://www.youtube.com/watch?v=JGjcEKc7oEc&t=1s), or to make it yourself, run the programs in order like this 6 | 7 | ```` 8 | ./1-caption.py 9 | ./1-email.sh 10 | ./1-log.sh 11 | ./2-avatar.py 12 | ./3-raw-video.sh 13 | ./4-final-video.sh 14 | ```` 15 | 16 | -------------------------------------------------------------------------------- /gource/caption-yymmdd.csv: -------------------------------------------------------------------------------- 1 | 2008-12-24,First BleachBit version released 2 | 2008-12-30,BleachBit version 0.2.0 released 3 | 2009-01-27,BleachBit version 0.3.0 released 4 | 2009-02-23,BleachBit version 0.4.0 released with CleanerML 5 | 2009-05-29,BleachBit version 0.5.0 released with Windows support 6 | 2009-08-02,BleachBit version 0.6.0 released 7 | 2009-10-22,BleachBit version 0.7.0 released 8 | 2009-10-25,Unit tests reorganized 9 | 2010-06-04,BleachBit version 0.8.0 released 10 | 2011-08-28,BleachBit version 0.9.0 released 11 | 2012-12-23,Bonus pack moved from SVN to Git as CleanerML 12 | 2013-07-03,BleachBit version 0.9.6 released 13 | 2013-11-15,BleachBit version 1.0 released 14 | 2014-03-09,Winapp2.ini tracked in Git 15 | 2014-03-18,Core code moved from SVN to Git 16 | 2014-03-18,Translators credited as committers 17 | 2014-06-08,BleachBit version 1.2 released 18 | 2014-09-08,BleachBit version 1.4 released 19 | 2014-11-18,BleachBit version 1.6 released 20 | 2015-01-06,Automated tests added to Travis CI 21 | 2015-06-19,BleachBit version 1.8 released 22 | 2015-09-26,Documentation moved to GitHub Pages 23 | 2015-12-07,Automated tests added to Appveyor CI 24 | 2015-12-30,BleachBit version 1.10 released 25 | 2016-05-29,First changes for Mac OS X 26 | 2016-07-01,BleachBit version 1.12 released 27 | 2016-12-17,Automated builds added to Appveyor CI 28 | 2017-02-25,BleachBit 1.17 beta released 29 | 2017-12-30,BleachBit 1.19 beta released 30 | 2018-02-26,BleachBit 2.0 released 31 | -------------------------------------------------------------------------------- /gource/caption.psv: -------------------------------------------------------------------------------- 1 | 1230102000|First BleachBit version released 2 | 1230620400|BleachBit version 0.2.0 released 3 | 1233039600|BleachBit version 0.3.0 released 4 | 1235372400|BleachBit version 0.4.0 released with CleanerML 5 | 1243576800|BleachBit version 0.5.0 released with Windows support 6 | 1249192800|BleachBit version 0.6.0 released 7 | 1256191200|BleachBit version 0.7.0 released 8 | 1256450400|Unit tests reorganized 9 | 1275631200|BleachBit version 0.8.0 released 10 | 1314511200|BleachBit version 0.9.0 released 11 | 1356246000|Bonus pack moved from SVN to Git as CleanerML 12 | 1372831200|BleachBit version 0.9.6 released 13 | 1384498800|BleachBit version 1.0 released 14 | 1394348400|Winapp2.ini tracked in Git 15 | 1395122400|Core code moved from SVN to Git 16 | 1395122400|Translators credited as committers 17 | 1402207200|BleachBit version 1.2 released 18 | 1410156000|BleachBit version 1.4 released 19 | 1416294000|BleachBit version 1.6 released 20 | 1420527600|Automated tests added to Travis CI 21 | 1434693600|BleachBit version 1.8 released 22 | 1443247200|Documentation moved to GitHub Pages 23 | 1449471600|Automated tests added to Appveyor CI 24 | 1451458800|BleachBit version 1.10 released 25 | 1464501600|First changes for Mac OS X 26 | 1467352800|BleachBit version 1.12 released 27 | 1481958000|Automated builds added to Appveyor CI 28 | 1488006000|BleachBit 1.17 beta released 29 | 1514617200|BleachBit 1.19 beta released 30 | 1519628400|BleachBit 2.0 released 31 | -------------------------------------------------------------------------------- /httpmail/LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | 3 | Version 2, June 1991 4 | 5 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 675 Mass Ave, 6 | Cambridge, MA 02139, USA. Everyone is permitted to copy and distribute 7 | verbatim copies of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your freedom to 12 | share and change it. By contrast, the GNU General Public License is 13 | intended to guarantee your freedom to share and change free software--to 14 | make sure the software is free for all its users. This General Public License 15 | applies to most of the Free Software Foundation's software and to any other 16 | program whose authors commit to using it. (Some other Free Software 17 | Foundation software is covered by the GNU Library General Public License 18 | instead.) You can apply it to your programs, too. 19 | 20 | When we speak of free software, we are referring to freedom, not price. Our 21 | General Public Licenses are designed to make sure that you have the 22 | freedom to distribute copies of free software (and charge for this service if 23 | you wish), that you receive source code or can get it if you want it, that you 24 | can change the software or use pieces of it in new free programs; and that 25 | you know you can do these things. 26 | 27 | To protect your rights, we need to make restrictions that forbid anyone to 28 | deny you these rights or to ask you to surrender the rights. These restrictions 29 | translate to certain responsibilities for you if you distribute copies of the 30 | software, or if you modify it. 31 | 32 | For example, if you distribute copies of such a program, whether gratis or for 33 | a fee, you must give the recipients all the rights that you have. You must make 34 | sure that they, too, receive or can get the source code. And you must show 35 | them these terms so they know their rights. 36 | 37 | We protect your rights with two steps: (1) copyright the software, and (2) 38 | offer you this license which gives you legal permission to copy, distribute 39 | and/or modify the software. 40 | 41 | Also, for each author's protection and ours, we want to make certain that 42 | everyone understands that there is no warranty for this free software. If the 43 | software is modified by someone else and passed on, we want its recipients 44 | to know that what they have is not the original, so that any problems 45 | introduced by others will not reflect on the original authors' reputations. 46 | 47 | Finally, any free program is threatened constantly by software patents. We 48 | wish to avoid the danger that redistributors of a free program will individually 49 | obtain patent licenses, in effect making the program proprietary. To prevent 50 | this, we have made it clear that any patent must be licensed for everyone's 51 | free use or not licensed at all. 52 | 53 | The precise terms and conditions for copying, distribution and modification 54 | follow. 55 | 56 | GNU GENERAL PUBLIC LICENSE 57 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND 58 | MODIFICATION 59 | 60 | 0. This License applies to any program or other work which contains a notice 61 | placed by the copyright holder saying it may be distributed under the terms 62 | of this General Public License. The "Program", below, refers to any such 63 | program or work, and a "work based on the Program" means either the 64 | Program or any derivative work under copyright law: that is to say, a work 65 | containing the Program or a portion of it, either verbatim or with 66 | modifications and/or translated into another language. (Hereinafter, translation 67 | is included without limitation in the term "modification".) Each licensee is 68 | addressed as "you". 69 | 70 | Activities other than copying, distribution and modification are not covered 71 | by this License; they are outside its scope. The act of running the Program is 72 | not restricted, and the output from the Program is covered only if its contents 73 | constitute a work based on the Program (independent of having been made 74 | by running the Program). Whether that is true depends on what the Program 75 | does. 76 | 77 | 1. You may copy and distribute verbatim copies of the Program's source 78 | code as you receive it, in any medium, provided that you conspicuously and 79 | appropriately publish on each copy an appropriate copyright notice and 80 | disclaimer of warranty; keep intact all the notices that refer to this License 81 | and to the absence of any warranty; and give any other recipients of the 82 | Program a copy of this License along with the Program. 83 | 84 | You may charge a fee for the physical act of transferring a copy, and you 85 | may at your option offer warranty protection in exchange for a fee. 86 | 87 | 2. You may modify your copy or copies of the Program or any portion of it, 88 | thus forming a work based on the Program, and copy and distribute such 89 | modifications or work under the terms of Section 1 above, provided that you 90 | also meet all of these conditions: 91 | 92 | a) You must cause the modified files to carry prominent notices stating that 93 | you changed the files and the date of any change. 94 | 95 | b) You must cause any work that you distribute or publish, that in whole or in 96 | part contains or is derived from the Program or any part thereof, to be 97 | licensed as a whole at no charge to all third parties under the terms of this 98 | License. 99 | 100 | c) If the modified program normally reads commands interactively when run, 101 | you must cause it, when started running for such interactive use in the most 102 | ordinary way, to print or display an announcement including an appropriate 103 | copyright notice and a notice that there is no warranty (or else, saying that 104 | you provide a warranty) and that users may redistribute the program under 105 | these conditions, and telling the user how to view a copy of this License. 106 | (Exception: if the Program itself is interactive but does not normally print such 107 | an announcement, your work based on the Program is not required to print 108 | an announcement.) 109 | 110 | These requirements apply to the modified work as a whole. If identifiable 111 | sections of that work are not derived from the Program, and can be 112 | reasonably considered independent and separate works in themselves, then 113 | this License, and its terms, do not apply to those sections when you distribute 114 | them as separate works. But when you distribute the same sections as part 115 | of a whole which is a work based on the Program, the distribution of the 116 | whole must be on the terms of this License, whose permissions for other 117 | licensees extend to the entire whole, and thus to each and every part 118 | regardless of who wrote it. 119 | 120 | Thus, it is not the intent of this section to claim rights or contest your rights to 121 | work written entirely by you; rather, the intent is to exercise the right to 122 | control the distribution of derivative or collective works based on the 123 | Program. 124 | 125 | In addition, mere aggregation of another work not based on the Program 126 | with the Program (or with a work based on the Program) on a volume of a 127 | storage or distribution medium does not bring the other work under the scope 128 | of this License. 129 | 130 | 3. You may copy and distribute the Program (or a work based on it, under 131 | Section 2) in object code or executable form under the terms of Sections 1 132 | and 2 above provided that you also do one of the following: 133 | 134 | a) Accompany it with the complete corresponding machine-readable source 135 | code, which must be distributed under the terms of Sections 1 and 2 above 136 | on a medium customarily used for software interchange; or, 137 | 138 | b) Accompany it with a written offer, valid for at least three years, to give 139 | any third party, for a charge no more than your cost of physically performing 140 | source distribution, a complete machine-readable copy of the corresponding 141 | source code, to be distributed under the terms of Sections 1 and 2 above on 142 | a medium customarily used for software interchange; or, 143 | 144 | c) Accompany it with the information you received as to the offer to distribute 145 | corresponding source code. (This alternative is allowed only for 146 | noncommercial distribution and only if you received the program in object 147 | code or executable form with such an offer, in accord with Subsection b 148 | above.) 149 | 150 | The source code for a work means the preferred form of the work for 151 | making modifications to it. For an executable work, complete source code 152 | means all the source code for all modules it contains, plus any associated 153 | interface definition files, plus the scripts used to control compilation and 154 | installation of the executable. However, as a special exception, the source 155 | code distributed need not include anything that is normally distributed (in 156 | either source or binary form) with the major components (compiler, kernel, 157 | and so on) of the operating system on which the executable runs, unless that 158 | component itself accompanies the executable. 159 | 160 | If distribution of executable or object code is made by offering access to 161 | copy from a designated place, then offering equivalent access to copy the 162 | source code from the same place counts as distribution of the source code, 163 | even though third parties are not compelled to copy the source along with the 164 | object code. 165 | 166 | 4. You may not copy, modify, sublicense, or distribute the Program except as 167 | expressly provided under this License. Any attempt otherwise to copy, 168 | modify, sublicense or distribute the Program is void, and will automatically 169 | terminate your rights under this License. However, parties who have received 170 | copies, or rights, from you under this License will not have their licenses 171 | terminated so long as such parties remain in full compliance. 172 | 173 | 5. You are not required to accept this License, since you have not signed it. 174 | However, nothing else grants you permission to modify or distribute the 175 | Program or its derivative works. These actions are prohibited by law if you 176 | do not accept this License. Therefore, by modifying or distributing the 177 | Program (or any work based on the Program), you indicate your acceptance 178 | of this License to do so, and all its terms and conditions for copying, 179 | distributing or modifying the Program or works based on it. 180 | 181 | 6. Each time you redistribute the Program (or any work based on the 182 | Program), the recipient automatically receives a license from the original 183 | licensor to copy, distribute or modify the Program subject to these terms and 184 | conditions. You may not impose any further restrictions on the recipients' 185 | exercise of the rights granted herein. You are not responsible for enforcing 186 | compliance by third parties to this License. 187 | 188 | 7. If, as a consequence of a court judgment or allegation of patent 189 | infringement or for any other reason (not limited to patent issues), conditions 190 | are imposed on you (whether by court order, agreement or otherwise) that 191 | contradict the conditions of this License, they do not excuse you from the 192 | conditions of this License. If you cannot distribute so as to satisfy 193 | simultaneously your obligations under this License and any other pertinent 194 | obligations, then as a consequence you may not distribute the Program at all. 195 | For example, if a patent license would not permit royalty-free redistribution 196 | of the Program by all those who receive copies directly or indirectly through 197 | you, then the only way you could satisfy both it and this License would be to 198 | refrain entirely from distribution of the Program. 199 | 200 | If any portion of this section is held invalid or unenforceable under any 201 | particular circumstance, the balance of the section is intended to apply and 202 | the section as a whole is intended to apply in other circumstances. 203 | 204 | It is not the purpose of this section to induce you to infringe any patents or 205 | other property right claims or to contest validity of any such claims; this 206 | section has the sole purpose of protecting the integrity of the free software 207 | distribution system, which is implemented by public license practices. Many 208 | people have made generous contributions to the wide range of software 209 | distributed through that system in reliance on consistent application of that 210 | system; it is up to the author/donor to decide if he or she is willing to 211 | distribute software through any other system and a licensee cannot impose 212 | that choice. 213 | 214 | This section is intended to make thoroughly clear what is believed to be a 215 | consequence of the rest of this License. 216 | 217 | 8. If the distribution and/or use of the Program is restricted in certain 218 | countries either by patents or by copyrighted interfaces, the original copyright 219 | holder who places the Program under this License may add an explicit 220 | geographical distribution limitation excluding those countries, so that 221 | distribution is permitted only in or among countries not thus excluded. In such 222 | case, this License incorporates the limitation as if written in the body of this 223 | License. 224 | 225 | 9. The Free Software Foundation may publish revised and/or new versions 226 | of the General Public License from time to time. Such new versions will be 227 | similar in spirit to the present version, but may differ in detail to address new 228 | problems or concerns. 229 | 230 | Each version is given a distinguishing version number. If the Program specifies 231 | a version number of this License which applies to it and "any later version", 232 | you have the option of following the terms and conditions either of that 233 | version or of any later version published by the Free Software Foundation. If 234 | the Program does not specify a version number of this License, you may 235 | choose any version ever published by the Free Software Foundation. 236 | 237 | 10. If you wish to incorporate parts of the Program into other free programs 238 | whose distribution conditions are different, write to the author to ask for 239 | permission. For software which is copyrighted by the Free Software 240 | Foundation, write to the Free Software Foundation; we sometimes make 241 | exceptions for this. Our decision will be guided by the two goals of 242 | preserving the free status of all derivatives of our free software and of 243 | promoting the sharing and reuse of software generally. 244 | 245 | NO WARRANTY 246 | 247 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 248 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT 249 | PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE 250 | STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 251 | OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT 252 | WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, 253 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 254 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 255 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND 256 | PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 257 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL 258 | NECESSARY SERVICING, REPAIR OR CORRECTION. 259 | 260 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR 261 | AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR 262 | ANY OTHER PARTY WHO MAY MODIFY AND/OR 263 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE 264 | LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, 265 | SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES 266 | ARISING OUT OF THE USE OR INABILITY TO USE THE 267 | PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA 268 | OR DATA BEING RENDERED INACCURATE OR LOSSES 269 | SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE 270 | PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN 271 | IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF 272 | THE POSSIBILITY OF SUCH DAMAGES. 273 | 274 | END OF TERMS AND CONDITIONS 275 | -------------------------------------------------------------------------------- /httpmail/README: -------------------------------------------------------------------------------- 1 | HTTP Mail 2 | http://bleachbit.svn.sourceforge.net/viewvc/bleachbit/misc/ 3 | Copyright (c) 2009 by Andrew Ziem. All Rights Reserved. 4 | 5 | 6 | 7 | SUMMARY 8 | HTTP Mail is a module for Drupal 6 to queue external mail and 9 | export it from Drupal by XML RPC. Then, a client fetches 10 | the mail from Drupal and relays it. A client in Python 11 | for Google App Engine is provided. 12 | 13 | 14 | APPLICATION 15 | SourceForge (and probably other web hosts) doesn't allow external 16 | connections. The module may also be useful for some high 17 | traffic sites. 18 | 19 | 20 | CREDITS 21 | A few parts copied from Khalid Baheyeldin's Queue Mail 22 | http://drupal.org/project/queue_mail 23 | 24 | 25 | LICENSE 26 | GNU General Public License version or later. 27 | 28 | 29 | INSTALLATION 30 | 1. Copy httpmail.{info,module,install} to Drupal sites/all/modules/httpmail 31 | 2. In Drupal under modules, enable HTTP Mail. 32 | 3. In Drupal under HTTP Mail, turn HTTP Mail on. 33 | 4. Setup the client to poll periodically using cron. (GAE supports cron.) 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /httpmail/httpmail.info: -------------------------------------------------------------------------------- 1 | name = HTTP Mail 2 | description = "Queue email for polling by HTTP" 3 | core = 6.x 4 | package = Mail 5 | php = 4.0.0 6 | version = 6.x-1 7 | 8 | 9 | -------------------------------------------------------------------------------- /httpmail/httpmail.install: -------------------------------------------------------------------------------- 1 | 'Stores emails.', 42 | 'fields' => array( 43 | 'id' => array( 44 | 'type' => 'serial', 45 | 'not null' => TRUE, 46 | 'description' => 'Primary Key: HTTP Mail queue', 47 | ), 48 | 'dt_queued' => array( 49 | 'type' => 'datetime', 50 | 'not null' => TRUE, 51 | 'description' => 'Date-time the record was inserted.', 52 | ), 53 | 'dt_sent' => array( 54 | 'type' => 'datetime', 55 | 'not null' => FALSE, 56 | 'description' => 'Date-time the record was exported by XML RPC.', 57 | ), 58 | 'status' => array( 59 | 'type' => 'char', 60 | 'length' => 1, 61 | 'default' => 'N', 62 | 'description' => 'N=new, O=old, E=error sending', 63 | ), 64 | 'type_id' => array( 65 | 'type' => 'varchar', 66 | 'length' => 255, 67 | 'description' => 'Unique identifier for type of email', 68 | ), 69 | 'to_addr' => array( 70 | 'type' => 'varchar', 71 | 'length' => 255, 72 | 'not null' => TRUE, 73 | 'description' => 'Recipient', 74 | ), 75 | 'from_addr' => array( 76 | 'type' => 'varchar', 77 | 'length' => 255, 78 | 'description' => 'Sender email address', 79 | ), 80 | 'subject' => array( 81 | 'type' => 'varchar', 82 | 'length' => 255, 83 | 'description' => 'Language', 84 | ), 85 | 'body' => array( 86 | 'type' => 'varchar', 87 | 'length' => 32000, 88 | 'description' => 'Body', 89 | ), 90 | 'headers' => array( 91 | 'type' => 'varchar', 92 | 'length' => 5000, 93 | 'description' => 'Headers', 94 | ), 95 | ), 96 | 97 | 'primary key' => array('id'), 98 | ); 99 | 100 | return $schema; 101 | } 102 | 103 | /** 104 | * http://www.php.net/manual/en/function.rand.php 105 | */ 106 | function rand_str($length = 32, $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890') 107 | { 108 | // Length of character list 109 | $chars_length = (strlen($chars) - 1); 110 | 111 | // Start our string 112 | $string = $chars{rand(0, $chars_length)}; 113 | 114 | // Generate random string 115 | for ($i = 1; $i < $length; $i = strlen($string)) 116 | { 117 | // Grab a random character from our list 118 | $r = $chars{rand(0, $chars_length)}; 119 | 120 | // Make sure the same two characters don't appear next to each other 121 | if ($r != $string{$i - 1}) $string .= $r; 122 | } 123 | 124 | // Return the string 125 | return $string; 126 | } 127 | ?> 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /httpmail/httpmail.module: -------------------------------------------------------------------------------- 1 | 'HTTP Mail', 37 | 'page callback' => 'drupal_get_form', 38 | 'page arguments' => array('httpmail_admin_settings'), 39 | 'access arguments' => array('administer site configuration'), 40 | 'description' => 'Queue email in database for polling by HTTP.', 41 | ); 42 | 43 | $secret = variable_get('httpmail_secret', 'You must set this to a unique value!!!'); 44 | 45 | $items[$secret] = array('title' => 'Secret!'); 46 | 47 | return $items; 48 | } 49 | 50 | 51 | /** 52 | * Administrative settings. 53 | * 54 | * @return 55 | * An array containing form items to place on the module settings page. 56 | */ 57 | function httpmail_admin_settings() { 58 | // Override the smtp_library variable. 59 | if (variable_get('httpmail_on', 0)) { 60 | $smtp_path = drupal_get_filename('module', 'httpmail'); 61 | if ($smtp_path) { 62 | variable_set('smtp_library', $smtp_path); /* see includes/mail.inc */ 63 | drupal_set_message(t('HTTPMAIL.module is active.')); 64 | } 65 | // If drupal can't find the path to the module, display an error. 66 | else { 67 | drupal_set_message(t("HTTPMAIL.module error: Can't find file."), 'error'); 68 | } 69 | } 70 | // If this module is turned off, delete the variable. 71 | else { 72 | variable_del('smtp_library'); 73 | drupal_set_message(t('HTTPMAIL.module is INACTIVE.')); 74 | } 75 | 76 | $form['onoff'] = array( 77 | '#type' => 'fieldset', 78 | '#title' => t('Install options'), 79 | ); 80 | $form['onoff']['httpmail_on'] = array( 81 | '#type' => 'radios', 82 | '#title' => t('Turn this module on or off'), 83 | '#default_value' => variable_get('httpmail_on', 0), 84 | '#options' => array(1 => t('On'), 0 => t('Off')), 85 | '#description' => t('To uninstall this module you must turn it off here first.'), 86 | ); 87 | 88 | $form['server'] = array( 89 | '#type' => 'fieldset', 90 | '#title' => t('SMTP server settings'), 91 | ); 92 | $form['server']['httpmail_secret'] = array( 93 | '#type' => 'textfield', 94 | '#title' => t('Secret'), 95 | '#default_value' => variable_get('httpmail_secret', ''), 96 | '#description' => t('A secret code'), 97 | ); 98 | 99 | // If an address was given, send a test e-mail message. 100 | $test_address = variable_get('httpmail_test_address', ''); 101 | if ($test_address != '') { 102 | // Clear the variable so only one message is sent. 103 | variable_del('httpmail_test_address'); 104 | global $language; 105 | $params['subject'] = t('Drupal test e-mail'); 106 | $params['body'] = t('If you receive this message it means your site is capable of sending e-mail.'); 107 | drupal_mail('httpmail', 'httpmail-test', $test_address, $language, $params); 108 | drupal_set_message(t('A test e-mail has been sent to @email. You may want to !check for any error messages.', array('@email' => $test_address, '!check' => l(t('check the logs'), 'admin/reports/dblog')))); 109 | } 110 | $form['email_test'] = array( 111 | '#type' => 'fieldset', 112 | '#title' => t('Send test e-mail'), 113 | ); 114 | $form['email_test']['httpmail_test_address'] = array( 115 | '#type' => 'textfield', 116 | '#title' => t('E-mail address to send a test e-mail to'), 117 | '#default_value' => '', 118 | '#description' => t('Type in an address to have a test e-mail sent there.'), 119 | ); 120 | 121 | return system_settings_form($form); 122 | } 123 | 124 | 125 | 126 | /** 127 | * Validation for the administrative settings form. 128 | * 129 | * @param form 130 | * An associative array containing the structure of the form. 131 | * @param form_state 132 | * A keyed array containing the current state of the form. 133 | */ 134 | function httpmail_admin_settings_validate($form, &$form_state) { 135 | 136 | 137 | } 138 | 139 | 140 | 141 | /** 142 | * Sends out the e-mail. 143 | * 144 | * @param message 145 | * An array with at least the following elements: id, to, subject, body and 146 | * headers. 147 | * 148 | * @see drupal_mail_send() 149 | */ 150 | function drupal_mail_wrapper($message) { 151 | $id = $message['id']; 152 | $to = $message['to']; 153 | $from = $message['from']; 154 | $language = $message['language']; 155 | $subject = $message['subject']; 156 | $body = $message['body']; 157 | $headers = $message['headers']; 158 | $headers_str = implode("\n", $headers); 159 | 160 | // check 161 | assert(is_string($id)); 162 | assert(is_string($to)); 163 | assert(is_string($from)); 164 | // assert(is_string($language)); 165 | assert(is_string($body)); 166 | 167 | // Log 168 | watchdog('httpmail', 'Sending mail to: @to', array('@to' => $to)); 169 | 170 | // Queue 171 | $ok = db_query("INSERT INTO {httpmail_queue} (dt_queued, type_id, to_addr, from_addr, subject, body, headers) VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s')", 172 | date("Y-m-d H:m:s"), $id, $to, $from, $subject, $body, $headers_str); 173 | 174 | // Log any error 175 | if (!$ok) { 176 | watchdog('httpmail', 'Error sending e-mail from @from to @to : !error_message', array('@from' => $from, '@to' => $to, '!error_message' => 'database'), WATCHDOG_ERROR); 177 | return FALSE; 178 | } 179 | 180 | return TRUE; 181 | } // End of drupal_mail_wrapper() 182 | 183 | 184 | 185 | /** 186 | * implementation of hook_mail(). 187 | */ 188 | function httpmail_mail($key, &$message, $params) { 189 | if ($key == 'httpmail-test') { 190 | $message['subject'] = $params['subject']; 191 | $message['body'] = $params['body']; 192 | } 193 | } 194 | 195 | 196 | /* 197 | * Authenticate 198 | * Used by XML RPC functions 199 | */ 200 | function httpmail_authenticate($secret) 201 | { 202 | $real_secret = variable_get('httpmail_secret', ""); 203 | if ($real_secret === $secret and strlen($real_secret) >= 10) 204 | return TRUE; 205 | else 206 | return t('Permission denied.'); 207 | } 208 | 209 | 210 | /* 211 | * List ID's of new mail 212 | * Exported by XML RPC 213 | */ 214 | function httpmail_list_new($secret) 215 | { 216 | $ret = httpmail_authenticate($secret); 217 | if (is_string($ret)) 218 | return array($ret); 219 | $result = db_query("SELECT id FROM {httpmail_queue} WHERE status='N' ORDER BY dt_queued"); 220 | $ret = array(); 221 | while ($mail = db_fetch_object($result)) { 222 | array_push($ret, $mail->id); 223 | } 224 | return $ret; 225 | } 226 | 227 | 228 | /* 229 | * Return one new email and mark it read 230 | * Export by XML RPC 231 | */ 232 | function httpmail_get_email($secret, $id) 233 | { 234 | $ret = httpmail_authenticate($secret); 235 | if (is_string($ret)) 236 | return array($ret); 237 | $id = intval($id); 238 | $result1 = db_query("SELECT to_addr, from_addr, subject, headers, body FROM {httpmail_queue} WHERE status='N' AND id='%d'", $id); 239 | $email = db_fetch_object($result1); 240 | $result2 = db_query("UPDATE {httpmail_queue} SET status='O', dt_sent=now() WHERE id='%d' LIMIT 1", $id); 241 | return $email; 242 | } 243 | 244 | /** 245 | * implementation of hook_xmlrpc(). 246 | */ 247 | function httpmail_xmlrpc() { 248 | return array( 249 | array( 250 | 'httpmail.list_new', 251 | httpmail_list_new, 252 | array('struct', 'string'), 253 | t('List new messages')), 254 | array( 255 | 'httpmail.get_email', 256 | httpmail_get_email, 257 | array('array', 'string', 'int'), 258 | t('Retrieve one new email and mark it read'))); 259 | } 260 | 261 | 262 | 263 | 264 | 265 | -------------------------------------------------------------------------------- /iso639_langcodes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2 2 | 3 | # Looks up all three letter iso 639-3 langcodes either given as argument or 4 | # in all installed locales 5 | 6 | from urllib2 import urlopen 7 | from sys import argv 8 | from os import listdir, path 9 | 10 | langcodes = argv[1:] or [dir for dir in listdir('/usr/share/locale') if len(dir) == 3] 11 | 12 | if not langcodes: 13 | print('Nothing to do...') 14 | exit(0) 15 | 16 | response = urlopen('http://www-01.sil.org/iso639-3/iso-639-3_Name_Index.tab') 17 | rows = (line.split('\t') for line in response.read().splitlines()) 18 | langdict = {cell[0] : cell[1] for cell in rows} 19 | 20 | for langcode in langcodes: 21 | langname = langdict[langcode] 22 | if not langname: 23 | print('Unknown code '+langcode) 24 | else: 25 | print("'{}': '{}',".format(langcode, langname)) 26 | -------------------------------------------------------------------------------- /list_cleaners.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # vim: ts=4:sw=4:expandtab 3 | 4 | 5 | # Copyright (C) 2009-2019 by Andrew Ziem. All rights reserved. 6 | # License GPLv3+: GNU GPL version 3 or later . 7 | # This is free software: you are free to change and redistribute it. 8 | # There is NO WARRANTY, to the extent permitted by law. 9 | 10 | 11 | """ 12 | List all cleaners in HTML format 13 | """ 14 | 15 | 16 | import gettext 17 | import os 18 | import sys 19 | 20 | os.chdir('../bleachbit') 21 | sys.path.append(".") 22 | 23 | from bleachbit.Cleaner import backends 24 | from bleachbit.CleanerML import load_cleaners 25 | 26 | 27 | def main(): 28 | # This could be used with the CleanerML repository 29 | #bleachbit.Common.personal_cleaners_dir = '../cleanerml' 30 | load_cleaners() 31 | for key in sorted(backends): 32 | 33 | options = [] 34 | for (option_id, name) in backends[key].get_options(): 35 | options.append(name) 36 | 37 | print('
  • %s
  • ' % (", ".join(options), backends[key].get_name())) 38 | 39 | 40 | main() 41 | -------------------------------------------------------------------------------- /lp2git.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # vim: ts=4:sw=4:expandtab 3 | 4 | # Copyright (C) 2014-2019 by Andrew Ziem. All rights reserved. 5 | # License GPLv3+: GNU GPL version 3 or later . 6 | # This is free software: you are free to change and redistribute it. 7 | # There is NO WARRANTY, to the extent permitted by law. 8 | # 9 | # 10 | 11 | 12 | """Import translated .po files from Launchpad to Git""" 13 | 14 | from bs4 import BeautifulSoup 15 | from polib import pofile 16 | from urllib.parse import urlencode 17 | from joblib import Memory 18 | import os 19 | import re 20 | import sys 21 | 22 | 23 | # example : translations['es']['Preview'] = 24 | # https://translations.launchpad.net/bleachbit/trunk/+pots/bleachbit/es/159/+translate 25 | translations = {} 26 | 27 | # Disabling search is a workaround for this timeout error 28 | # https://bugs.launchpad.net/launchpad/+bug/1909501 29 | disable_search = True 30 | 31 | cache_dir = None 32 | 33 | 34 | def get_cache_dir(): 35 | """Initialize and return the directory name to use for joblib caching""" 36 | global cache_dir 37 | if cache_dir: 38 | # If available, use the cached result. 39 | return cache_dir 40 | cache_base = os.getenv('XDG_CACHE_HOME') or os.path.expanduser('~/.cache') 41 | if not os.path.exists(cache_base): 42 | os.mkdir(cache_base) 43 | # Add hack to expire cache daily. 44 | # https://github.com/joblib/joblib/issues/313 45 | from datetime import datetime 46 | date_code = datetime.now().strftime('%Y-%m-%d') 47 | cache_dir = os.path.join(cache_base, f'lp2git-{date_code}') 48 | if not os.path.exists(cache_dir): 49 | os.mkdir(cache_dir) 50 | print(f'info: caching to directory {cache_dir}') 51 | return cache_dir 52 | 53 | 54 | def open_read_decode(opener, url): 55 | """Given a build_opener, return the decoded body""" 56 | print(f'debug: fetch url {url}') 57 | return opener.open(url).read().decode() 58 | 59 | 60 | def read_http(url): 61 | from urllib.request import build_opener 62 | opener = build_opener() 63 | opener.addheaders = [('User-agent', 'lp2git')] 64 | memory = Memory(get_cache_dir(), verbose=0) 65 | ord_func = memory.cache(open_read_decode) 66 | return ord_func(opener, url) 67 | 68 | 69 | def parse_search_html(lang_id, msgid, start): 70 | """ 71 | Query Launchpad for a message, and add its URL to the 72 | global dictionary. 73 | 74 | Parameters 75 | ---------- 76 | lang_id: language code such as en_GB 77 | msgid: message (i.e., English string) 78 | start: index for pagination 79 | 80 | Returns 81 | ------- 82 | integer: value of start indicating to get more results 83 | None: there are no more search results 84 | """ 85 | if disable_search: 86 | url = f'https://translations.launchpad.net/bleachbit/trunk/+pots/bleachbit/{lang_id}/+translate?start={start}' 87 | else: 88 | param = urlencode({'show': 'all', 89 | 'search': msgid, 90 | 'start': start}) 91 | url = f'https://translations.launchpad.net/bleachbit/trunk/+pots/bleachbit/{lang_id}/+translate?{param}' 92 | 93 | doc = read_http(url) 94 | soup = BeautifulSoup(doc, features="lxml") 95 | for tr in soup.findAll('tr', attrs={'class': 'translation'}): 96 | # en_div: div element that contains the English translation 97 | en_div = tr.find('div', attrs={'id': re.compile("^msgset_[0-9]+_")}) 98 | if not en_div: 99 | continue 100 | from html import unescape 101 | # en_txt: English version of the message (i.e., untranslated) 102 | # unescape for example for "environment's" 103 | en_txt = unescape(en_div.text) 104 | en_div_id = en_div.attrs['id'] 105 | en_ctxt_div = soup.findAll( 106 | id=en_div_id.replace('singular', 'context')) 107 | if en_ctxt_div: 108 | # Specific (i.e., non-default) context of the message. 109 | en_ctxt = en_ctxt_div[0].text 110 | else: 111 | # Default context. This is the most common. 112 | en_ctxt = "none" 113 | if not tr.find('a'): 114 | continue 115 | # href: link to page with just one message 116 | href = tr.find('a').attrs['href'] 117 | if not en_ctxt in translations[lang_id].keys(): 118 | # initialize the context 119 | translations[lang_id][en_ctxt] = {} 120 | translations[lang_id][en_ctxt][en_txt] = href 121 | del href 122 | more = soup.findAll('a', attrs={'class': 'next'}) 123 | if more: 124 | # more results 125 | ret = re.search('start=([0-9]+)', more[0]['href']) 126 | if ret: 127 | return int(ret.groups()[0]) 128 | # no more results 129 | return None 130 | 131 | 132 | def parse_detail_html(url): 133 | """Parse a Launchpad page for an individual translation message""" 134 | doc = read_http(url) 135 | soup = BeautifulSoup(doc, features="lxml") 136 | label = soup.find('label', 'no-translation') 137 | if label: 138 | raise RuntimeError('not translated') 139 | ret = [] 140 | td_tr = soup.find('td', attrs={'id': 'translated_and_reviewed_by'}) 141 | if td_tr: 142 | ret.append(td_tr.a.text) 143 | td_t = soup.find('td', attrs={'id': 'translated_by'}) 144 | if td_t: 145 | ret.append(td_t.a.text) 146 | td_r = soup.find('td', attrs={'id': 'reviewed_by'}) 147 | if td_r: 148 | ret.append(td_r.a.text) 149 | if 0 == len(ret): 150 | raise RuntimeError('translated but no translators found') 151 | return ret 152 | 153 | 154 | def who_translated(lang_id, msgctxt, msgid): 155 | """Returns name of person who translated a particular string""" 156 | 157 | if not lang_id in translations.keys(): 158 | # initialize the language 159 | translations[lang_id] = {} 160 | 161 | start = 0 162 | while True: 163 | start = parse_search_html(lang_id, msgid, start) 164 | msgctxt_key = "none" if msgctxt is None else msgctxt 165 | if msgctxt_key in translations[lang_id]: 166 | if msgid in translations[lang_id][msgctxt_key]: 167 | url = translations[lang_id][msgctxt_key][msgid] 168 | return parse_detail_html(url) 169 | if None == start: 170 | raise RuntimeError('not found "%s"' % msgid) 171 | 172 | 173 | def get_lang_name(po, lang_id): 174 | """Given a pofile, return the human-readable language name""" 175 | # workaround inconsistent data 176 | if 'sr' == lang_id: 177 | return 'Serbian' 178 | if 'pl' == lang_id: 179 | return 'Polish' 180 | if 'bg' == lang_id: 181 | return 'Bulgarian' 182 | if 'id' == lang_id: 183 | return 'Indonesian' 184 | return po.metadata['Language-Team'].split('<')[0].strip() 185 | 186 | 187 | def process_po(lang_id): 188 | new_fn = lang_id + '_new.po' 189 | old_fn = lang_id + '.po' 190 | po_new = pofile(new_fn) 191 | po_old = pofile(old_fn) 192 | msgids = [] 193 | # Build list of new messages 194 | for new_entry in po_new.translated_entries(): 195 | msgids.append([new_entry.msgctxt, new_entry.msgid]) 196 | for old_entry in po_old.translated_entries(): 197 | if 'translator-credits' == new_entry.msgid: 198 | msgids.pop() 199 | break 200 | if new_entry.msgctxt == old_entry.msgctxt and \ 201 | new_entry.msgid == old_entry.msgid and \ 202 | new_entry.msgstr == old_entry.msgstr: 203 | msgids.pop() 204 | break 205 | 206 | print('Count of new messages:', len(msgids)) 207 | 208 | if not msgids: 209 | print('No changes for language %s' % lang_id) 210 | return None 211 | 212 | names = [] 213 | for (msgctxt, msgid) in msgids: 214 | print("looking for msgctxt '%s' msgid '%s' for lang '%s'" % 215 | (msgctxt, msgid, lang_id)) 216 | names = names + who_translated(lang_id, msgctxt, msgid) 217 | lang_name = get_lang_name(po_new, lang_id) 218 | if len(set(names)) == 1: 219 | author = po_new.metadata['Last-Translator'] 220 | else: 221 | author = ', '.join(set(names)) + ' ' 222 | cmd = 'git commit %s.po -m "Update %s translation thanks to %s" --author "%s" -m "[skip ci]"' % \ 223 | (lang_id, lang_name, ', '.join(set(names)), author) 224 | return cmd 225 | 226 | 227 | def download_po_files(urls): 228 | """Download .po files from Launchpad and prepare for Git""" 229 | langs = {} 230 | 231 | for url in urls: 232 | ret = re.search('-([a-z]{2,3}(_[A-Z]{2})?).po$', url, re.I) 233 | lang_id = ret.groups(0)[0] 234 | if not os.path.exists('%s.po' % lang_id): 235 | raise RuntimeError('file %s.po does not exist' % lang_id) 236 | 237 | for url in urls: 238 | doc = read_http(url) 239 | ret = re.search('-([a-z]{2,3}(_[A-Z]{2})?).po$', url, re.I) 240 | lang_id = ret.groups(0)[0] 241 | with open(lang_id + '_new.po', 'w') as f: 242 | f.write(doc) 243 | cmd = process_po(lang_id) 244 | if cmd: 245 | langs[lang_id] = cmd 246 | 247 | for lang_id, cmd in langs.items(): 248 | print('mv %s_new.po %s.po' % (lang_id, lang_id)) 249 | os.rename('%s_new.po' % lang_id, '%s.po' % lang_id) 250 | print(cmd) 251 | # cmd is a Unicode, so encode to bytestring to avoid 252 | # UnicodeEncodeError: 'ascii' codec can't encode character u'\xf6' in position 49: ordinal not in range(128) 253 | os.system(cmd.encode('utf-8')) 254 | 255 | 256 | def go(): 257 | """Main program""" 258 | download_po_files(sys.argv[1:]) 259 | 260 | 261 | if __name__ == '__main__': 262 | go() 263 | -------------------------------------------------------------------------------- /opensuse_build_service_meta.xml: -------------------------------------------------------------------------------- 1 | 2 | andrew_z's Home Project 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | x86_64 25 | 26 | 27 | 28 | x86_64 29 | 30 | 31 | 32 | x86_64 33 | 34 | 35 | 36 | x86_64 37 | 38 | 39 | 40 | x86_64 41 | 42 | 43 | 44 | x86_64 45 | 46 | 47 | 48 | x86_64 49 | 50 | 51 | 52 | aarch64 53 | x86_64 54 | 55 | 56 | 57 | aarch64 58 | x86_64 59 | 60 | 61 | 62 | x86_64 63 | 64 | 65 | 66 | x86_64 67 | 68 | 69 | 70 | x86_64 71 | 72 | 73 | 74 | 75 | x86_64 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /purge_old_ci_builds.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # vim: ts=4:sw=4:expandtab 3 | 4 | """ 5 | Copyright (C) 2016-2025 by Andrew Ziem. All rights reserved. 6 | License GPLv3+: GNU GPL version 3 or later . 7 | This is free software: you are free to change and redistribute it. 8 | There is NO WARRANTY, to the extent permitted by law. 9 | 10 | Windows builds are frequently and automatically published to 11 | ci.bleachbit.org. This script helps purge the older builds. 12 | """ 13 | 14 | import subprocess 15 | import unittest 16 | from packaging.version import parse as parse_version 17 | 18 | 19 | def key_ver(path): 20 | """Convert an S3 path into a StrictVersion for sorting""" 21 | ver_str = path.split('/')[4] 22 | try: 23 | # Extract version number before the hyphen 24 | ver_str = ver_str.split('-')[0] 25 | ret = parse_version(ver_str) 26 | except ValueError: 27 | print('Not a recognizable version:', ver_str, path) 28 | ret = ver_str 29 | return ret 30 | 31 | def get_dirs(): 32 | """Get the list of directories from S3.""" 33 | args = ['s3cmd', 'ls', 's3://bleachbitci/dl/'] 34 | ls_raw = subprocess.check_output(args) 35 | ls_lines = ls_raw.decode().split('\n') 36 | 37 | # get relevant directories 38 | dirs = [] 39 | for line in ls_lines: 40 | if not line: 41 | break 42 | line_s = line.split() 43 | if not len(line_s) == 2: 44 | break 45 | if line_s[0] != 'DIR': 46 | break 47 | dirs.append(line_s[1]) 48 | 49 | # Sort by version number, keeping the newest first. 50 | dirs.sort(key=key_ver, reverse=True) 51 | return dirs 52 | 53 | 54 | class TestKeyVer(unittest.TestCase): 55 | def test_key_ver(self): 56 | # Test cases 57 | test_cases = [ 58 | ("s3://bleachbitci/dl/4.6.2.2665-v4.6.2/", "4.6.2.2665"), 59 | ("s3://bleachbitci/dl/4.9.0.2773-certificate_verify/", "4.9.0.2773"), 60 | ] 61 | for path, expected in test_cases: 62 | with self.subTest(path=path): 63 | # Use parse_version for both actual and expected values 64 | self.assertEqual(key_ver(path), parse_version(expected)) 65 | 66 | def main(): 67 | """Main function.""" 68 | dirs = get_dirs() 69 | assert dirs is not None 70 | assert len(dirs) > 3 71 | # Keep the newest builds. 72 | keep_newest_n = 5 73 | print(f'Keeping the following {keep_newest_n} newest directories:') 74 | for d in dirs[:keep_newest_n]: 75 | print(f' {d}') 76 | print() 77 | # Delete the older builds. 78 | if len(dirs) > keep_newest_n: 79 | print('Issue the following command to perform the delete') 80 | print() 81 | # s3cmd 1.6.1 does not support multiple paths to delete at once 82 | # Support was added in its Git repository November 2016. 83 | # For now, issue separate commands. 84 | #delete_dirs = ' '.join(dirs[keep_newest_n:]) 85 | #delete_cmd = 's3cmd del -r {}'.format(delete_dirs) 86 | # print(delete_cmd) 87 | for delete_dir in dirs[keep_newest_n:]: 88 | print(f's3cmd del -r {delete_dir}') 89 | else: 90 | print('nothing to delete') 91 | 92 | 93 | if __name__ == '__main__': 94 | main() 95 | -------------------------------------------------------------------------------- /read_windows_exe_metadata.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Show metadata for a Windows executable. 4 | 5 | This script runs on any platform. 6 | 7 | Copyright (C) 2025 by Andrew Ziem. All rights reserved. 8 | License GPLv3+: GNU GPL version 3 or later . 9 | This is free software: you are free to change and redistribute it. 10 | There is NO WARRANTY, to the extent permitted by law. 11 | """ 12 | import sys 13 | import argparse 14 | 15 | try: 16 | import pefile 17 | except ImportError: 18 | print("Error: pefile module is required. Try running: 'pip install pefile'.") 19 | sys.exit(1) 20 | 21 | 22 | def extract_string_table_metadata(pe, metadata, metadata_fields): 23 | # Extract string table info 24 | if hasattr(pe, 'FileInfo') and pe.FileInfo: 25 | for fileinfo in pe.FileInfo[0]: 26 | if fileinfo.Key.decode() == 'StringFileInfo': 27 | for st in fileinfo.StringTable: 28 | for key, value in st.entries.items(): 29 | decoded_key = key.decode() 30 | decoded_value = value.decode() 31 | 32 | if decoded_key in metadata_fields: 33 | metadata_key = metadata_fields[decoded_key] 34 | metadata[metadata_key] = decoded_value 35 | else: 36 | print(f"Unknown key: {decoded_key}") 37 | 38 | 39 | def extract_metadata(file_path): 40 | try: 41 | pe = pefile.PE(file_path) 42 | except pefile.PEFormatError: 43 | print("Error: Not a valid PE file.") 44 | return {} 45 | 46 | metadata_fields = { 47 | 'FileDescription': 'File description', 48 | 'FileVersion': 'File version', 49 | 'ProductName': 'Product name', 50 | 'ProductVersion': 'Product version', 51 | 'LegalCopyright': 'Legal copyright', 52 | 'LegalTrademarks': 'Legal trademarks', 53 | 'Comments': 'Comments', 54 | 'CompanyName': 'Company name' 55 | } 56 | 57 | # Initialize metadata with defaults 58 | metadata = {field: 'N/A' for field in metadata_fields.values()} 59 | metadata['Digitally signed'] = 'No' 60 | 61 | extract_string_table_metadata(pe, metadata, metadata_fields) 62 | 63 | # Check digital signature 64 | if pe.OPTIONAL_HEADER.DATA_DIRECTORY[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_SECURITY']].Size > 0: 65 | metadata['Digitally signed'] = 'Yes' 66 | 67 | # Print all metadata 68 | for field_name, value in metadata.items(): 69 | print(f"{field_name}: {value}") 70 | 71 | return metadata 72 | 73 | 74 | def main(): 75 | parser = argparse.ArgumentParser( 76 | description='Check metadata of Windows executable') 77 | parser.add_argument('file_path', help='Path to the Windows executable') 78 | parser.add_argument('--require-signature', '-r', action='store_true', 79 | help='Exit with code 1 if digital signature is missing') 80 | args = parser.parse_args() 81 | 82 | metadata = extract_metadata(args.file_path) 83 | 84 | if args.require_signature and metadata.get('Digitally signed', 'No') == 'No': 85 | sys.exit(1) 86 | 87 | 88 | if __name__ == "__main__": 89 | main() 90 | -------------------------------------------------------------------------------- /sdist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (C) 2014 by Andrew Ziem. All rights reserved. 4 | # License GPLv3+: GNU GPL version 3 or later . 5 | # This is free software: you are free to change and redistribute it. 6 | # There is NO WARRANTY, to the extent permitted by law. 7 | # 8 | # Build clean source tarball 9 | # 10 | 11 | 12 | NAME=bleachbit 13 | # Use CLI argument for branch if provided, otherwise use default 14 | BRANCH=${1:-master} 15 | GITD=/tmp/bleachbit_git 16 | GITURL=https://github.com/bleachbit/bleachbit.git 17 | 18 | if [[ -d $GITD ]]; then 19 | echo "rm -rf $GITD" 20 | rm -rf "$GITD" 21 | fi 22 | echo "mkdir $GITD" 23 | mkdir $GITD || exit 1 24 | cd $GITD || exit 1 25 | echo "git clone $BRANCH" 26 | time git clone --depth 1 $GITURL -b $BRANCH $GITDIR || { echo "git clone failed"; exit 1; } 27 | cd bleachbit || { echo "cd bleachbit failed"; exit 1; } 28 | 29 | echo "python setup" 30 | VER=$(python3 bleachbit.py --version | perl -ne 'print if s/^BleachBit version (.*)/$1/') 31 | NAMEV=${NAME}-${VER} 32 | make clean 33 | REV=`git rev-parse --short HEAD` 34 | echo "revision = \"$REV\"" > bleachbit/Revision.py 35 | python3 setup.py sdist --formats=bztar,gztar || { echo "python3 setup.py failed"; exit 1; } 36 | 37 | [[ -e "dist/$NAMEV.tar.gz" ]] || { echo dist/$NAMEV.tar.gz missing; exit 1; } 38 | 39 | 40 | echo "creating LZMA tarball" 41 | bzcat dist/$NAMEV.tar.bz2 | xz -9 - > dist/$NAMEV.tar.lzma 42 | [[ -e "dist/$NAMEV.tar.lzma" ]] || { echo dist/$NAMEV.tar.lzma missing; exit 1; } 43 | 44 | echo Success! 45 | -------------------------------------------------------------------------------- /summarize_translations.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # vim: ts=4:sw=4:expandtab 3 | 4 | # Copyright (C) 2014 by Andrew Ziem. All rights reserved. 5 | # License GPLv3+: GNU GPL version 3 or later . 6 | # This is free software: you are free to change and redistribute it. 7 | # There is NO WARRANTY, to the extent permitted by law. 8 | # 9 | # 10 | 11 | 12 | import gettext 13 | import os 14 | import os.path 15 | import re 16 | import subprocess 17 | import sys 18 | 19 | dir_bb_root = os.path.abspath('../bleachbit') 20 | dir_bb_locale = os.path.abspath('../bleachbit/locale') 21 | dir_bb_po = os.path.abspath('../bleachbit/po') 22 | 23 | os.chdir(dir_bb_root) 24 | sys.path.append(dir_bb_root) 25 | 26 | import setup 27 | import bleachbit.Unix 28 | 29 | gettext.bindtextdomain('bleachbit', dir_bb_locale) 30 | gettext.textdomain('bleachbit') 31 | gettext.install('bleachbit', dir_bb_locale) 32 | 33 | strs = [ 34 | 'Delete', 35 | 'Unnecessary file cleaner', 36 | 'Free space and maintain privacy', 37 | 'Program to clean unnecessary files', 38 | 'translator-credits'] 39 | 40 | summary_str = 'Free space and maintain privacy' 41 | 42 | 43 | def get_translation_progress(lang): 44 | """Get the progress for a translation""" 45 | assert (isinstance(lang, str)) 46 | oldcwd = os.getcwd() 47 | os.chdir(dir_bb_po) 48 | args = ['msgfmt', '--statistics', '-o', lang + '.mo', lang + '.po'] 49 | outputs = subprocess.Popen( 50 | args, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() 51 | output = outputs[1].decode() 52 | os.chdir(oldcwd) 53 | # 53 translated messages, 82 untranslated messages. 54 | match = re.search('([0-9]+) translated messages.* ([0-9]+) untranslated message', output) 55 | if match: 56 | # you should run 'make refresh-po' to update untranslated 57 | translated = int(match.group(1)) 58 | untranslated = int(match.group(2)) 59 | return "%.0f%%" % (100. * translated / (untranslated + translated)) 60 | match = re.search('([0-9]+) translated messages', output) 61 | if match: 62 | return "100%" 63 | sys.stderr.write("Unknown output for language '%s': '%s'" % (lang, output)) 64 | return "?" 65 | 66 | 67 | def main(): 68 | print (""" 69 | 70 | 71 | 72 | 73 | 74 | """) 75 | print ('\n') 76 | print(('\n' % summary_str)) 77 | for langid in sorted(setup.supported_languages()): 78 | assert (isinstance(langid, str)) 79 | print('' % (langid)) 80 | native_name = bleachbit.Unix.Locales.native_locale_names[langid] 81 | print('' % langid) 82 | print('' % (native_name)) 83 | lang = gettext.translation( 84 | 'bleachbit', localedir=dir_bb_locale, languages=[langid]) 85 | lang.install() 86 | stats = get_translation_progress(langid) 87 | print('' % (stats)) 88 | free_space = lang.gettext(summary_str) 89 | # print 'free_space =', free_space 90 | if free_space == summary_str: 91 | free_space = ' ' 92 | print('' % free_space) 93 | print('') 94 | print('
    CodeNamePercentage translated"%s"
    %s%s%s%s
    ') 95 | 96 | main() 97 | -------------------------------------------------------------------------------- /summarize_translations.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (C) 2014 by Andrew Ziem. All rights reserved. 4 | # License GPLv3+: GNU GPL version 3 or later . 5 | # This is free software: you are free to change and redistribute it. 6 | # There is NO WARRANTY, to the extent permitted by law. 7 | # 8 | # 9 | 10 | cd ../bleachbit/po 11 | make clean 12 | make refresh-po > /dev/null 13 | make local > /dev/null 14 | cd ../../bleachbit-misc 15 | python3 summarize_translations.py > /tmp/summarized_translations.html 16 | cd ../bleachbit/po 17 | git checkout *po 18 | cd - 19 | xdg-open /tmp/summarized_translations.html 20 | 21 | -------------------------------------------------------------------------------- /svg2ico.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (C) 2012 by Andrew Ziem. All rights reserved. 4 | # License GPLv3+: GNU GPL version 3 or later . 5 | # This is free software: you are free to change and redistribute it. 6 | # There is NO WARRANTY, to the extent permitted by law. 7 | # 8 | # Convert SVG to a Windows application icon 9 | # 10 | 11 | 12 | DIR=/tmp/ 13 | 14 | function svg2ico { 15 | echo svg2ico $1 16 | rm -f $DIR/icon.png $DIR/bleachbit_$1.png 17 | convert -filter Box -background none bleachbit.svg -resize $1x$1 $DIR/icon.png 18 | optipng -quiet -o7 $DIR/icon.png 19 | pngcrush -rem allb -reduce -brute -q $DIR/icon.png $DIR/bleachbit_$1.png 20 | } 21 | 22 | svg2ico 16 23 | svg2ico 32 24 | svg2ico 48 25 | svg2ico 128 26 | 27 | png2ico $DIR/bleachbit.ico $DIR/bleachbit_{128,48,32,16}.png 28 | 29 | -------------------------------------------------------------------------------- /svn2git.sh: -------------------------------------------------------------------------------- 1 | # A one-time conversion from SVN to Git 2 | 3 | # svn2git fails with an error 4 | # 2>&1 git branch --track "0.2.0" "remotes/svn/0.2.0" 5 | #svn2git http://svn.code.sf.net/p/bleachbit/code/ --exclude bonus --exclude misc --tags releases 6 | 7 | # This method loses release history, but it keeps all the commits (except for the bonus and misc directories). 8 | time git svn clone http://svn.code.sf.net/p/bleachbit/code/trunk 9 | 10 | # Fix committer (there was only one direct committer) 11 | cd trunk 12 | git filter-branch -f --env-filter "GIT_AUTHOR_NAME='Andrew Ziem'; GIT_AUTHOR_EMAIL='ahz001@gmail.com'; GIT_COMMITTER_NAME='Andrew Ziem'; GIT_COMMITTER_EMAIL='ahz001@gmail.com';" HEAD 13 | 14 | # Push to Github 15 | git remote add origin git@github.com:az0/bleachbit.git 16 | git push origin master 17 | 18 | # Repeat for misc 19 | time git svn clone http://svn.code.sf.net/p/bleachbit/code/misc 20 | cd misc 21 | git filter-branch -f --env-filter "GIT_AUTHOR_NAME='Andrew Ziem'; GIT_AUTHOR_EMAIL='ahz001@gmail.com'; GIT_COMMITTER_NAME='Andrew Ziem'; GIT_COMMITTER_EMAIL='ahz001@gmail.com';" HEAD 22 | git remote add origin git@github.com:az0/bleachbit-misc.git 23 | git push origin master 24 | -------------------------------------------------------------------------------- /test_check_update.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # vim: ts=4:sw=4:expandtab 3 | 4 | """ 5 | Copyright (C) 2014-2025 by Andrew Ziem. All rights reserved. 6 | License GPLv3+: GNU GPL version 3 or later . 7 | This is free software: you are free to change and redistribute it. 8 | There is NO WARRANTY, to the extent permitted by law. 9 | 10 | Verify the online update notification system returns the right version numbers 11 | """ 12 | 13 | import os 14 | import sys 15 | import time 16 | 17 | dir_bb_root = os.path.abspath('../bleachbit') 18 | os.chdir(dir_bb_root) 19 | sys.path.append(dir_bb_root) 20 | 21 | import bleachbit # noqa: E402 22 | import bleachbit.Update # noqa: E402 23 | 24 | LATEST_STABLE = '5.0.0' 25 | LATEST_BETA = '4.9.2 (beta)' 26 | # tuple in the format 27 | # (current version sent, version returned 1, version returned 2) 28 | TESTS = \ 29 | (('3.0', LATEST_STABLE, None), 30 | ('3.1.0', LATEST_STABLE, None), 31 | ('3.2.0', LATEST_STABLE, None), 32 | ('3.9.0', LATEST_STABLE, None), 33 | ('3.9.2', LATEST_STABLE, None), 34 | ('4.0.0', LATEST_STABLE, None), 35 | ('4.1.0', LATEST_STABLE, None), 36 | ('4.2.0', LATEST_STABLE, None), 37 | ('4.4.0', LATEST_STABLE, None), 38 | ('4.4.2', LATEST_STABLE, None), 39 | ('4.5.0', LATEST_STABLE, None), 40 | ('4.5.1', LATEST_STABLE, None), 41 | ('4.6.0', LATEST_STABLE, None), 42 | ('4.6.1', LATEST_STABLE, None), 43 | ('4.6.2', LATEST_STABLE, None), 44 | ('4.6.3', LATEST_STABLE, None), 45 | ('4.9.0', LATEST_STABLE, None), 46 | ('4.9.1', LATEST_STABLE, None), 47 | ('4.9.2', LATEST_STABLE, None), 48 | ('5.0.0', None, None)) 49 | 50 | 51 | def do_test(app_version, version1_expected, version2_expected, base_url=bleachbit.base_url): 52 | """Do a single test""" 53 | print('\n', '*' * 10, app_version) 54 | bleachbit.APP_VERSION = app_version 55 | bleachbit.update_check_url = f'{base_url}/update/{bleachbit.APP_VERSION}' 56 | 57 | v1e = version1_expected # e=expected 58 | v2e = version2_expected 59 | 60 | start_time = time.time() 61 | cu = bleachbit.Update.check_updates(True, False, None, None) 62 | elapsed_time_ms = (time.time() - start_time) * 1000 63 | 64 | print(f'returned={cu}, time={elapsed_time_ms:.2f}ms') 65 | 66 | test_success = True 67 | if cu == (): 68 | v1r = None # r=returned 69 | v2r = None 70 | else: 71 | if cu[0]: 72 | v1r = cu[0][0] 73 | else: 74 | v1r = None 75 | if len(cu) > 1: 76 | v2r = cu[1][0] 77 | else: 78 | v2r = None 79 | if not v1e == v1r: 80 | print( 81 | f'ERROR: sent version {test[0]}, expected v1={v1e}, returned v1={v1r}') 82 | test_success = False 83 | if not v2e == v2r: 84 | print( 85 | f'ERROR: sent version {test[0]}, expected v2={v2e}, returned v2={v2r}') 86 | test_success = False 87 | 88 | return elapsed_time_ms, test_success 89 | 90 | 91 | def main(): 92 | """Main function""" 93 | base_url = None 94 | if len(sys.argv) > 1: 95 | base_url = sys.argv[1] 96 | 97 | times_ms = [] 98 | test_count = 0 99 | success_count = 0 100 | error_count = 0 101 | 102 | for (app_version, version1_expected, version2_expected) in TESTS: 103 | elapsed_time_ms, test_success = do_test( 104 | app_version, version1_expected, version2_expected, base_url) 105 | test_count += 1 106 | times_ms.append(elapsed_time_ms) 107 | if test_success: 108 | success_count += 1 109 | else: 110 | error_count += 1 111 | 112 | print( 113 | f"\nTest summary: {test_count} total, {success_count} successful, {error_count} failed") 114 | if times_ms: 115 | min_time = min(times_ms) 116 | avg_time = sum(times_ms) / len(times_ms) 117 | max_time = max(times_ms) 118 | print( 119 | f" min/avg/max = {min_time:.2f}/{avg_time:.2f}/{max_time:.2f} ms") 120 | 121 | 122 | if __name__ == '__main__': 123 | main() 124 | --------------------------------------------------------------------------------