Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 2be0fde

Browse files
authored
Fix empty url_cache_thumbnails/yyyy-mm-dd/ directories being left behind (#10924)
1 parent 9fd057b commit 2be0fde

File tree

3 files changed

+75
-31
lines changed

3 files changed

+75
-31
lines changed

changelog.d/10924.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a bug where empty `yyyy-mm-dd/` directories would be left behind in the media store's `url_cache_thumbnails/` directory.

synapse/rest/media/v1/preview_url_resource.py

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373

7474
ONE_HOUR = 60 * 60 * 1000
7575
ONE_DAY = 24 * ONE_HOUR
76+
IMAGE_CACHE_EXPIRY_MS = 2 * ONE_DAY
7677

7778

7879
@attr.s(slots=True, frozen=True, auto_attribs=True)
@@ -496,6 +497,27 @@ async def _expire_url_cache_data(self) -> None:
496497
logger.info("Still running DB updates; skipping expiry")
497498
return
498499

500+
def try_remove_parent_dirs(dirs: Iterable[str]) -> None:
501+
"""Attempt to remove the given chain of parent directories
502+
503+
Args:
504+
dirs: The list of directory paths to delete, with children appearing
505+
before their parents.
506+
"""
507+
for dir in dirs:
508+
try:
509+
os.rmdir(dir)
510+
except FileNotFoundError:
511+
# Already deleted, continue with deleting the rest
512+
pass
513+
except OSError as e:
514+
# Failed, skip deleting the rest of the parent dirs
515+
if e.errno != errno.ENOTEMPTY:
516+
logger.warning(
517+
"Failed to remove media directory: %r: %s", dir, e
518+
)
519+
break
520+
499521
# First we delete expired url cache entries
500522
media_ids = await self.store.get_expired_url_cache(now)
501523

@@ -504,20 +526,16 @@ async def _expire_url_cache_data(self) -> None:
504526
fname = self.filepaths.url_cache_filepath(media_id)
505527
try:
506528
os.remove(fname)
529+
except FileNotFoundError:
530+
pass # If the path doesn't exist, meh
507531
except OSError as e:
508-
# If the path doesn't exist, meh
509-
if e.errno != errno.ENOENT:
510-
logger.warning("Failed to remove media: %r: %s", media_id, e)
511-
continue
532+
logger.warning("Failed to remove media: %r: %s", media_id, e)
533+
continue
512534

513535
removed_media.append(media_id)
514536

515-
try:
516-
dirs = self.filepaths.url_cache_filepath_dirs_to_delete(media_id)
517-
for dir in dirs:
518-
os.rmdir(dir)
519-
except Exception:
520-
pass
537+
dirs = self.filepaths.url_cache_filepath_dirs_to_delete(media_id)
538+
try_remove_parent_dirs(dirs)
521539

522540
await self.store.delete_url_cache(removed_media)
523541

@@ -530,44 +548,38 @@ async def _expire_url_cache_data(self) -> None:
530548
# These may be cached for a bit on the client (i.e., they
531549
# may have a room open with a preview url thing open).
532550
# So we wait a couple of days before deleting, just in case.
533-
expire_before = now - 2 * ONE_DAY
551+
expire_before = now - IMAGE_CACHE_EXPIRY_MS
534552
media_ids = await self.store.get_url_cache_media_before(expire_before)
535553

536554
removed_media = []
537555
for media_id in media_ids:
538556
fname = self.filepaths.url_cache_filepath(media_id)
539557
try:
540558
os.remove(fname)
559+
except FileNotFoundError:
560+
pass # If the path doesn't exist, meh
541561
except OSError as e:
542-
# If the path doesn't exist, meh
543-
if e.errno != errno.ENOENT:
544-
logger.warning("Failed to remove media: %r: %s", media_id, e)
545-
continue
562+
logger.warning("Failed to remove media: %r: %s", media_id, e)
563+
continue
546564

547-
try:
548-
dirs = self.filepaths.url_cache_filepath_dirs_to_delete(media_id)
549-
for dir in dirs:
550-
os.rmdir(dir)
551-
except Exception:
552-
pass
565+
dirs = self.filepaths.url_cache_filepath_dirs_to_delete(media_id)
566+
try_remove_parent_dirs(dirs)
553567

554568
thumbnail_dir = self.filepaths.url_cache_thumbnail_directory(media_id)
555569
try:
556570
shutil.rmtree(thumbnail_dir)
571+
except FileNotFoundError:
572+
pass # If the path doesn't exist, meh
557573
except OSError as e:
558-
# If the path doesn't exist, meh
559-
if e.errno != errno.ENOENT:
560-
logger.warning("Failed to remove media: %r: %s", media_id, e)
561-
continue
574+
logger.warning("Failed to remove media: %r: %s", media_id, e)
575+
continue
562576

563577
removed_media.append(media_id)
564578

565-
try:
566-
dirs = self.filepaths.url_cache_thumbnail_dirs_to_delete(media_id)
567-
for dir in dirs:
568-
os.rmdir(dir)
569-
except Exception:
570-
pass
579+
dirs = self.filepaths.url_cache_thumbnail_dirs_to_delete(media_id)
580+
# Note that one of the directories to be deleted has already been
581+
# removed by the `rmtree` above.
582+
try_remove_parent_dirs(dirs)
571583

572584
await self.store.delete_url_cache_media(removed_media)
573585

tests/rest/media/v1/test_url_preview.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@
2121
from twisted.test.proto_helpers import AccumulatingProtocol
2222

2323
from synapse.config.oembed import OEmbedEndpointConfig
24+
from synapse.rest.media.v1.preview_url_resource import IMAGE_CACHE_EXPIRY_MS
2425
from synapse.util.stringutils import parse_and_validate_mxc_uri
2526

2627
from tests import unittest
2728
from tests.server import FakeTransport
2829
from tests.test_utils import SMALL_PNG
30+
from tests.utils import MockClock
2931

3032
try:
3133
import lxml
@@ -851,3 +853,32 @@ def test_storage_providers_exclude_thumbnails(self):
851853
404,
852854
"URL cache thumbnail was unexpectedly retrieved from a storage provider",
853855
)
856+
857+
def test_cache_expiry(self):
858+
"""Test that URL cache files and thumbnails are cleaned up properly on expiry."""
859+
self.preview_url.clock = MockClock()
860+
861+
_host, media_id = self._download_image()
862+
863+
file_path = self.preview_url.filepaths.url_cache_filepath(media_id)
864+
file_dirs = self.preview_url.filepaths.url_cache_filepath_dirs_to_delete(
865+
media_id
866+
)
867+
thumbnail_dir = self.preview_url.filepaths.url_cache_thumbnail_directory(
868+
media_id
869+
)
870+
thumbnail_dirs = self.preview_url.filepaths.url_cache_thumbnail_dirs_to_delete(
871+
media_id
872+
)
873+
874+
self.assertTrue(os.path.isfile(file_path))
875+
self.assertTrue(os.path.isdir(thumbnail_dir))
876+
877+
self.preview_url.clock.advance_time_msec(IMAGE_CACHE_EXPIRY_MS + 1)
878+
self.get_success(self.preview_url._expire_url_cache_data())
879+
880+
for path in [file_path] + file_dirs + [thumbnail_dir] + thumbnail_dirs:
881+
self.assertFalse(
882+
os.path.exists(path),
883+
f"{os.path.relpath(path, self.media_store_path)} was not deleted",
884+
)

0 commit comments

Comments
 (0)