Skip to content

Commit fa9aa5b

Browse files
committed
guard expire/fsreport file iteration against vanishing, improve reporting
also activates actual deletion (after quite some dry test runs on nine)
1 parent 0155f32 commit fa9aa5b

File tree

3 files changed

+76
-19
lines changed

3 files changed

+76
-19
lines changed

chatmaild/src/chatmaild/expire.py

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,30 @@ def iter_mailboxes(basedir, maxnum):
2222
print_info(f"no mailboxes found at: {basedir}")
2323
return
2424

25-
for name in os.listdir(basedir)[:maxnum]:
25+
for name in os_listdir_if_exists(basedir)[:maxnum]:
2626
if "@" in name:
2727
yield MailboxStat(basedir + "/" + name)
2828

2929

30+
def get_file_entry(path):
31+
"""return a FileEntry or None if the path does not exist or is not a regular file."""
32+
try:
33+
st = os.stat(path)
34+
except FileNotFoundError:
35+
return None
36+
if not S_ISREG(st.st_mode):
37+
return None
38+
return FileEntry(path, st.st_mtime, st.st_size)
39+
40+
41+
def os_listdir_if_exists(path):
42+
"""return a list of names obtained from os.listdir or an empty list if the path does not exist."""
43+
try:
44+
return os.listdir(path)
45+
except FileNotFoundError:
46+
return []
47+
48+
3049
class MailboxStat:
3150
last_login = None
3251

@@ -40,19 +59,23 @@ def __init__(self, basedir):
4059

4160
# scan all relevant files (without recursion)
4261
old_cwd = os.getcwd()
43-
os.chdir(self.basedir)
44-
for name in os.listdir("."):
62+
try:
63+
os.chdir(self.basedir)
64+
except FileNotFoundError:
65+
return
66+
for name in os_listdir_if_exists("."):
4567
if name in ("cur", "new", "tmp"):
46-
for msg_name in os.listdir(name):
47-
relpath = name + "/" + msg_name
48-
st = os.stat(relpath)
49-
self.messages.append(FileEntry(relpath, st.st_mtime, st.st_size))
68+
for msg_name in os_listdir_if_exists(name):
69+
entry = get_file_entry(f"{name}/{msg_name}")
70+
if entry is not None:
71+
self.messages.append(entry)
72+
5073
else:
51-
st = os.stat(name)
52-
if S_ISREG(st.st_mode):
53-
self.extrafiles.append(FileEntry(name, st.st_mtime, st.st_size))
74+
entry = get_file_entry(name)
75+
if entry is not None:
76+
self.extrafiles.append(entry)
5477
if name == "password":
55-
self.last_login = st.st_mtime
78+
self.last_login = entry.mtime
5679
self.extrafiles.sort(key=lambda x: -x.size)
5780
os.chdir(old_cwd)
5881

@@ -80,9 +103,13 @@ def remove_mailbox(self, mboxdir):
80103
shutil.rmtree(mboxdir)
81104
self.del_mboxes += 1
82105

83-
def remove_file(self, path):
106+
def remove_file(self, path, mtime=None):
84107
if self.verbose:
85-
print_info(f"removing {path}")
108+
if mtime is not None:
109+
date = datetime.fromtimestamp(mtime).strftime("%b %d")
110+
print_info(f"removing {date} {path}")
111+
else:
112+
print_info(f"removing {path}")
86113
if not self.dry:
87114
try:
88115
os.unlink(path)
@@ -104,18 +131,27 @@ def process_mailbox_stat(self, mbox):
104131
return
105132

106133
# all to-be-removed files are relative to the mailbox basedir
107-
os.chdir(mbox.basedir)
134+
try:
135+
os.chdir(mbox.basedir)
136+
except FileNotFoundError:
137+
print_info(f"mailbox not found/vanished {mbox.basedir}")
138+
return
139+
108140
mboxname = os.path.basename(mbox.basedir)
109141
if self.verbose:
110-
print_info(f"checking for mailbox messages in: {mboxname}")
142+
date = datetime.fromtimestamp(mbox.last_login) if mbox.last_login else None
143+
if date:
144+
print_info(f"checking mailbox {date.strftime('%b %d')} {mboxname}")
145+
else:
146+
print_info(f"checking mailbox (no last_login) {mboxname}")
111147
self.all_files += len(mbox.messages)
112148
for message in mbox.messages:
113149
if message.mtime < cutoff_mails:
114-
self.remove_file(message.relpath)
150+
self.remove_file(message.relpath, mtime=message.mtime)
115151
elif message.size > 200000 and message.mtime < cutoff_large_mails:
116152
# we only remove noticed large files (not unnoticed ones in new/)
117153
if message.relpath.startswith("cur/"):
118-
self.remove_file(message.relpath)
154+
self.remove_file(message.relpath, mtime=message.mtime)
119155
else:
120156
continue
121157
changed = True

chatmaild/src/chatmaild/tests/test_expire.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66

77
import pytest
88

9-
from chatmaild.expire import FileEntry, MailboxStat, iter_mailboxes
9+
from chatmaild.expire import (
10+
FileEntry,
11+
MailboxStat,
12+
get_file_entry,
13+
iter_mailboxes,
14+
os_listdir_if_exists,
15+
)
1016
from chatmaild.expire import main as expiry_main
1117
from chatmaild.fsreport import main as report_main
1218

@@ -127,3 +133,18 @@ def test_expiry_cli_old_files(capsys, example_config, mbox1):
127133
pytest.fail(f"failed to remove {path}\n{err}")
128134

129135
assert "shouldstay" not in err
136+
137+
138+
def test_get_file_entry(tmp_path):
139+
assert get_file_entry(str(tmp_path.joinpath("123123"))) is None
140+
p = tmp_path.joinpath("x")
141+
p.write_text("hello")
142+
entry = get_file_entry(str(p))
143+
assert entry.size == 5
144+
assert entry.mtime
145+
146+
147+
def test_os_listdir_if_exists(tmp_path):
148+
tmp_path.joinpath("x").write_text("hello")
149+
assert len(os_listdir_if_exists(str(tmp_path))) == 1
150+
assert len(os_listdir_if_exists(str(tmp_path.joinpath("123123")))) == 0

cmdeploy/src/cmdeploy/service/chatmail-expire.service.f

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
[Service]
66
Type=oneshot
77
User=vmail
8-
ExecStart=/usr/local/lib/chatmaild/venv/bin/chatmail-expire /usr/local/lib/chatmaild/chatmail.ini -v
8+
ExecStart=/usr/local/lib/chatmaild/venv/bin/chatmail-expire /usr/local/lib/chatmaild/chatmail.ini -v --remove
99

0 commit comments

Comments
 (0)