Skip to content

Commit 2ace498

Browse files
Baraff24fsbraun
andauthored
Fix: Include Private Storage in Orphan File Scanning for filer_check Command (#1518)
* feat: add filename length safety check with random suffix * fix: correct base filename length for validation test * fix: update base filename length in validation test * test: enhance filename length validation and edge case handling Integration of the suggestion from the community and sourcery-ai * fix: update _ensure_safe_length to use settings for max length and random suffix * test: read max filename length and random suffix from settings * fix: set default values for max filename length and random suffix in _ensure_safe_length * Update filer/utils/files.py * Apply suggestions from code review * refactor: improve command help text and streamline orphan file handling Issues: Management command filer_check does not find orphaned files in the private storage #1514 * refactor: enhance output messages and improve storage handling in filer_check * fix: add missing newline at end of test_filer_check.py * Update test.yml --------- Co-authored-by: Fabian Braun <[email protected]>
1 parent 5e46265 commit 2ace498

File tree

2 files changed

+252
-112
lines changed

2 files changed

+252
-112
lines changed
+94-76
Original file line numberDiff line numberDiff line change
@@ -1,163 +1,181 @@
11
import os
22

3-
from django.core.files.storage import DefaultStorage
43
from django.core.management.base import BaseCommand
54
from django.utils.module_loading import import_string
65

7-
from PIL import UnidentifiedImageError
8-
96
from filer import settings as filer_settings
7+
from filer.models.filemodels import File
108
from filer.utils.loader import load_model
119

10+
from PIL import UnidentifiedImageError
11+
1212

1313
class Command(BaseCommand):
14-
help = "Look for orphaned files in media folders."
15-
storage = DefaultStorage()
16-
prefix = filer_settings.FILER_STORAGES['public']['main']['UPLOAD_TO_PREFIX']
14+
help = "Check for orphaned files, missing file references, and set image dimensions."
1715

1816
def add_arguments(self, parser):
1917
parser.add_argument(
2018
'--orphans',
2119
action='store_true',
2220
dest='orphans',
2321
default=False,
24-
help="Walk through the media folders and look for orphaned files.",
22+
help="Scan media folders for orphaned files.",
2523
)
2624
parser.add_argument(
2725
'--delete-orphans',
2826
action='store_true',
2927
dest='delete_orphans',
3028
default=False,
31-
help="Delete orphaned files from their media folders.",
29+
help="Delete orphaned files from storage.",
3230
)
3331
parser.add_argument(
3432
'--missing',
3533
action='store_true',
3634
dest='missing',
3735
default=False,
38-
help="Verify media folders and report about missing files.",
36+
help="Check file references and report missing files.",
3937
)
4038
parser.add_argument(
4139
'--delete-missing',
4240
action='store_true',
4341
dest='delete_missing',
4442
default=False,
45-
help="Delete references in database if files are missing in media folder.",
43+
help="Delete database entries if files are missing in the media folder.",
4644
)
4745
parser.add_argument(
4846
'--image-dimensions',
4947
action='store_true',
5048
dest='image_dimensions',
5149
default=False,
52-
help="Look for images without dimensions set, set them accordingly.",
50+
help="Set image dimensions if they are not set.",
5351
)
5452
parser.add_argument(
5553
'--noinput',
5654
'--no-input',
5755
action='store_false',
5856
dest='interactive',
5957
default=True,
60-
help="Do NOT prompt the user for input of any kind."
58+
help="Do not prompt the user for any interactive input.",
6159
)
6260

6361
def handle(self, *args, **options):
6462
if options['missing']:
6563
self.verify_references(options)
6664
if options['delete_missing']:
6765
if options['interactive']:
68-
msg = "\nThis will delete entries from your database. Are you sure you want to do this?\n\n" \
69-
"Type 'yes' to continue, or 'no' to cancel: "
70-
if input(msg) != 'yes':
71-
self.stdout.write("Aborted: Delete missing file entries from database.")
66+
if input(
67+
"\nThis will delete missing file references from the database.\n"
68+
"Type 'yes' to continue, or 'no' to cancel: "
69+
) != 'yes':
70+
self.stdout.write("Aborted: Missing file references were not deleted.\n")
71+
self.stdout.flush()
7272
return
7373
self.verify_references(options)
7474

75-
if options['orphans']:
76-
self.verify_storages(options)
77-
if options['delete_orphans']:
78-
if options['interactive']:
79-
msg = "\nThis will delete orphaned files from your storage. Are you sure you want to do this?\n\n" \
80-
"Type 'yes' to continue, or 'no' to cancel: "
81-
if input(msg) != 'yes':
82-
self.stdout.write("Aborted: Delete orphaned files from storage.")
75+
if options['orphans'] or options['delete_orphans']:
76+
if options['delete_orphans'] and options['interactive']:
77+
if input(
78+
"\nThis will delete orphaned files from storage.\n"
79+
"Type 'yes' to continue, or 'no' to cancel: "
80+
) != 'yes':
81+
self.stdout.write("Aborted: Orphaned files were not deleted.\n")
82+
self.stdout.flush()
8383
return
8484
self.verify_storages(options)
85+
8586
if options['image_dimensions']:
8687
self.image_dimensions(options)
8788

8889
def verify_references(self, options):
89-
from filer.models.filemodels import File
90-
90+
"""
91+
Checks that every file reference in the database exists in storage.
92+
If a file is missing, either report it or delete the reference based on the provided options.
93+
"""
9194
for file in File.objects.all():
9295
if not file.file.storage.exists(file.file.name):
9396
if options['delete_missing']:
9497
file.delete()
95-
msg = "Delete missing file reference '{}/{}' from database."
98+
verbose_msg = f"Deleted missing file reference '{file.folder}/{file}' from the database."
9699
else:
97-
msg = "Referenced file '{}/{}' is missing in media folder."
98-
if options['verbosity'] > 2:
99-
self.stdout.write(msg.format(str(file.folder), str(file)))
100-
elif options['verbosity']:
101-
self.stdout.write(os.path.join(str(file.folder), str(file)))
100+
verbose_msg = f"File reference '{file.folder}/{file}' is missing in storage."
101+
if options.get('verbosity', 1) > 2:
102+
self.stdout.write(verbose_msg + "\n")
103+
self.stdout.flush()
104+
elif options.get('verbosity'):
105+
self.stdout.write(os.path.join(str(file.folder), str(file)) + "\n")
106+
self.stdout.flush()
102107

103108
def verify_storages(self, options):
104-
from filer.models.filemodels import File
105-
106-
def walk(prefix):
109+
"""
110+
Scans all storages defined in FILER_STORAGES (e.g., public and private)
111+
for orphaned files, then reports or deletes them based on the options.
112+
"""
113+
114+
def walk(storage, prefix, label_prefix):
115+
# If the directory does not exist, there is nothing to scan
116+
if not storage.exists(prefix):
117+
return
107118
child_dirs, files = storage.listdir(prefix)
108119
for filename in files:
109-
relfilename = os.path.join(prefix, filename)
110-
if not File.objects.filter(file=relfilename).exists():
120+
actual_path = os.path.join(prefix, filename)
121+
relfilename = os.path.join(label_prefix, filename)
122+
if not File.objects.filter(file=actual_path).exists():
111123
if options['delete_orphans']:
112-
storage.delete(relfilename)
113-
msg = "Deleted orphaned file '{}'"
124+
storage.delete(actual_path)
125+
message = f"Deleted orphaned file '{relfilename}'"
114126
else:
115-
msg = "Found orphaned file '{}'"
116-
if options['verbosity'] > 2:
117-
self.stdout.write(msg.format(relfilename))
118-
elif options['verbosity']:
119-
self.stdout.write(relfilename)
120-
127+
message = f"Found orphaned file '{relfilename}'"
128+
if options.get('verbosity', 1) > 2:
129+
self.stdout.write(message + "\n")
130+
self.stdout.flush()
131+
elif options.get('verbosity'):
132+
self.stdout.write(relfilename + "\n")
133+
self.stdout.flush()
121134
for child in child_dirs:
122-
walk(os.path.join(prefix, child))
123-
124-
filer_public = filer_settings.FILER_STORAGES['public']['main']
125-
storage = import_string(filer_public['ENGINE'])()
126-
walk(filer_public['UPLOAD_TO_PREFIX'])
135+
walk(storage, os.path.join(prefix, child), os.path.join(label_prefix, child))
136+
137+
# Loop through each storage configuration (e.g., public, private, etc.)
138+
for storage_name, storage_config in filer_settings.FILER_STORAGES.items():
139+
storage_settings = storage_config.get('main')
140+
if not storage_settings:
141+
continue
142+
storage = import_string(storage_settings['ENGINE'])()
143+
if storage_settings.get('OPTIONS', {}).get('location'):
144+
storage.location = storage_settings['OPTIONS']['location']
145+
# Set label_prefix: for public and private storages, use their names.
146+
label_prefix = storage_name if storage_name in ['public', 'private'] else storage_settings.get('UPLOAD_TO_PREFIX', '')
147+
walk(storage, storage_settings.get('UPLOAD_TO_PREFIX', ''), label_prefix)
127148

128149
def image_dimensions(self, options):
150+
"""
151+
For images without set dimensions (_width == 0 or None), try to read their dimensions
152+
and save them, handling SVG files and possible image errors.
153+
"""
129154
from django.db.models import Q
130-
131155
import easy_thumbnails
132156
from easy_thumbnails.VIL import Image as VILImage
133-
134157
from filer.utils.compatibility import PILImage
135158

136-
no_dimensions = load_model(filer_settings.FILER_IMAGE_MODEL).objects.filter(
137-
Q(_width=0) | Q(_width__isnull=True)
138-
)
139-
self.stdout.write(f"trying to set dimensions on {no_dimensions.count()} files")
140-
for image in no_dimensions:
141-
if image.file_ptr:
142-
file_holder = image.file_ptr
143-
else:
144-
file_holder = image
159+
ImageModel = load_model(filer_settings.FILER_IMAGE_MODEL)
160+
images_without_dimensions = ImageModel.objects.filter(Q(_width=0) | Q(_width__isnull=True))
161+
self.stdout.write(f"Setting dimensions for {images_without_dimensions.count()} images" + "\n")
162+
self.stdout.flush()
163+
for image in images_without_dimensions:
164+
file_holder = image.file_ptr if getattr(image, 'file_ptr', None) else image
145165
try:
146166
imgfile = file_holder.file
147167
imgfile.seek(0)
148-
except (FileNotFoundError):
149-
pass
168+
except FileNotFoundError:
169+
continue
170+
if image.file.name.lower().endswith('.svg'):
171+
# For SVG files, use VILImage (invalid SVGs do not throw errors)
172+
with VILImage.load(imgfile) as vil_image:
173+
image._width, image._height = vil_image.size
150174
else:
151-
if image.file.name.lower().endswith('.svg'):
152-
with VILImage.load(imgfile) as vil_image:
153-
# invalid svg doesnt throw errors
154-
image._width, image._height = vil_image.size
155-
else:
156-
try:
157-
with PILImage.open(imgfile) as pil_image:
158-
image._width, image._height = pil_image.size
159-
image._transparent = easy_thumbnails.utils.is_transparent(pil_image)
160-
except UnidentifiedImageError:
161-
continue
162-
image.save()
163-
return
175+
try:
176+
with PILImage.open(imgfile) as pil_image:
177+
image._width, image._height = pil_image.size
178+
image._transparent = easy_thumbnails.utils.is_transparent(pil_image)
179+
except UnidentifiedImageError:
180+
continue
181+
image.save()

0 commit comments

Comments
 (0)