11import os
22import shutil
3+ import tempfile
34from io import BytesIO , StringIO
45
56from django .core .files .uploadedfile import SimpleUploadedFile
67from django .core .management import call_command
78from django .test import TestCase
89from django .utils .module_loading import import_string
10+ from django .core .files .base import ContentFile
911
1012from filer import settings as filer_settings
1113from filer .models .filemodels import File
@@ -27,13 +29,18 @@ class FilerCheckTestCase(TestCase):
2729 </svg>"""
2830
2931 def setUp (self ):
30- # Clean up the public folder to avoid interference between tests.
31- public_settings = filer_settings .FILER_STORAGES ['public' ]['main' ]
32- storage = import_string (public_settings ['ENGINE' ])()
33- upload_prefix = public_settings ['UPLOAD_TO_PREFIX' ]
34- if storage .exists (upload_prefix ):
35- shutil .rmtree (storage .path (upload_prefix ))
32+ # Clear all configured storages to ensure a clean state for each test.
33+ # This prevents interference from files left in any storage.
34+ for storage_alias , storage_configs in filer_settings .FILER_STORAGES .items ():
35+ config = storage_configs .get ('main' )
36+ if not config :
37+ continue
38+ storage = import_string (config ['ENGINE' ])()
39+ upload_prefix = config .get ('UPLOAD_TO_PREFIX' , '' )
40+ if storage .exists (upload_prefix ):
41+ shutil .rmtree (storage .path (upload_prefix ))
3642
43+ # Create a sample file for testing in the public storage.
3744 original_filename = 'testimage.jpg'
3845 file_obj = SimpleUploadedFile (
3946 name = original_filename ,
@@ -55,8 +62,10 @@ def test_delete_missing(self):
5562 call_command ('filer_check' , stdout = out , missing = True )
5663 self .assertEqual ('' , out .getvalue ())
5764
65+ # Remove the file to simulate a missing file.
5866 os .remove (self .filer_file .file .path )
5967 call_command ('filer_check' , stdout = out , missing = True )
68+ # When verbosity is low, a simple relative file path is output.
6069 self .assertEqual ("None/testimage.jpg\n " , out .getvalue ())
6170 self .assertIsInstance (File .objects .get (id = file_pk ), File )
6271
@@ -65,26 +74,36 @@ def test_delete_missing(self):
6574 File .objects .get (id = file_pk )
6675
6776 def test_delete_orphans_public (self ):
77+ # First check - should be empty initially
6878 out = StringIO ()
69- self .assertTrue (os .path .exists (self .filer_file .file .path ))
70- call_command ('filer_check' , stdout = out , orphans = True )
71- # The public folder should be free of orphaned files.
79+ call_command ('filer_check' , stdout = out , orphans = True , verbosity = 1 )
7280 self .assertEqual ('' , out .getvalue ())
7381
74- # Add an orphan file to the public storage.
82+ # Add an orphan file using the storage API directly
7583 public_settings = filer_settings .FILER_STORAGES ['public' ]['main' ]
7684 storage = import_string (public_settings ['ENGINE' ])()
77- public_path = storage .path (public_settings ['UPLOAD_TO_PREFIX' ])
78- orphan_file = os .path .join (public_path , 'hello.txt' )
79- os .makedirs (public_path , exist_ok = True )
80- with open (orphan_file , 'w' ) as fh :
81- fh .write ("I don't belong here!" )
82- call_command ('filer_check' , stdout = out , orphans = True )
85+
86+ # Configure storage location if specified in settings
87+ if public_settings .get ('OPTIONS' , {}).get ('location' ):
88+ storage .location = public_settings ['OPTIONS' ]['location' ]
89+
90+ # Get upload prefix and create file path
91+ prefix = public_settings .get ('UPLOAD_TO_PREFIX' , '' )
92+ file_path = 'hello.txt'
93+ rel_path = os .path .join (prefix , file_path ) if prefix else file_path
94+
95+ # Save file through storage API
96+ storage .save (rel_path , ContentFile (b"I don't belong here!" ))
97+ self .assertTrue (storage .exists (rel_path ))
98+
99+ # Check if orphan is detected
100+ out = StringIO ()
101+ call_command ('filer_check' , stdout = out , orphans = True , verbosity = 1 )
83102 self .assertEqual ("public/hello.txt\n " , out .getvalue ())
84- self .assertTrue (os .path .exists (orphan_file ))
85103
104+ # Delete orphans
86105 call_command ('filer_check' , delete_orphans = True , interactive = False , verbosity = 0 )
87- self .assertFalse (os . path . exists (orphan_file ))
106+ self .assertFalse (storage . exists (rel_path ))
88107
89108 def test_delete_orphans_private (self ):
90109 # Skip test if private storage is not configured.
@@ -94,15 +113,16 @@ def test_delete_orphans_private(self):
94113 out = StringIO ()
95114 private_settings = filer_settings .FILER_STORAGES ['private' ]['main' ]
96115 storage = import_string (private_settings ['ENGINE' ])()
116+ # Set storage location if defined in OPTIONS.
97117 if private_settings .get ('OPTIONS' , {}).get ('location' ):
98118 storage .location = private_settings ['OPTIONS' ]['location' ]
99- private_path = storage .path (private_settings [ 'UPLOAD_TO_PREFIX' ] )
119+ private_path = storage .path (private_settings . get ( 'UPLOAD_TO_PREFIX' , '' ) )
100120 os .makedirs (private_path , exist_ok = True )
101121
102122 orphan_file = os .path .join (private_path , 'private_orphan.txt' )
103123 with open (orphan_file , 'w' ) as fh :
104124 fh .write ("I don't belong here!" )
105- # Verify that the command detects the orphan file.
125+ # Run the command and check that it detects the private orphan file.
106126 call_command ('filer_check' , stdout = out , orphans = True )
107127 self .assertIn ("private_orphan.txt" , out .getvalue ())
108128 self .assertTrue (os .path .exists (orphan_file ))
@@ -111,11 +131,84 @@ def test_delete_orphans_private(self):
111131 call_command ('filer_check' , delete_orphans = True , interactive = False , verbosity = 0 )
112132 self .assertFalse (os .path .exists (orphan_file ))
113133
134+ def test_delete_orphans_multiple_storages (self ):
135+ """
136+ Test that the filer_check command correctly handles orphaned files in multiple storages
137+ without permanently modifying the settings. We use monkey-patching to assign temporary
138+ directories to the storage configurations.
139+ """
140+ out = StringIO ()
141+
142+ # --- Monkey-patch public storage location ---
143+ public_config = filer_settings .FILER_STORAGES ['public' ]['main' ]
144+ temp_public_dir = tempfile .mkdtemp ()
145+ if 'OPTIONS' in public_config :
146+ public_config ['OPTIONS' ]['location' ] = temp_public_dir
147+ else :
148+ public_config ['OPTIONS' ] = {'location' : temp_public_dir }
149+ # Determine the upload prefix (if any) and ensure the corresponding directory exists.
150+ public_upload_prefix = public_config .get ('UPLOAD_TO_PREFIX' , '' )
151+ if public_upload_prefix :
152+ public_full_dir = os .path .join (temp_public_dir , public_upload_prefix )
153+ else :
154+ public_full_dir = temp_public_dir
155+ os .makedirs (public_full_dir , exist_ok = True )
156+
157+ # --- Monkey-patch private storage location ---
158+ private_config = filer_settings .FILER_STORAGES .get ('private' , {}).get ('main' )
159+ if private_config :
160+ temp_private_dir = tempfile .mkdtemp ()
161+ if 'OPTIONS' in private_config :
162+ private_config ['OPTIONS' ]['location' ] = temp_private_dir
163+ else :
164+ private_config ['OPTIONS' ] = {'location' : temp_private_dir }
165+ private_upload_prefix = private_config .get ('UPLOAD_TO_PREFIX' , '' )
166+ if private_upload_prefix :
167+ private_full_dir = os .path .join (temp_private_dir , private_upload_prefix )
168+ else :
169+ private_full_dir = temp_private_dir
170+ os .makedirs (private_full_dir , exist_ok = True )
171+ else :
172+ self .skipTest ("Private storage not configured in FILER_STORAGES." )
173+
174+ # --- Initialize storages using the patched locations ---
175+ from django .core .files .storage import FileSystemStorage
176+ storage_public = FileSystemStorage (location = temp_public_dir )
177+ storage_private = FileSystemStorage (location = private_config ['OPTIONS' ]['location' ])
178+
179+ # --- Save dummy orphan files in both storages ---
180+ # For public storage, include the upload prefix in the filename if needed.
181+ if public_upload_prefix :
182+ filename_public = os .path .join (public_upload_prefix , 'orphan_public.txt' )
183+ else :
184+ filename_public = 'orphan_public.txt'
185+ if private_config .get ('UPLOAD_TO_PREFIX' , '' ):
186+ filename_private = os .path .join (private_config ['UPLOAD_TO_PREFIX' ], 'orphan_private.txt' )
187+ else :
188+ filename_private = 'orphan_private.txt'
189+
190+ storage_public .save (filename_public , ContentFile (b"dummy content" ))
191+ storage_private .save (filename_private , ContentFile (b"dummy content" ))
192+
193+ # --- Run the filer_check command ---
194+ call_command ('filer_check' , stdout = out , orphans = True )
195+ output = out .getvalue ()
196+
197+ # Verify that the output contains indicators for both storages.
198+ self .assertIn ('public' , output )
199+ self .assertIn ('private' , output )
200+
201+ # --- Clean up ---
202+ storage_public .delete (filename_public )
203+ storage_private .delete (filename_private )
204+ shutil .rmtree (temp_public_dir )
205+ shutil .rmtree (private_config ['OPTIONS' ]['location' ])
206+
114207 def test_image_dimensions_corrupted_file (self ):
115208 original_filename = 'testimage.jpg'
116209 file_obj = SimpleUploadedFile (
117210 name = original_filename ,
118- content = create_image ().tobytes (), # corrupted file
211+ content = create_image ().tobytes (), # Simulate a corrupted file.
119212 content_type = 'image/jpeg'
120213 )
121214 self .filer_image = Image .objects .create (
@@ -189,3 +282,4 @@ def test_image_dimensions_svg(self):
189282 call_command ('filer_check' , image_dimensions = True )
190283 self .filer_image .refresh_from_db ()
191284 self .assertGreater (self .filer_image ._width , 0 )
285+
0 commit comments