# coding: utf-8 from filebrowser.actions import flip_horizontal, flip_vertical, rotate_90_clockwise, rotate_90_counterclockwise, rotate_180 import os import re import json from time import gmtime, strftime, localtime, time from django import forms from django import VERSION as DJANGO_VERSION from django.contrib import messages from django.contrib.admin.sites import site as admin_site from django.utils.module_loading import import_string from django.contrib.admin.views.decorators import staff_member_required from django.core.files.storage import DefaultStorage, default_storage, FileSystemStorage from django.core.paginator import Paginator, InvalidPage, EmptyPage try: from django.urls import reverse, get_urlconf, get_resolver except ImportError: from django.core.urlresolvers import reverse, get_urlconf, get_resolver from django.http import HttpResponseRedirect, HttpResponseBadRequest from django.shortcuts import render, HttpResponse from django.template import RequestContext as Context from django.template.response import TemplateResponse from django.utils.encoding import smart_str from django.utils.translation import gettext as _ from django.views.decorators.cache import never_cache from django.views.decorators.clickjacking import xframe_options_sameorigin from django.views.decorators.csrf import csrf_exempt from filebrowser import signals from filebrowser.base import FileListing, FileObject from filebrowser.decorators import path_exists, file_exists from filebrowser.storage import FileSystemStorageMixin from filebrowser.templatetags.fb_tags import query_helper from filebrowser.utils import convert_filename from filebrowser.settings import (DIRECTORY, EXTENSIONS, SELECT_FORMATS, ADMIN_VERSIONS, ADMIN_THUMBNAIL, MAX_UPLOAD_SIZE, NORMALIZE_FILENAME, CONVERT_FILENAME, SEARCH_TRAVERSE, EXCLUDE, VERSIONS, VERSIONS_BASEDIR, EXTENSION_LIST, DEFAULT_SORTING_BY, DEFAULT_SORTING_ORDER, LIST_PER_PAGE, OVERWRITE_EXISTING, DEFAULT_PERMISSIONS, UPLOAD_TEMPDIR, ADMIN_CUSTOM ) # This use admin_custom and not admin.sites.site of Django. admin_site = import_string(ADMIN_CUSTOM) if ADMIN_CUSTOM else admin_site # Add some required methods to FileSystemStorage if FileSystemStorageMixin not in FileSystemStorage.__bases__: FileSystemStorage.__bases__ += (FileSystemStorageMixin,) # This cache contains all *instantiated* FileBrowser sites _sites_cache = {} def get_site_dict(app_name='filebrowser'): """ Return a dict with all *deployed* FileBrowser sites that have a given app_name. """ if app_name not in _sites_cache: return {} # Get names of all deployed filebrowser sites with a give app_name deployed = get_resolver(get_urlconf()).app_dict[app_name] # Get the deployed subset from the cache return dict((k, v) for k, v in _sites_cache[app_name].items() if k in deployed) def register_site(app_name, site_name, site): """ Add a site into the site dict. """ if app_name not in _sites_cache: _sites_cache[app_name] = {} _sites_cache[app_name][site_name] = site def get_default_site(app_name='filebrowser'): """ Returns the default site. This function uses Django's url resolution method to obtain the name of the default site. """ # Get the name of the default site: resolver = get_resolver(get_urlconf()) name = 'filebrowser' # Django's default name resolution method (see django.core.urlresolvers.reverse()) app_list = resolver.app_dict[app_name] if name not in app_list: name = app_list[0] return get_site_dict()[name] def get_breadcrumbs(query, path): """ Get breadcrumbs. """ breadcrumbs = [] dir_query = "" if path: for item in path.split(os.sep): dir_query = os.path.join(dir_query, item) breadcrumbs.append([item, dir_query]) return breadcrumbs def get_filterdate(filter_date, date_time): """ Get filterdate. """ returnvalue = '' date_year = strftime("%Y", gmtime(date_time)) date_month = strftime("%m", gmtime(date_time)) date_day = strftime("%d", gmtime(date_time)) if filter_date == 'today' and int(date_year) == int(localtime()[0]) and int(date_month) == int(localtime()[1]) and int(date_day) == int(localtime()[2]): returnvalue = 'true' elif filter_date == 'thismonth' and date_time >= time() - 2592000: returnvalue = 'true' elif filter_date == 'thisyear' and int(date_year) == int(localtime()[0]): returnvalue = 'true' elif filter_date == 'past7days' and date_time >= time() - 604800: returnvalue = 'true' elif filter_date == '': returnvalue = 'true' return returnvalue def get_settings_var(directory=DIRECTORY): """ Get settings variables used for FileBrowser listing. """ settings_var = {} # Main # Extensions/Formats (for FileBrowseField) settings_var['EXTENSIONS'] = EXTENSIONS settings_var['SELECT_FORMATS'] = SELECT_FORMATS # Versions settings_var['ADMIN_VERSIONS'] = ADMIN_VERSIONS settings_var['ADMIN_THUMBNAIL'] = ADMIN_THUMBNAIL # FileBrowser Options settings_var['MAX_UPLOAD_SIZE'] = MAX_UPLOAD_SIZE # Normalize Filenames settings_var['NORMALIZE_FILENAME'] = NORMALIZE_FILENAME # Convert Filenames settings_var['CONVERT_FILENAME'] = CONVERT_FILENAME # Traverse directories when searching settings_var['SEARCH_TRAVERSE'] = SEARCH_TRAVERSE return settings_var def handle_file_upload(path, file, site): """ Handle File Upload. """ uploadedfile = None try: file_path = os.path.join(path, file.name) uploadedfile = site.storage.save(file_path, file) except Exception as inst: raise inst return uploadedfile def filebrowser_view(view): "Only let staff browse the files" return staff_member_required(never_cache(xframe_options_sameorigin(view))) class FileBrowserSite(object): """ A filebrowser.site defines admin views for browsing your servers media files. """ filelisting_class = FileListing def __init__(self, name=None, app_name='filebrowser', storage=default_storage): self.name = name self.app_name = app_name self.storage = storage self._actions = {} self._global_actions = self._actions.copy() # Register this site in the global site cache register_site(self.app_name, self.name, self) # Per-site settings: self.directory = DIRECTORY def _directory_get(self): "Set directory" return self._directory def _directory_set(self, val): "Get directory" self._directory = val directory = property(_directory_get, _directory_set) def get_urls(self): "URLs for a filebrowser.site" from django.urls import re_path # filebrowser urls (views) urlpatterns = [ re_path(r'^browse/$', path_exists(self, filebrowser_view(self.browse)), name="fb_browse"), re_path(r'^createdir/', path_exists(self, filebrowser_view(self.createdir)), name="fb_createdir"), re_path(r'^upload/', path_exists(self, filebrowser_view(self.upload)), name="fb_upload"), re_path(r'^delete_confirm/$', file_exists(self, path_exists(self, filebrowser_view(self.delete_confirm))), name="fb_delete_confirm"), re_path(r'^delete/$', file_exists(self, path_exists(self, filebrowser_view(self.delete))), name="fb_delete"), re_path(r'^detail/$', file_exists(self, path_exists(self, filebrowser_view(self.detail))), name="fb_detail"), re_path(r'^version/$', file_exists(self, path_exists(self, filebrowser_view(self.version))), name="fb_version"), re_path(r'^upload_file/$', staff_member_required( csrf_exempt(self._upload_file)), name="fb_do_upload"), ] return urlpatterns def add_action(self, action, name=None): """ Register an action to be available globally. """ name = name or action.__name__ # Check/create short description if not hasattr(action, 'short_description'): action.short_description = action.__name__.replace( "_", " ").capitalize() # Check/create applies-to filter if not hasattr(action, 'applies_to'): action.applies_to = lambda x: True self._actions[name] = action self._global_actions[name] = action def disable_action(self, name): """ Disable a globally-registered action. Raises KeyError for invalid names. """ del self._actions[name] def get_action(self, name): """ Explicitally get a registered global action wheather it's enabled or not. Raises KeyError for invalid names. """ return self._global_actions[name] def applicable_actions(self, fileobject): """ Return a list of tuples (name, action) of actions applicable to a given fileobject. """ res = [] for name, action in self.actions: if action.applies_to(fileobject): res.append((name, action)) return res @property def actions(self): """ Get all the enabled actions as a list of (name, func). The list is sorted alphabetically by actions names """ res = list(self._actions.items()) res.sort(key=lambda name_func: name_func[0]) return res @property def urls(self): "filebrowser.site URLs" return self.get_urls(), self.app_name, self.name def browse(self, request): "Browse Files/Directories." filter_re = [] for exp in EXCLUDE: filter_re.append(re.compile(exp)) # do not filter if VERSIONS_BASEDIR is being used if not VERSIONS_BASEDIR: for k, v in VERSIONS.items(): exp = (r'_%s(%s)$') % (k, '|'.join(EXTENSION_LIST)) filter_re.append(re.compile(exp, re.IGNORECASE)) def filter_browse(item): "Defining a browse filter" filtered = item.filename.startswith('.') for re_prefix in filter_re: if re_prefix.search(item.filename): filtered = True if filtered: return False return True query = request.GET.copy() path = u'%s' % os.path.join(self.directory, query.get('dir', '')) filelisting = self.filelisting_class( path, filter_func=filter_browse, sorting_by=query.get('o', DEFAULT_SORTING_BY), sorting_order=query.get('ot', DEFAULT_SORTING_ORDER), site=self) files = [] if SEARCH_TRAVERSE and query.get("q"): listing = filelisting.files_walk_filtered() else: listing = filelisting.files_listing_filtered() # If we do a search, precompile the search pattern now do_search = query.get("q") if do_search: re_q = re.compile(query.get("q").lower(), re.M) filter_type = query.get('filter_type') filter_date = query.get('filter_date') for fileobject in listing: # date/type filter append = False if (not filter_type or fileobject.filetype == filter_type) and (not filter_date or get_filterdate(filter_date, fileobject.date or 0)): append = True # search if do_search and not re_q.search(fileobject.filename.lower()): append = False # append if append: files.append(fileobject) filelisting.results_total = len(listing) filelisting.results_current = len(files) p = Paginator(files, LIST_PER_PAGE) page_nr = request.GET.get('p', '1') try: page = p.page(page_nr) except (EmptyPage, InvalidPage): page = p.page(p.num_pages) request.current_app = self.name return TemplateResponse(request, 'filebrowser/index.html', dict( admin_site.each_context(request), **{ 'p': p, 'page': page, 'filelisting': filelisting, 'query': query, 'title': _(u'FileBrowser'), 'is_popup': "pop" in request.GET, # ChangeList uses "pop" 'settings_var': get_settings_var(directory=self.directory), 'breadcrumbs': get_breadcrumbs(query, query.get('dir', '')), 'breadcrumbs_title': "", 'filebrowser_site': self, } )) def createdir(self, request): "Create Directory" from filebrowser.forms import CreateDirForm query = request.GET path = u'%s' % os.path.join(self.directory, query.get('dir', '')) if request.method == 'POST': form = CreateDirForm(path, request.POST, filebrowser_site=self) if form.is_valid(): server_path = os.path.join(path, form.cleaned_data['name']) try: signals.filebrowser_pre_createdir.send( sender=request, path=server_path, name=form.cleaned_data['name'], site=self) self.storage.makedirs(server_path) signals.filebrowser_post_createdir.send( sender=request, path=server_path, name=form.cleaned_data['name'], site=self) messages.add_message(request, messages.SUCCESS, _( 'The Folder %s was successfully created.') % form.cleaned_data['name']) redirect_url = reverse("filebrowser:fb_browse", current_app=self.name) + query_helper( query, "ot=desc,o=date", "ot,o,filter_type,filter_date,q,p") return HttpResponseRedirect(redirect_url) except OSError as e: errno = e.args[0] if errno == 13: form.errors['name'] = forms.util.ErrorList( [_('Permission denied.')]) else: form.errors['name'] = forms.util.ErrorList( [_('Error creating folder.')]) else: form = CreateDirForm(path, filebrowser_site=self) request.current_app = self.name return TemplateResponse(request, 'filebrowser/createdir.html', dict( admin_site.each_context(request), **{ 'form': form, 'query': query, 'title': _(u'New Folder'), 'is_popup': "pop" in request.GET, 'settings_var': get_settings_var(directory=self.directory), 'breadcrumbs': get_breadcrumbs(query, query.get('dir', '')), 'breadcrumbs_title': _(u'New Folder'), 'filebrowser_site': self } )) def upload(self, request): "Multipe File Upload." query = request.GET request.current_app = self.name return TemplateResponse(request, 'filebrowser/upload.html', dict( admin_site.each_context(request), **{ 'query': query, 'title': _(u'Select files to upload'), 'is_popup': "pop" in request.GET, 'settings_var': get_settings_var(directory=self.directory), 'breadcrumbs': get_breadcrumbs(query, query.get('dir', '')), 'breadcrumbs_title': _(u'Upload'), 'filebrowser_site': self } )) def delete_confirm(self, request): "Delete existing File/Directory." query = request.GET path = u'%s' % os.path.join(self.directory, query.get('dir', '')) fileobject = FileObject(os.path.join( path, query.get('filename', '')), site=self) if fileobject.filetype == "Folder": filelisting = self.filelisting_class( os.path.join(path, fileobject.filename), sorting_by=query.get('o', 'filename'), sorting_order=query.get('ot', DEFAULT_SORTING_ORDER), site=self) filelisting = filelisting.files_walk_total() if len(filelisting) > 100: additional_files = len(filelisting) - 100 filelisting = filelisting[:100] else: additional_files = None else: filelisting = None additional_files = None request.current_app = self.name return TemplateResponse(request, 'filebrowser/delete_confirm.html', dict( admin_site.each_context(request), **{ 'fileobject': fileobject, 'filelisting': filelisting, 'additional_files': additional_files, 'query': query, 'title': _(u'Confirm delete'), 'is_popup': "pop" in request.GET, 'settings_var': get_settings_var(directory=self.directory), 'breadcrumbs': get_breadcrumbs(query, query.get('dir', '')), 'breadcrumbs_title': _(u'Confirm delete'), 'filebrowser_site': self } )) def delete(self, request): "Delete existing File/Directory." query = request.GET path = u'%s' % os.path.join(self.directory, query.get('dir', '')) fileobject = FileObject(os.path.join( path, query.get('filename', '')), site=self) if request.GET: try: signals.filebrowser_pre_delete.send( sender=request, path=fileobject.path, name=fileobject.filename, site=self) fileobject.delete_versions() fileobject.delete() signals.filebrowser_post_delete.send( sender=request, path=fileobject.path, name=fileobject.filename, site=self) messages.add_message(request, messages.SUCCESS, _( 'Successfully deleted %s') % fileobject.filename) except OSError: # TODO: define error-message pass redirect_url = reverse("filebrowser:fb_browse", current_app=self.name) + \ query_helper(query, "", "filename,filetype") return HttpResponseRedirect(redirect_url) def detail(self, request): """ Show detail page for a file. Rename existing File/Directory (deletes existing Image Versions/Thumbnails). """ from filebrowser.forms import ChangeForm query = request.GET path = u'%s' % os.path.join(self.directory, query.get('dir', '')) fileobject = FileObject(os.path.join( path, query.get('filename', '')), site=self) if request.method == 'POST': form = ChangeForm(request.POST, path=path, fileobject=fileobject, filebrowser_site=self) if form.is_valid(): new_name = form.cleaned_data['name'] action_name = form.cleaned_data['custom_action'] try: action_response = None if action_name: action = self.get_action(action_name) # Pre-action signal signals.filebrowser_actions_pre_apply.send( sender=request, action_name=action_name, fileobject=[fileobject], site=self) # Call the action to action action_response = action( request=request, fileobjects=[fileobject]) # Post-action signal signals.filebrowser_actions_post_apply.send(sender=request, action_name=action_name, fileobject=[ fileobject], result=action_response, site=self) if new_name != fileobject.filename: signals.filebrowser_pre_rename.send( sender=request, path=fileobject.path, name=fileobject.filename, new_name=new_name, site=self) fileobject.delete_versions() self.storage.move(fileobject.path, os.path.join( fileobject.head, new_name)) signals.filebrowser_post_rename.send( sender=request, path=fileobject.path, name=fileobject.filename, new_name=new_name, site=self) messages.add_message(request, messages.SUCCESS, _( 'Renaming was successful.')) if isinstance(action_response, HttpResponse): return action_response if "_continue" in request.POST: redirect_url = reverse("filebrowser:fb_detail", current_app=self.name) + query_helper( query, "filename=" + new_name, "filename") else: redirect_url = reverse( "filebrowser:fb_browse", current_app=self.name) + query_helper(query, "", "filename") return HttpResponseRedirect(redirect_url) except OSError: form.errors['name'] = forms.util.ErrorList([_('Error.')]) else: form = ChangeForm(initial={"name": fileobject.filename}, path=path, fileobject=fileobject, filebrowser_site=self) request.current_app = self.name return TemplateResponse(request, 'filebrowser/detail.html', dict( admin_site.each_context(request), **{ 'form': form, 'fileobject': fileobject, 'query': query, 'title': u'%s' % fileobject.filename, 'is_popup': "pop" in request.GET, 'settings_var': get_settings_var(directory=self.directory), 'breadcrumbs': get_breadcrumbs(query, query.get('dir', '')), 'breadcrumbs_title': u'%s' % fileobject.filename, 'filebrowser_site': self } )) def version(self, request): """ Version detail. This just exists in order to select a version with a filebrowser–popup. """ query = request.GET path = u'%s' % os.path.join(self.directory, query.get('dir', '')) fileobject = FileObject(os.path.join( path, query.get('filename', '')), site=self) request.current_app = self.name return TemplateResponse(request, 'filebrowser/version.html', dict( admin_site.each_context(request), **{ 'fileobject': fileobject, 'query': query, 'settings_var': get_settings_var(directory=self.directory), 'filebrowser_site': self } )) def _upload_file(self, request): """ Upload file to the server. If temporary is true, we upload to UPLOAD_TEMPDIR, otherwise we upload to site.directory """ if request.method == "POST": folder = request.GET.get('folder', '') temporary = request.GET.get('temporary', '') temp_filename = None if len(request.FILES) == 0: return HttpResponseBadRequest('Invalid request! No files included.') if len(request.FILES) > 1: return HttpResponseBadRequest('Invalid request! Multiple files included.') filedata = list(request.FILES.values())[0] fb_uploadurl_re = re.compile( r'^.*(%s)' % reverse("filebrowser:fb_upload", current_app=self.name)) folder = fb_uploadurl_re.sub('', folder) # temporary upload folder should be outside self.directory if folder == UPLOAD_TEMPDIR and temporary == "true": path = folder else: path = os.path.join(self.directory, folder) # we convert the filename before uploading in order # to check for existing files/folders file_name = convert_filename(filedata.name) filedata.name = file_name file_path = os.path.join(path, file_name) file_already_exists = self.storage.exists(file_path) # construct temporary filename by adding the upload folder, because # otherwise we don't have any clue if the file has temporary been # uploaded or not if folder == UPLOAD_TEMPDIR and temporary == "true": temp_filename = os.path.join(folder, file_name) # Check for name collision with a directory if file_already_exists and self.storage.isdir(file_path): ret_json = {'success': False, 'filename': file_name} return HttpResponse(json.dumps(ret_json)) signals.filebrowser_pre_upload.send( sender=request, path=folder, file=filedata, site=self) uploadedfile = handle_file_upload(path, filedata, site=self) if file_already_exists and OVERWRITE_EXISTING: old_file = smart_str(file_path) new_file = smart_str(uploadedfile) self.storage.move(new_file, old_file, allow_overwrite=True) full_path = FileObject( smart_str(old_file), site=self).path_full else: file_name = smart_str(uploadedfile) filedata.name = os.path.relpath(file_name, path) full_path = FileObject( smart_str(file_name), site=self).path_full # set permissions if DEFAULT_PERMISSIONS is not None: os.chmod(full_path, DEFAULT_PERMISSIONS) f = FileObject(smart_str(file_name), site=self) signals.filebrowser_post_upload.send( sender=request, path=folder, file=f, site=self) # let Ajax Upload know whether we saved it or not ret_json = { 'success': True, 'filename': f.filename, 'temp_filename': temp_filename, 'url': f.url, } return HttpResponse(json.dumps(ret_json), content_type="application/json") storage = DefaultStorage() # Default FileBrowser site site = FileBrowserSite(name='filebrowser', storage=storage) # Default actions site.add_action(flip_horizontal) site.add_action(flip_vertical) site.add_action(rotate_90_clockwise) site.add_action(rotate_90_counterclockwise) site.add_action(rotate_180)