diff --git a/filemanager/__init__.py b/filemanager/__init__.py index c4d4db4..0a60bda 100644 --- a/filemanager/__init__.py +++ b/filemanager/__init__.py @@ -1,36 +1,17 @@ -from django.shortcuts import render -from django.http import HttpResponse -from django import forms -from PIL import Image -from . import settings import mimetypes import os -import shutil import re import tarfile -import zipfile -import magic - -path_end = r'(?P[\w\d_ -/.]*)$' -ActionChoices = ( - ('upload', 'upload'), - ('rename', 'rename'), - ('delete', 'delete'), - ('add', 'add'), - ('move', 'move'), - ('copy', 'copy'), - ('unzip', 'unzip'), -) +from django.http import HttpResponse +from django.shortcuts import render +from . import settings +from .actions import Action +from .forms import FileManagerForm +from .utils import get_size, get_media -class FileManagerForm(forms.Form): - ufile = forms.FileField(required=False) - action = forms.ChoiceField(choices=ActionChoices) - path = forms.CharField(max_length=200, required=False) - name = forms.CharField(max_length=32, required=False) - current_path = forms.CharField(max_length=200, required=False) - file_or_dir = forms.CharField(max_length=4) +path_end = r'(?P[\w\d_ -/.]*)$' class FileManager(object): @@ -61,34 +42,17 @@ def __init__( self.extensions = extensions self.public_url_base = public_url_base - def rename_if_exists(self, folder, file): - if folder[-1] != os.sep: - folder = folder + os.sep - if os.path.exists(folder + file): - if file.find('.') == -1: - # no extension - for i in range(1000): - if not os.path.exists(folder + file + '.' + str(i)): - break - return file + '.' + str(i) - else: - extension = file[file.rfind('.'):] - name = file[:file.rfind('.')] - for i in range(1000): - full_path = folder + name + '.' + str(i) + extension - if not os.path.exists(full_path): - break - return name + '.' + str(i) + extension - else: - return file - - def get_size(self, start_path): - total_size = 0 - for dirpath, dirnames, filenames in os.walk(start_path): - for f in filenames: - fp = os.path.join(dirpath, f) - total_size += os.path.getsize(fp) - return total_size + self.config = { + 'FILEMANAGER_STATIC_ROOT': settings.FILEMANAGER_STATIC_ROOT, + 'FILEMANAGER_CKEDITOR_JS': settings.FILEMANAGER_CKEDITOR_JS, + 'FILEMANAGER_CHECK_SPACE': settings.FILEMANAGER_CHECK_SPACE, + 'FILEMANAGER_SHOW_SPACE': settings.FILEMANAGER_SHOW_SPACE, + 'maxfolders': self.maxfolders, + 'maxspace': self.maxspace, + 'maxfilesize': self.maxfilesize, + 'extensions': self.extensions, + 'basepath': self.basepath, + } def next_id(self): self.idee = self.idee + 1 @@ -101,10 +65,11 @@ def handle_form(self, form, files): file_or_dir = form.cleaned_data['file_or_dir'] self.current_path = form.cleaned_data['current_path'] messages = [] + is_directory = file_or_dir == 'dir' invalid_folder_name = ( name - and file_or_dir == 'dir' + and is_directory and not re.match(r'[\w\d_ -]+', name).group(0) == name ) if invalid_folder_name: @@ -113,7 +78,7 @@ def handle_form(self, form, files): invalid_file_name = ( name - and file_or_dir == 'file' + and not is_directory and ( re.search(r'\.\.', name) or not re.match(r'[\w\d_ -.]+', name).group(0) == name @@ -127,240 +92,10 @@ def handle_form(self, form, files): if invalid_path: messages.append("Invalid path : " + path) return messages - if action == 'upload': - for f in files.getlist('ufile'): - file_name_invalid = ( - re.search(r'\.\.', f.name) - or not re.match(r'[\w\d_ -/.]+', f.name).group(0) == f.name - ) - if file_name_invalid: - messages.append("File name is not valid : " + f.name) - elif f.size > self.maxfilesize*1024: - messages.append( - "File size exceeded " - + str(self.maxfilesize) - + " KB : " - + f.name - ) - elif ( - settings.FILEMANAGER_CHECK_SPACE and - ( - (self.get_size(self.basepath) + f.size) - > self.maxspace*1024 - ) - ): - messages.append( - "Total Space size exceeded " - + str(self.maxspace) - + " KB : " - + f.name - ) - elif ( - self.extensions - and len(f.name.split('.')) > 1 - and f.name.split('.')[-1] not in self.extensions - ): - messages.append( - "File extension not allowed (." - + f.name.split('.')[-1] - + ") : " - + f.name - ) - elif ( - self.extensions - and len(f.name.split('.')) == 1 - and f.name.split('.')[-1] - not in self.extensions - ): - messages.append( - "No file extension in uploaded file : " - + f.name - ) - else: - filename = f.name.replace(' ', '_') # replace spaces to prevent fs error - filepath = ( - self.basepath - + path - + self.rename_if_exists(self.basepath + path, filename) - ) - with open(filepath, 'wb') as dest: - for chunk in f.chunks(): - dest.write(chunk) - f.close() - mimetype = magic.from_file(filepath, mime=True) - guessed_exts = mimetypes.guess_all_extensions(mimetype) - guessed_exts = [ext[1:] for ext in guessed_exts] - common = [ext for ext in guessed_exts if ext in self.extensions] - if not common: - os.remove(filepath) - messages.append( - "File type not allowed : " - + f.name - ) - if len(messages) == 0: - messages.append('All files uploaded successfully') - elif action == 'add': - os.chdir(self.basepath) - no_of_folders = len(list(os.walk('.'))) - if (no_of_folders + 1) <= self.maxfolders: - try: - os.chdir(self.basepath + path) - os.mkdir(name) - messages.append('Folder created successfully : ' + name) - except OSError: - messages.append('Folder couldn\'t be created : ' + name) - except Exception as e: - messages.append('Unexpected error : ' + e) - else: - messages.append( - 'Folder couldn\' be created because maximum number of ' - + 'folders exceeded : ' - + str(self.maxfolders) - ) - elif action == 'rename' and file_or_dir == 'dir': - oldname = path.split('/')[-2] - path = '/'.join(path.split('/')[:-2]) - try: - os.chdir(self.basepath + path) - os.rename(oldname, name) - messages.append( - 'Folder renamed successfully from ' - + oldname - + ' to ' - + name - ) - except OSError: - messages.append('Folder couldn\'t renamed to ' + name) - except Exception as e: - messages.append('Unexpected error : ' + e) - elif action == 'delete' and file_or_dir == 'dir': - if path == '/': - messages.append('root folder can\'t be deleted') - else: - name = path.split('/')[-2] - path = '/'.join(path.split('/')[:-2]) - try: - os.chdir(self.basepath + path) - shutil.rmtree(name) - messages.append('Folder deleted successfully : ' + name) - except OSError: - messages.append('Folder couldn\'t deleted : ' + name) - except Exception as e: - messages.append('Unexpected error : ' + e) - elif action == 'rename' and file_or_dir == 'file': - oldname = path.split('/')[-1] - old_ext = ( - oldname.split('.')[1] - if len(oldname.split('.')) > 1 - else None - ) - new_ext = name.split('.')[1] if len(name.split('.')) > 1 else None - if old_ext == new_ext: - path = '/'.join(path.split('/')[:-1]) - try: - os.chdir(self.basepath + path) - os.rename(oldname, name) - messages.append( - 'File renamed successfully from ' - + oldname - + ' to ' - + name - ) - except OSError: - messages.append('File couldn\'t be renamed to ' + name) - except Exception as e: - messages.append('Unexpected error : ' + e) - else: - if old_ext: - messages.append( - 'File extension should be same : .' - + old_ext - ) - else: - messages.append( - 'New file extension didn\'t match with old file' - + ' extension' - ) - elif action == 'delete' and file_or_dir == 'file': - if path == '/': - messages.append('root folder can\'t be deleted') - else: - name = path.split('/')[-1] - path = '/'.join(path.split('/')[:-1]) - try: - os.chdir(self.basepath + path) - os.remove(name) - messages.append('File deleted successfully : ' + name) - except OSError: - messages.append('File couldn\'t deleted : ' + name) - except Exception as e: - messages.append('Unexpected error : ' + e) - elif action == 'move' or action == 'copy': - # from path to current_path - if self.current_path.find(path) == 0: - messages.append('Cannot move/copy to a child folder') - else: - path = os.path.normpath(path) # strip trailing slash if any - filename = ( - self.basepath - + self.current_path - + os.path.basename(path) - ) - if os.path.exists(filename): - messages.append( - 'ERROR: A file/folder with this name already exists in' - + ' the destination folder.' - ) - else: - if action == 'move': - method = shutil.move - else: - if file_or_dir == 'dir': - method = shutil.copytree - else: - method = shutil.copy - try: - method(self.basepath + path, filename) - except OSError: - messages.append( - 'File/folder couldn\'t be moved/copied.' - ) - except Exception as e: - messages.append('Unexpected error : ' + e) - elif action == 'unzip': - if file_or_dir == 'dir': - messages.append('Cannot unzip a directory') - else: - try: - path = os.path.normpath(path) # strip trailing slash if any - filename = ( - self.basepath - + self.current_path - + os.path.basename(path) - ) - zip_ref = zipfile.ZipFile(filename, 'r') - # zip_ref.extractall(self.basepath + self.current_path) - directory = self.basepath + self.current_path - for file in zip_ref.namelist(): - if file.endswith(tuple(self.extensions)): - zip_ref.extract(file, directory) - mimetype = magic.from_file(directory + file, mime=True) - print(directory + file) - guessed_exts = mimetypes.guess_all_extensions(mimetype) - guessed_exts = [ext[1:] for ext in guessed_exts] - common = [ext for ext in guessed_exts if ext in self.extensions] - if not common: - os.remove(directory+file) - messages.append( - "File in the zip is not allowed : " - + file - ) - zip_ref.close() - except Exception as e: - print(e) - messages.append('ERROR : Could not unzip the file.') - if len(messages) == 0: - messages.append('Extraction completed successfully.') + + # actual handling of action + messages = Action.handle_action(action, path, name, is_directory, files, + self.current_path, self.config) return messages @@ -404,49 +139,8 @@ def directory_structure(self): return dir_structure def media(self, path): - ext = path.split('.')[-1] - try: - mimetypes.init() - mimetype = mimetypes.guess_type(path)[0] - img = Image.open(self.basepath + '/' + path) - width, height = img.size - mx = max([width, height]) - w, h = width, height - if mx > 60: - w = width*60/mx - h = height*60/mx - img = img.resize((w, h), Image.ANTIALIAS) - response = HttpResponse(content_type=mimetype or "image/" + ext) - response['Cache-Control'] = 'max-age=3600' - img.save( - response, - mimetype.split('/')[1] if mimetype else ext.upper() - ) - return response - except Exception: - imagepath = ( - settings.FILEMANAGER_STATIC_ROOT - + 'images/icons/' - + ext - + '.png' - ) - if not os.path.exists(imagepath): - imagepath = ( - settings.FILEMANAGER_STATIC_ROOT - + 'images/icons/default.png' - ) - img = Image.open(imagepath) - width, height = img.size - mx = max([width, height]) - w, h = width, height - if mx > 60: - w = int(width*60/mx) - h = int(height*60/mx) - img = img.resize((w, h), Image.ANTIALIAS) - response = HttpResponse(content_type="image/png") - response['Cache-Control'] = 'max-age:3600' - img.save(response, 'png') - return response + response = get_media(self.basepath, path) + return response def download(self, path, file_or_dir): if not re.match(r'[\w\d_ -/]*', path).group(0) == path: @@ -490,7 +184,7 @@ def render(self, request, path): if form.is_valid(): messages = self.handle_form(form, request.FILES) if settings.FILEMANAGER_CHECK_SPACE: - space_consumed = self.get_size(self.basepath) + space_consumed = get_size(self.basepath) else: space_consumed = 0 return render( diff --git a/filemanager/actions.py b/filemanager/actions.py new file mode 100644 index 0000000..60d539f --- /dev/null +++ b/filemanager/actions.py @@ -0,0 +1,290 @@ +import mimetypes +import os +import re +import shutil +import zipfile + +import magic + +from .utils import get_size, rename_if_exists + + +class Action: + def __init__(self, action, path, name, is_directory, files, current_path, config): + self.action = action + self.path = path + self.name = name + self.is_directory = is_directory + self.files = files + self.current_path = current_path + self.config = config + self.messages = [] + + def process_action(self): + pass + + def get_file_path(self): + return '/'.join(self.path.split('/')[:-1]) + + def get_directory_path(self): + return '/'.join(self.path.split('/')[:-2]) + + def get_file_name(self): + return self.path.split('/')[-1] + + def get_directory_name(self): + return self.path.split('/')[-2] + + def get_name_and_path(self): + if self.is_directory: + return self.get_directory_name(), self.get_directory_path() + return self.get_file_name(), self.get_file_path() + + def get_message_with_prefix(self, message): + prefix = 'Folder ' if self.is_directory else 'File ' + message = "{}{}".format(prefix, message) + return message + + def add_message(self, message, name='', dir_prefix=False): + if dir_prefix: + message = self.get_message_with_prefix(message) + + if name: + self.messages.append("{} {}".format(message, name)) + else: + self.messages.append(message) + + @staticmethod + def handle_action(action, path, name, is_directory, files, current_path, config): + action_classes = { + 'upload': UploadAction, + 'add': AddAction, + 'delete': DeleteAction, + 'rename': RenameAction, + 'move': MoveAction, + 'copy': CopyAction, + 'unzip': UnzipAction, + } + + action_class = action_classes.get(action) + action_class_instance = action_class(action, path, name, is_directory, files, current_path, config) + messages = action_class_instance.process_action() + + return messages + + +class UploadAction(Action): + def process_action(self): + for f in self.files.getlist('ufile'): + + self.validate_file(f) + if len(self.messages) == 0: + filename = f.name.replace(' ', '_') # replace spaces to prevent fs error + filepath = ( + self.config['basepath'] + + self.path + + rename_if_exists(self.config['basepath'] + self.path, filename) + ) + with open(filepath, 'wb') as dest: + for chunk in f.chunks(): + dest.write(chunk) + f.close() + mimetype = magic.from_file(filepath, mime=True) + guessed_exts = mimetypes.guess_all_extensions(mimetype) + guessed_exts = [ext[1:] for ext in guessed_exts] + common = [ext for ext in guessed_exts if ext in self.config['extensions']] + if not common: + os.remove(filepath) + self.messages.append( + "File type not allowed : " + + f.name + ) + if len(self.messages) == 0: + self.messages.append('All files uploaded successfully') + + return self.messages + + def validate_file(self, f): + file_name_invalid = ( + re.search(r'\.\.', f.name) + or not re.match(r'[\w\d_ -/.]+', f.name).group(0) == f.name + ) + if file_name_invalid: + self.messages.append("File name is not valid : " + f.name) + elif f.size > self.config['maxfilesize'] * 1024: + self.messages.append("File size exceeded " + str(self.config['maxfilesize']) + " KB : " + f.name) + elif ( + self.config['FILEMANAGER_CHECK_SPACE'] and + ((get_size(self.config['basepath']) + f.size) > self.config['maxspace'] * 1024) + ): + self.messages.append("Total Space size exceeded " + str(self.config['maxspace']) + + " KB : " + f.name) + elif ( + self.config['extensions'] + and len(f.name.split('.')) > 1 + and f.name.split('.')[-1] not in self.config['extensions'] + ): + self.messages.append("File extension not allowed (." + f.name.split('.')[-1] + ") : " + f.name) + elif ( + self.config['extensions'] + and len(f.name.split('.')) == 1 + and f.name.split('.')[-1] + not in self.config['extensions'] + ): + self.messages.append("No file extension in uploaded file : " + f.name) + + +class RenameAction(Action): + def process_action(self): + oldname, path = self.get_name_and_path() + + if not self.is_directory: + old_ext = self.get_extension_from_filename(oldname) + new_ext = self.get_extension_from_filename(self.name) + + if old_ext != new_ext: + if old_ext: + self.messages.append('File extension should be same : .' + old_ext) + else: + self.messages.append('New file extension didn\'t match with old file' + ' extension') + + return self.messages + + try: + os.chdir(self.config['basepath'] + path) + os.rename(oldname, self.name) + self.add_message('renamed successfully from ' + oldname + ' to ', self.name, dir_prefix=True) + except OSError: + self.add_message('couldn\'t be renamed to ', self.name, dir_prefix=True) + except Exception as e: + self.add_message('Unexpected error : ', str(e)) + + return self.messages + + def get_extension_from_filename(self, filename): + return filename.split('.')[1] if len(filename.split('.')) > 1 else None + + +class DeleteAction(Action): + def process_action(self): + if self.path == '/': + self.add_message('root folder can\'t be deleted') + return self.messages + + name, path = self.get_name_and_path() + os.chdir(self.config['basepath'] + path) + + try: + if self.is_directory: + shutil.rmtree(name) + else: + os.remove(name) + self.add_message('deleted successfully : ', name, dir_prefix=True) + except OSError: + self.add_message('couldn\'t be deleted : ', name, dir_prefix=True) + except Exception as e: + self.add_message('Unexpected error : ', str(e)) + + return self.messages + + +class AddAction(Action): + def process_action(self): + os.chdir(self.config['basepath']) + no_of_folders = len(list(os.walk('.'))) + if (no_of_folders + 1) <= self.config['maxfolders']: + try: + os.chdir(self.config['basepath'] + self.path) + os.mkdir(self.name) + self.messages.append('Folder created successfully : ' + self.name) + except OSError: + self.messages.append('Folder couldn\'t be created : ' + self.name) + except Exception as e: + self.messages.append('Unexpected error : ' + e) + else: + self.messages.append( + 'Folder couldn\' be created because maximum number of ' + + 'folders exceeded : ' + + str(self.config['maxfolders']) + ) + + return self.messages + + +class MoveAction(Action): + def process_action(self): + # from path to current_path + if self.current_path.find(self.path) == 0: + self.messages.append('Cannot move/copy to a child folder') + else: + self.path = os.path.normpath(self.path) # strip trailing slash if any + filename = ( + self.config['basepath'] + + self.current_path + + os.path.basename(self.path) + ) + if os.path.exists(filename): + self.messages.append( + 'ERROR: A file/folder with this name already exists in' + + ' the destination folder.' + ) + else: + if self.action == 'move': + method = shutil.move + else: + if self.is_directory: + method = shutil.copytree + else: + method = shutil.copy + try: + method(self.config['basepath'] + self.path, filename) + except OSError: + self.messages.append( + 'File/folder couldn\'t be moved/copied.' + ) + except Exception as e: + self.messages.append('Unexpected error : ' + e) + + return self.messages + + +class CopyAction(MoveAction): + pass + + +class UnzipAction(Action): + def process_action(self): + if self.is_directory: + self.messages.append('Cannot unzip a directory') + else: + try: + self.path = os.path.normpath(self.path) # strip trailing slash if any + filename = ( + self.config['basepath'] + + self.current_path + + os.path.basename(self.path) + ) + zip_ref = zipfile.ZipFile(filename, 'r') + directory = self.config['basepath'] + self.current_path + for file in zip_ref.namelist(): + if file.endswith(tuple(self.config['extensions'])): + zip_ref.extract(file, directory) + mimetype = magic.from_file(directory + file, mime=True) + print(directory + file) + guessed_exts = mimetypes.guess_all_extensions(mimetype) + guessed_exts = [ext[1:] for ext in guessed_exts] + common = [ext for ext in guessed_exts if ext in self.config['extensions']] + if not common: + os.remove(directory + file) + self.messages.append( + "File in the zip is not allowed : " + + file + ) + zip_ref.close() + except Exception as e: + print(e) + self.messages.append('ERROR : Could not unzip the file.') + if len(self.messages) == 0: + self.messages.append('Extraction completed successfully.') + + return self.messages diff --git a/filemanager/forms.py b/filemanager/forms.py new file mode 100644 index 0000000..c8d7e04 --- /dev/null +++ b/filemanager/forms.py @@ -0,0 +1,20 @@ +from django import forms + +ActionChoices = ( + ('upload', 'upload'), + ('rename', 'rename'), + ('delete', 'delete'), + ('add', 'add'), + ('move', 'move'), + ('copy', 'copy'), + ('unzip', 'unzip'), +) + + +class FileManagerForm(forms.Form): + ufile = forms.FileField(required=False) + action = forms.ChoiceField(choices=ActionChoices) + path = forms.CharField(max_length=200, required=False) + name = forms.CharField(max_length=32, required=False) + current_path = forms.CharField(max_length=200, required=False) + file_or_dir = forms.CharField(max_length=4) diff --git a/filemanager/utils.py b/filemanager/utils.py new file mode 100644 index 0000000..8680d0a --- /dev/null +++ b/filemanager/utils.py @@ -0,0 +1,86 @@ +import mimetypes +import os + +from django.http import HttpResponse +from PIL import Image + +from . import settings + + +def get_size(start_path): + total_size = 0 + for dirpath, dirnames, filenames in os.walk(start_path): + for f in filenames: + fp = os.path.join(dirpath, f) + total_size += os.path.getsize(fp) + return total_size + + +def rename_if_exists(folder, file): + if folder[-1] != os.sep: + folder = folder + os.sep + if os.path.exists(folder + file): + if file.find('.') == -1: + # no extension + for i in range(1000): + if not os.path.exists(folder + file + '.' + str(i)): + break + return file + '.' + str(i) + else: + extension = file[file.rfind('.'):] + name = file[:file.rfind('.')] + for i in range(1000): + full_path = folder + name + '.' + str(i) + extension + if not os.path.exists(full_path): + break + return name + '.' + str(i) + extension + else: + return file + + +def get_media(basepath, path): + ext = path.split('.')[-1] + try: + mimetypes.init() + mimetype = mimetypes.guess_type(path)[0] + img = Image.open(basepath + '/' + path) + width, height = img.size + mx = max([width, height]) + w, h = width, height + if mx > 60: + w = width * 60 / mx + h = height * 60 / mx + img = img.resize((w, h), Image.ANTIALIAS) + response = HttpResponse(content_type=mimetype or "image/" + ext) + response['Cache-Control'] = 'max-age=3600' + img.save( + response, + mimetype.split('/')[1] if mimetype else ext.upper() + ) + return response + + except Exception: + imagepath = ( + settings.FILEMANAGER_STATIC_ROOT + + 'images/icons/' + + ext + + '.png' + ) + if not os.path.exists(imagepath): + imagepath = ( + settings.FILEMANAGER_STATIC_ROOT + + 'images/icons/default.png' + ) + img = Image.open(imagepath) + width, height = img.size + mx = max([width, height]) + w, h = width, height + if mx > 60: + w = int(width * 60 / mx) + h = int(height * 60 / mx) + img = img.resize((w, h), Image.ANTIALIAS) + response = HttpResponse(content_type="image/png") + response['Cache-Control'] = 'max-age:3600' + img.save(response, 'png') + + return response