├── .gitignore ├── LICENSE ├── README.md ├── django_orphaned ├── __init__.py ├── app_settings.py └── management │ ├── __init__.py │ └── commands │ ├── __init__.py │ └── deleteorphaned.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | #Mr Developer 27 | .mr.developer.cfg 28 | 29 | # project files 30 | *.pydevproject 31 | *.project 32 | *.sublime-project 33 | *.sublime-workspace -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Leonardo Di Lella 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 12 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 13 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 14 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 15 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 16 | SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # about 2 | delete all orphaned files 3 | 4 | # setup 5 | install via easy_install or pip 6 | 7 | easy_install django-orphaned 8 | 9 | with pip 10 | 11 | pip install django-orphaned 12 | 13 | add it to installed apps in django settings.py 14 | 15 | INSTALLED_APPS = ( 16 | 'django_orphaned', 17 | ... 18 | ) 19 | 20 | now add this to your settings.py ('app' is your project name where models.py is located): 21 | 22 | ORPHANED_APPS_MEDIABASE_DIRS = { 23 | 'app': { 24 | 'root': MEDIABASE_ROOT, # MEDIABASE_ROOT => default location(s) of your uploaded items e.g. /var/www/mediabase 25 | 'skip': ( # optional iterable of subfolders to preserve, e.g. sorl.thumbnail cache 26 | path.join(MEDIABASE_ROOT, 'cache'), 27 | path.join(MEDIABASE_ROOT, 'foobar'), 28 | ), 29 | 'exclude': ('.gitignore',) # optional iterable of files to preserve 30 | } 31 | } 32 | 33 | **NOTE**: from version 0.4.2 you can define ''root'' as string or iterable (list, array) 34 | 35 | the least to do is to run this command to show all orphaned files 36 | 37 | python manage.py deleteorphaned --info 38 | 39 | and to finally delete all orphaned files 40 | 41 | python manage.py deleteorphaned 42 | 43 | # license 44 | MIT-License, see [LICENSE](/ledil/django-orphaned/blob/master/LICENSE) file. 45 | -------------------------------------------------------------------------------- /django_orphaned/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledil/django-orphaned/9954fa22667d81822de91376145d6f68d8d981e0/django_orphaned/__init__.py -------------------------------------------------------------------------------- /django_orphaned/app_settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | ORPHANED_APPS_MEDIABASE_DIRS = getattr(settings, 'ORPHANED_APPS_MEDIABASE_DIRS',{}) 4 | -------------------------------------------------------------------------------- /django_orphaned/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledil/django-orphaned/9954fa22667d81822de91376145d6f68d8d981e0/django_orphaned/management/__init__.py -------------------------------------------------------------------------------- /django_orphaned/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledil/django-orphaned/9954fa22667d81822de91376145d6f68d8d981e0/django_orphaned/management/commands/__init__.py -------------------------------------------------------------------------------- /django_orphaned/management/commands/deleteorphaned.py: -------------------------------------------------------------------------------- 1 | from django.contrib.contenttypes.models import ContentType 2 | from django.core.management.base import BaseCommand 3 | from django_orphaned.app_settings import ORPHANED_APPS_MEDIABASE_DIRS 4 | from optparse import make_option 5 | from django.core.exceptions import ImproperlyConfigured 6 | import os 7 | import shutil 8 | 9 | 10 | class Command(BaseCommand): 11 | help = "Delete all orphaned files" 12 | base_options = ( 13 | make_option('--info', action='store_true', dest='info', default=False, 14 | help='If provided, the files will not be deleted.'), 15 | ) 16 | option_list = BaseCommand.option_list + base_options 17 | 18 | def handle(self, **options): 19 | self.only_info = options.get('info') 20 | self.verbose = True 21 | 22 | for app in ORPHANED_APPS_MEDIABASE_DIRS.keys(): 23 | if self.verbose: 24 | print('', "=" * 80, "\n\tProcessing App", app, '\n', "=" * 80) 25 | if (ORPHANED_APPS_MEDIABASE_DIRS[app].get('root')): 26 | file_paths_in_db = [] 27 | files_in_app_root = [] 28 | possible_empty_dirs = [] 29 | empty_dirs = [] 30 | total_freed_bytes = 0 31 | total_freed = '0' 32 | delete_files_list = [] 33 | 34 | for model in ContentType.objects.filter(app_label=app): 35 | mc = model.model_class() 36 | fields = [] 37 | for field in mc._meta.fields: 38 | if (field.get_internal_type() == 'FileField') or (field.get_internal_type() == 'ImageField'): 39 | fields.append(field.name) 40 | 41 | # we have found a model with FileFields or ImageFields 42 | if (len(fields) > 0): 43 | if self.verbose: 44 | print("\nFound ", len(fields), " fields" if len( 45 | fields) > 1 else "field", " - ", fields, " in model: ", mc.__name__) 46 | 47 | for field in fields: 48 | files = mc.objects.all().values_list(field, flat=True) 49 | file_paths_in_db.extend([os.path.join(ORPHANED_APPS_MEDIABASE_DIRS[ 50 | app]['root'], file) for file in files]) 51 | print("\n--------", files, "\n") 52 | if self.verbose: 53 | print(len(files), 54 | " file are stored database.\n", "-" * 80,) 55 | 56 | print("\n", file_paths_in_db) 57 | # traverse root folder and store all files and empty 58 | # directories 59 | for root, dirs, files in os.walk(ORPHANED_APPS_MEDIABASE_DIRS[app]['root']): 60 | if (len(files) > 0): 61 | for basename in files: 62 | print(basename) 63 | files_in_app_root.append( 64 | os.path.join(root, basename)) 65 | else: 66 | if (root != ORPHANED_APPS_MEDIABASE_DIRS[app]['root']) and ((root + '/') != ORPHANED_APPS_MEDIABASE_DIRS[app]['root']): 67 | possible_empty_dirs.append(root) 68 | 69 | if files_in_app_root: 70 | if self.verbose: 71 | print("--> Total files found in app root directory: ", 72 | len(files_in_app_root)) 73 | else: 74 | if self.verbose: 75 | print("+++ No file found in app root directory +++") 76 | 77 | if possible_empty_dirs: 78 | if self.verbose: 79 | print("--> empty directories found: ", 80 | len(possible_empty_dirs), " - ", possible_empty_dirs) 81 | else: 82 | if self.verbose: 83 | print("+++ No empty directories found +++") 84 | 85 | # Ignore empty dirs with subdirs + files 86 | for ed in possible_empty_dirs: 87 | dont_delete = False 88 | for files in files_in_app_root: 89 | try: 90 | if (files.index(ed) == 0): 91 | dont_delete = True 92 | except ValueError: 93 | pass 94 | if (not dont_delete): 95 | empty_dirs.append(ed) 96 | 97 | # select deleted files 98 | # delete_files_list = files_in_app_root - file_paths_in_db 99 | files_in_app_root_set = set(files_in_app_root) 100 | file_paths_in_db_set = set(file_paths_in_db) 101 | print("\n files in app root: ", files_in_app_root_set) 102 | print("\n files in databse: ", file_paths_in_db_set) 103 | 104 | delete_files_list = list( 105 | files_in_app_root_set.difference(file_paths_in_db_set)) 106 | 107 | delete_files_list.sort() 108 | empty_dirs.sort() 109 | 110 | if self.verbose: 111 | print("\nDELETING FILES\n", delete_files_list) 112 | 113 | return 114 | # TODO: to be fried 115 | for df in delete_files_list: 116 | total_freed_bytes += os.path.getsize(df) 117 | total_freed = "%0.1f MB" % ( 118 | total_freed_bytes / (1024 * 1024.0)) 119 | 120 | # only show 121 | if (self.only_info): 122 | if (len(delete_files_list) > 0): 123 | if self.verbose: 124 | print("\r\nFollowing files will be deleted:\r\n") 125 | for file in delete_files_list: 126 | if self.verbose: 127 | print(" ", file) 128 | 129 | if (len(empty_dirs) > 0): 130 | if self.verbose: 131 | print("\r\nFollowing empty dirs will be removed:\r\n") 132 | for file in empty_dirs: 133 | if self.verbose: 134 | print(" ", file) 135 | 136 | if (len(delete_files_list) > 0): 137 | if self.verbose: 138 | print("\r\nTotally %s files will be deleted, and totally %s will be freed\r\n" % ( 139 | len(delete_files_list), total_freed)) 140 | else: 141 | if self.verbose: 142 | print("No files to delete!") 143 | 144 | # DELETE NOW! 145 | else: 146 | for file in delete_files_list: 147 | # os.remove(file) 148 | if self.verbose: 149 | print("removing %s" % file) 150 | for dirs in empty_dirs: 151 | # shutil.rmtree(dirs,ignore_errors=True) 152 | if self.verbose: 153 | print("removing tree %s" % dirs) 154 | 155 | else: 156 | raise ImproperlyConfigured( 157 | "MEDIA ROOT settings is not defined in ORPHANED_APPS_MEDIABASE_DIRS settings.") 158 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup 3 | 4 | setup( 5 | name='django-orphaned', 6 | description='delete all orphaned files from your models', 7 | version='0.4.3', 8 | author='Leonardo Di Lella', 9 | author_email='leonardo.dilella@mobileapart.com', 10 | license='MIT', 11 | url='https://github.com/ledil/django-orphaned', 12 | packages=[ 13 | 'django_orphaned', 14 | 'django_orphaned.management', 15 | 'django_orphaned.management.commands'], 16 | classifiers=[ 17 | 'Development Status :: 3 - Alpha', 18 | 'Environment :: Web Environment', 19 | 'Framework :: Django', 20 | 'Intended Audience :: Developers', 21 | 'License :: OSI Approved :: MIT License', 22 | 'Natural Language :: English', 23 | 'Operating System :: POSIX :: Linux', 24 | 'Programming Language :: Python', 25 | 'Topic :: System :: Installation/Setup' 26 | ] 27 | ) 28 | --------------------------------------------------------------------------------