Skip to content

Commit e1e95d7

Browse files
jriefFinalAngel
authored andcommitted
added management command (#912)
* added management command * Flake8 complained, used isort * Use the storage.location as intended * change interface according to @yakky’s annotations * add management command ‘filer_check’ plus tests * Use verbosity flag <3 to only render filenames * fix indention * use ‘input’ from PY2/3 compatibility layer * isort complained for that extra line * fix typos * remove translations from managment commands * aborting a managment command does not raise an error * fix typo * fix flake8 complaint * fix isort complaints * fix all tests regarding filer_check * fix flake8 complaint
1 parent c74aa95 commit e1e95d7

File tree

4 files changed

+223
-0
lines changed

4 files changed

+223
-0
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ CHANGELOG
99
* Removed support for Django <= 1.10
1010
* Removed outdated files
1111
* Code alignments with other addons
12+
* Added management command ``filer_check`` to check the integrity of the
13+
database against the file system, and vice versa.
1214
* Add jQuery as AdminFileWidget Media dependency
1315

1416

docs/management_commands.rst

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,33 @@ command.
1212
To generate them, use::
1313

1414
./manage.py generate_thumbnails
15+
16+
17+
Filesystem Checks
18+
-----------------
19+
20+
**django-filer** offers a few commands to check the integrity of the database against the files
21+
stored on disk. By invoking::
22+
23+
./manage.py filer_check --missing
24+
25+
all files which are referenced by the database, but missing on disk are reported.
26+
27+
Invoking::
28+
29+
./manage.py filer_check --delete-missing
30+
31+
deletes those file references from the database.
32+
33+
Invoking::
34+
35+
./manage.py filer_check --orphans
36+
37+
lists all files found on disk belonging to the configured storage engine, but which
38+
are not referenced by the database.
39+
40+
Invoking::
41+
42+
./manage.py filer_check --delete-orphans
43+
44+
deletes those orphaned files from disk.
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
import os
5+
6+
from django.core.files.storage import DefaultStorage
7+
from django.core.management.base import BaseCommand
8+
from django.utils.module_loading import import_string
9+
from django.utils.six.moves import input
10+
11+
from filer import settings as filer_settings
12+
13+
14+
class Command(BaseCommand):
15+
help = "Look for orphaned files in media folders."
16+
storage = DefaultStorage()
17+
prefix = filer_settings.FILER_STORAGES['public']['main']['UPLOAD_TO_PREFIX']
18+
19+
def add_arguments(self, parser):
20+
parser.add_argument(
21+
'--orphans',
22+
action='store_true',
23+
dest='orphans',
24+
default=False,
25+
help="Walk through the media folders and look for orphaned files.",
26+
)
27+
parser.add_argument(
28+
'--delete-orphans',
29+
action='store_true',
30+
dest='delete_orphans',
31+
default=False,
32+
help="Delete orphaned files from their media folders.",
33+
)
34+
parser.add_argument(
35+
'--missing',
36+
action='store_true',
37+
dest='missing',
38+
default=False,
39+
help="Verify media folders and report about missing files.",
40+
)
41+
parser.add_argument(
42+
'--delete-missing',
43+
action='store_true',
44+
dest='delete_missing',
45+
default=False,
46+
help="Delete references in database if files are missing in media folder.",
47+
)
48+
parser.add_argument(
49+
'--noinput',
50+
'--no-input',
51+
action='store_false',
52+
dest='interactive',
53+
default=True,
54+
help="Do NOT prompt the user for input of any kind."
55+
)
56+
57+
def handle(self, *args, **options):
58+
if options['missing']:
59+
self.verify_references(options)
60+
if options['delete_missing']:
61+
if options['interactive']:
62+
msg = "\nThis will delete entries from your database. Are you sure you want to do this?\n\n" \
63+
"Type 'yes' to continue, or 'no' to cancel: "
64+
if input(msg) != 'yes':
65+
self.stdout.write("Aborted: Delete missing file entries from database.")
66+
return
67+
self.verify_references(options)
68+
69+
if options['orphans']:
70+
self.verify_storages(options)
71+
if options['delete_orphans']:
72+
if options['interactive']:
73+
msg = "\nThis will delete orphaned files from your storage. Are you sure you want to do this?\n\n" \
74+
"Type 'yes' to continue, or 'no' to cancel: "
75+
if input(msg) != 'yes':
76+
self.stdout.write("Aborted: Delete orphaned files from storage.")
77+
return
78+
self.verify_storages(options)
79+
80+
def verify_references(self, options):
81+
from filer.models.filemodels import File
82+
83+
for file in File.objects.all():
84+
if not file.file.storage.exists(file.file.name):
85+
if options['delete_missing']:
86+
file.delete()
87+
msg = "Delete missing file reference '{}/{}' from database."
88+
else:
89+
msg = "Referenced file '{}/{}' is missing in media folder."
90+
if options['verbosity'] > 2:
91+
self.stdout.write(msg.format(str(file.folder), str(file)))
92+
elif options['verbosity']:
93+
self.stdout.write(os.path.join(str(file.folder), str(file)))
94+
95+
def verify_storages(self, options):
96+
from filer.models.filemodels import File
97+
98+
def walk(prefix):
99+
child_dirs, files = storage.listdir(prefix)
100+
for filename in files:
101+
relfilename = os.path.join(prefix, filename)
102+
if not File.objects.filter(file=relfilename).exists():
103+
if options['delete_orphans']:
104+
storage.delete(relfilename)
105+
msg = "Deleted orphaned file '{}'"
106+
else:
107+
msg = "Found orphaned file '{}'"
108+
if options['verbosity'] > 2:
109+
self.stdout.write(msg.format(relfilename))
110+
elif options['verbosity']:
111+
self.stdout.write(relfilename)
112+
113+
for child in child_dirs:
114+
walk(os.path.join(prefix, child))
115+
116+
filer_public = filer_settings.FILER_STORAGES['public']['main']
117+
storage = import_string(filer_public['ENGINE'])()
118+
walk(filer_public['UPLOAD_TO_PREFIX'])

tests/test_filer_check.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import absolute_import
3+
4+
import os
5+
import shutil
6+
7+
from django.core.files.uploadedfile import SimpleUploadedFile
8+
from django.core.management import call_command
9+
from django.test import TestCase
10+
from django.utils.module_loading import import_string
11+
from django.utils.six import StringIO
12+
13+
from filer import settings as filer_settings
14+
from filer.models.filemodels import File
15+
16+
from tests.helpers import create_image
17+
18+
19+
class FilerCheckTestCase(TestCase):
20+
def setUp(self):
21+
# ensure that filer_public directory is empty from previous tests
22+
storage = import_string(filer_settings.FILER_STORAGES['public']['main']['ENGINE'])()
23+
upload_to_prefix = filer_settings.FILER_STORAGES['public']['main']['UPLOAD_TO_PREFIX']
24+
if storage.exists(upload_to_prefix):
25+
shutil.rmtree(storage.path(upload_to_prefix))
26+
27+
original_filename = 'testimage.jpg'
28+
file_obj = SimpleUploadedFile(
29+
name=original_filename,
30+
content=create_image().tobytes(),
31+
content_type='image/jpeg')
32+
self.filer_file = File.objects.create(
33+
file=file_obj,
34+
original_filename=original_filename)
35+
36+
def tearDown(self):
37+
self.filer_file.delete()
38+
39+
def test_delete_missing(self):
40+
out = StringIO()
41+
self.assertTrue(os.path.exists(self.filer_file.file.path))
42+
file_pk = self.filer_file.id
43+
call_command('filer_check', stdout=out, missing=True)
44+
self.assertEqual('', out.getvalue())
45+
46+
os.remove(self.filer_file.file.path)
47+
call_command('filer_check', stdout=out, missing=True)
48+
self.assertEqual("None/testimage.jpg\n", out.getvalue())
49+
self.assertIsInstance(File.objects.get(id=file_pk), File)
50+
51+
call_command('filer_check', delete_missing=True, interactive=False, verbosity=0)
52+
with self.assertRaises(File.DoesNotExist):
53+
File.objects.get(id=file_pk)
54+
55+
def test_delete_orphans(self):
56+
out = StringIO()
57+
self.assertTrue(os.path.exists(self.filer_file.file.path))
58+
call_command('filer_check', stdout=out, orphans=True)
59+
# folder must be clean, free of orphans
60+
self.assertEqual('', out.getvalue())
61+
62+
# add an orphan file to our storage
63+
storage = import_string(filer_settings.FILER_STORAGES['public']['main']['ENGINE'])()
64+
filer_public = storage.path(filer_settings.FILER_STORAGES['public']['main']['UPLOAD_TO_PREFIX'])
65+
orphan_file = os.path.join(filer_public, 'hello.txt')
66+
with open(orphan_file, 'w') as fh:
67+
fh.write("I don't belong here!")
68+
call_command('filer_check', stdout=out, orphans=True)
69+
self.assertEqual("filer_public/hello.txt\n", out.getvalue())
70+
self.assertTrue(os.path.exists(orphan_file))
71+
72+
call_command('filer_check', delete_orphans=True, interactive=False, verbosity=0)
73+
self.assertFalse(os.path.exists(orphan_file))

0 commit comments

Comments
 (0)