bf56f5fff7b6c142ee3845dbf11eae9aa12adcb9
[freebsd-maintainer-scripts/.git] / distfile-cleaner.py
1 #!/usr/bin/env python3
2
3 """
4 This simple script is an helper to purge old files left into
5 freefall:~/public_distfiles for ages to reclaim disk space. 
6
7 It uses paramiko, /bin/ls and /bin/rm to delete the obsolete files
8
9 --
10 sbz@FreeBSD.org
11 """
12
13 import argparse
14 import datetime
15 import time
16 import sys
17
18 from dataclasses import dataclass
19
20 import paramiko
21
22 # by default keep file for at least 5 years
23 MAX_KEEP_YEAR = 5
24
25 __version__ = "1.0"
26
27 @dataclass
28 class FileEntry:
29     name: str
30     date: str
31
32     def is_older(self) -> bool:
33
34         current_year = datetime.datetime.fromtimestamp(time.time()).year
35         year, month, day = self.date.split("-")
36         if current_year - int(year) >= MAX_KEEP_YEAR:
37             return True
38
39         return False
40
41
42 class Checker(object):
43     """Class to purge old FreeBSD distfiles"""
44
45     def __init__(self, host: str="freefall.freebsd.org", user: str="sbz",
46                  dryrun: bool=False):
47         self.host = host
48         self.user = user
49         self.path = f"/home/{self.user}/public_distfiles/"
50         self.list_cmd = f"ls -ltr -D%F {self.path}"
51         self.dryrun = dryrun
52
53     def remote_exec(self, cmd: str) -> str:
54         client = paramiko.SSHClient()
55         client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
56         client.load_system_host_keys()
57         client.connect(self.host)
58
59         try:
60             stdin, stdout, stderr = client.exec_command(cmd)
61         except paramiko.SSHException as e:
62             print(f"Failed to execute remote {cmd}: (stderr: {stderr.read()}")
63
64         return stdout
65
66     def clean_files(self) -> bool:
67         stdout = self.remote_exec(self.list_cmd)
68         files = self.read_stream(stdout)
69         if len(files) == 0:
70             return False
71
72         for f in files:
73             mode = "+" if not self.dryrun else "Dry run"
74             print(f"[{mode}] Cleaning {f.name} dating from {f.date}", end=' ')
75             self.clean_file(f.name)
76             print(f"\N{check mark}" if not self.dryrun else f"\N{cross mark}")
77
78         return True
79         
80     def read_stream(self, stream_input: paramiko.Channel) -> list:
81         lines = []
82
83         for line in stream_input.readlines()[1:]:
84             if line == "":
85                 continue
86             col = line.strip().split()
87             f = FileEntry(col[-1], col[-2])
88             if f.is_older():
89                 lines.append(f)
90
91         return lines
92
93     def clean_file(self, file_name: str):
94         clean_cmd = f"rm -vf {self.path}{file_name}"
95         if self.dryrun:
96             clean_cmd = "echo " + clean_cmd
97         stdout = self.remote_exec(clean_cmd)
98
99 def main() -> int:
100     global MAX_KEEP_YEAR
101
102     parser = argparse.ArgumentParser(
103         description="Purge old FreeBSD files stored in ~/public_distfiles"
104     )
105
106     parser.add_argument(
107         "-n", "--dryrun", action='store_true',
108         help="Dry run mode. Do not execute remote command"
109     )
110     parser.add_argument(
111         "-m", "--max-year", type=int,
112         help="Maximum year to keep the files"
113     )
114
115     args = parser.parse_args()
116     if args.max_year:
117         MAX_KEEP_YEAR = args.max_year
118
119     checker = Checker()
120     if args.dryrun:
121         checker = Checker(dryrun=True)
122
123     print(f"[+] Only files older than {MAX_KEEP_YEAR}")
124     if not checker.clean_files():
125         print("Nothing to clean.")
126         return 1
127
128     return 0
129
130 if __name__ == "__main__":
131     try:
132         sys.exit(main())
133     except KeyboardInterrupt as e:
134         sys.exit(0)
135     except SystemExit as sysexit:
136         if sysexit.code != 0:
137             raise
138         else:
139             sys.exit(sysexit.code)
140     except Exception:
141         raise