Skip to content

Commit bfa3dd6

Browse files
authored
fix #79: Remove the files generated by processors after update or deletion (#84)
1 parent df8d2c6 commit bfa3dd6

File tree

3 files changed

+53
-11
lines changed

3 files changed

+53
-11
lines changed

sqlalchemy_file/helpers.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import re
55
from builtins import RuntimeError
66
from tempfile import SpooledTemporaryFile
7-
from typing import Any, Dict, Union
7+
from typing import Any, Dict, List, Union
88

99
INMEMORY_FILESIZE = 1024 * 1024
1010
LOCAL_STORAGE_DRIVER_NAME = "Local Storage"
@@ -74,3 +74,8 @@ def convert_size(size: Union[str, int]) -> int:
7474
si_map = {"k": 1000, "K": 1000, "M": 1000**2, "Ki": 1024, "Mi": 1024**2}
7575
return int(value) * si_map[si]
7676
return size
77+
78+
79+
def flatmap(lists: List[List[Any]]) -> List[Any]:
80+
"""Flattens a list of lists into a single list."""
81+
return [value for _list in lists for value in _list]

sqlalchemy_file/types.py

+13-8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from sqlalchemy.orm import ColumnProperty, Mapper, Session, SessionTransaction
66
from sqlalchemy.orm.attributes import get_history
77
from sqlalchemy_file.file import File
8+
from sqlalchemy_file.helpers import flatmap
89
from sqlalchemy_file.mutable_list import MutableList
910
from sqlalchemy_file.processors import Processor, ThumbnailGenerator
1011
from sqlalchemy_file.storage import StorageManager
@@ -190,10 +191,10 @@ def extract_files_from_history(cls, data: Union[Tuple[()], List[Any]]) -> List[s
190191
paths = []
191192
for item in data:
192193
if isinstance(item, list):
193-
paths.extend([f["path"] for f in item])
194+
paths.extend([f["files"] for f in item])
194195
elif isinstance(item, File):
195-
paths.append(item["path"])
196-
return paths
196+
paths.append(item["files"])
197+
return flatmap(paths)
197198

198199
@classmethod
199200
def _mapper_configured(cls, mapper: Mapper, class_: Any) -> None: # type: ignore[type-arg]
@@ -242,10 +243,12 @@ def _after_delete(cls, mapper: Mapper, _: Connection, obj: Any) -> None: # type
242243
if value is not None:
243244
cls.add_old_files_to_session(
244245
inspect(obj).session,
245-
[
246-
f["path"]
247-
for f in (value if isinstance(value, list) else [value])
248-
],
246+
flatmap(
247+
[
248+
f["files"]
249+
for f in (value if isinstance(value, list) else [value])
250+
]
251+
),
249252
)
250253

251254
@classmethod
@@ -280,7 +283,9 @@ def _before_update(cls, mapper: Mapper, _: Connection, obj: Any) -> None: # typ
280283
)
281284
if isinstance(value, MutableList):
282285
_removed = getattr(value, "_removed", ())
283-
cls.add_old_files_to_session(session, [f["path"] for f in _removed])
286+
cls.add_old_files_to_session(
287+
session, flatmap([f["files"] for f in _removed])
288+
)
284289

285290
@classmethod
286291
def _before_insert(cls, mapper: Mapper, _: Connection, obj: Any) -> None: # type: ignore[type-arg]

tests/test_processor.py

+34-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import tempfile
33

44
import pytest
5+
from libcloud.storage.types import ObjectDoesNotExistError
6+
from PIL import Image
57
from sqlalchemy import Column, Integer, String, select
68
from sqlalchemy.orm import Session, declarative_base
79
from sqlalchemy_file.storage import StorageManager
@@ -51,8 +53,6 @@ def setup_method(self, method) -> None:
5153

5254
def test_create_image_with_thumbnail(self, fake_image) -> None:
5355
with Session(engine) as session:
54-
from PIL import Image
55-
5656
session.add(Book(title="Pointless Meetings", cover=fake_image))
5757
session.flush()
5858
book = session.execute(
@@ -66,6 +66,38 @@ def test_create_image_with_thumbnail(self, fake_image) -> None:
6666
assert book.cover["thumbnail"]["width"] == thumbnail.width
6767
assert book.cover["thumbnail"]["height"] == thumbnail.height
6868

69+
def test_update_image_with_thumbnail(self, fake_image) -> None:
70+
with Session(engine) as session:
71+
session.add(Book(title="Pointless Meetings", cover=fake_image))
72+
session.commit()
73+
book = session.execute(
74+
select(Book).where(Book.title == "Pointless Meetings")
75+
).scalar_one()
76+
old_file_id = book.cover.path
77+
old_thumbnail_file_id = book.cover.thumbnail["path"]
78+
book.cover = fake_image
79+
session.commit()
80+
with pytest.raises(ObjectDoesNotExistError):
81+
assert StorageManager.get_file(old_file_id)
82+
with pytest.raises(ObjectDoesNotExistError):
83+
assert StorageManager.get_file(old_thumbnail_file_id)
84+
85+
def test_delete_image_with_thumbnail(self, fake_image) -> None:
86+
with Session(engine) as session:
87+
session.add(Book(title="Pointless Meetings", cover=fake_image))
88+
session.commit()
89+
book = session.execute(
90+
select(Book).where(Book.title == "Pointless Meetings")
91+
).scalar_one()
92+
old_file_id = book.cover.path
93+
old_thumbnail_file_id = book.cover.thumbnail["path"]
94+
session.delete(book)
95+
session.commit()
96+
with pytest.raises(ObjectDoesNotExistError):
97+
assert StorageManager.get_file(old_file_id)
98+
with pytest.raises(ObjectDoesNotExistError):
99+
assert StorageManager.get_file(old_thumbnail_file_id)
100+
69101
def teardown_method(self, method):
70102
for obj in StorageManager.get().list_objects():
71103
obj.delete()

0 commit comments

Comments
 (0)