core/models: refactor SithFile moving and add methods to manage filesystem

Signed-off-by: Skia <skia@libskia.so>
This commit is contained in:
Skia 2018-03-20 22:09:27 +01:00
parent e9e51d34d3
commit 0d3c34c155
4 changed files with 161 additions and 18 deletions

View File

@ -0,0 +1,42 @@
# -*- coding:utf-8 -*
#
# Copyright 2018
# - Skia <skia@libskia.so>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
import os
from django.core.management.base import BaseCommand
from django.core.management import call_command
from core.models import SithFile
class Command(BaseCommand):
help = "Recursively check the file system with respect to the DB"
def add_arguments(self, parser):
parser.add_argument('ids', metavar='ID', type=int, nargs='+', help="The file IDs to process")
def handle(self, *args, **options):
root_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
files = SithFile.objects.filter(id__in=options['ids']).all()
for f in files:
f._check_fs()

View File

@ -0,0 +1,42 @@
# -*- coding:utf-8 -*
#
# Copyright 2018
# - Skia <skia@libskia.so>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
import os
from django.core.management.base import BaseCommand
from django.core.management import call_command
from core.models import SithFile
class Command(BaseCommand):
help = "Recursively repair the file system with respect to the DB"
def add_arguments(self, parser):
parser.add_argument('ids', metavar='ID', type=int, nargs='+', help="The file IDs to process")
def handle(self, *args, **options):
root_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
files = SithFile.objects.filter(id__in=options['ids']).all()
for f in files:
f._repair_fs()

View File

@ -759,25 +759,72 @@ class SithFile(models.Model):
self.save() self.save()
def move_to(self, parent): def move_to(self, parent):
"""Move a file to somewhere else""" """
Move a file to a new parent.
`parent` must be a SithFile with the `is_folder=True` property. Otherwise, this function doesn't change
anything.
This is done only at the DB level, so that it's very fast for the user. Indeed, this function doesn't modify
SithFiles recursively, so it stays efficient even with top-level folders.
"""
if not parent.is_folder: if not parent.is_folder:
return return
import shutil
import os
with transaction.atomic():
if self.is_folder:
old_file_name = self.get_full_path()
else:
old_file_name = self.file.name
self.parent = parent self.parent = parent
self.clean()
self.save() self.save()
def _repair_fs(self):
"""
This function rebuilds recursively the filesystem as it should be
regarding the DB tree.
"""
if self.is_folder: if self.is_folder:
for c in self.children.all(): for c in self.children.all():
c.move_to(self) c._repair_fs()
shutil.rmtree(os.path.join(settings.MEDIA_ROOT, old_file_name)) return
else: else:
self.file.save(name=self.name, content=self.file) import os
os.remove(os.path.join(settings.MEDIA_ROOT, old_file_name)) # First get future parent path and the old file name
# Prepend "." so that we match all relative handling of Django's
# file storage
parent_path = "." + self.parent.get_full_path()
parent_full_path = settings.MEDIA_ROOT + parent_path
print("Parent full path: %s" % parent_full_path)
os.makedirs(parent_full_path, exist_ok=True)
old_path = self.file.name # Should be relative: "./users/skia/bleh.jpg"
new_path = "." + self.get_full_path()
print("Old path: %s " % old_path)
print("New path: %s " % new_path)
# Make this atomic, so that a FS problem rolls back the DB change
with transaction.atomic():
# Set the new filesystem path
self.file.name = new_path
self.save()
print("New file path: %s " % self.file.path)
# Really move at the FS level
if os.path.exists(parent_full_path):
os.rename(settings.MEDIA_ROOT + old_path, settings.MEDIA_ROOT + new_path)
# Empty directories may remain, but that's not really a
# problem, and that can be solved with a simple shell
# command: `find . -type d -empty -delete`
def _check_path_consistence(self):
file_path = str(self.file)
db_path = ".%s" % self.get_full_path()
if file_path != db_path:
print("%s: " % self.id, end='')
print("file path: %s" % file_path, end='')
print(" db path: %s" % db_path)
return False
else:
return True
def _check_fs(self):
if self.is_folder:
for c in self.children.all():
c._check_fs()
return
else:
self._check_path_consistence()
def __getattribute__(self, attr): def __getattribute__(self, attr):
if attr == "is_file": if attr == "is_file":

View File

@ -189,6 +189,18 @@ class FileView(CanViewMixin, DetailView, FormMixin):
form_class = AddFilesForm form_class = AddFilesForm
def handle_clipboard(request, object): def handle_clipboard(request, object):
"""
This method handles the clipboard in the view.
This method can fail, since it does not catch the exceptions coming from
below, allowing proper handling in the calling view.
Use this method like this:
FileView.handle_clipboard(request, self.object)
`request` is usually the self.request object in your view
`object` is the SithFile object you want to put in the clipboard, or
where you want to paste the clipboard
"""
if 'delete' in request.POST.keys(): if 'delete' in request.POST.keys():
for f_id in request.POST.getlist('file_list'): for f_id in request.POST.getlist('file_list'):
sf = SithFile.objects.filter(id=f_id).first() sf = SithFile.objects.filter(id=f_id).first()
@ -200,7 +212,6 @@ class FileView(CanViewMixin, DetailView, FormMixin):
for f_id in request.POST.getlist('file_list'): for f_id in request.POST.getlist('file_list'):
f_id = int(f_id) f_id = int(f_id)
if f_id in [c.id for c in object.children.all()] and f_id not in request.session['clipboard']: if f_id in [c.id for c in object.children.all()] and f_id not in request.session['clipboard']:
print(f_id)
request.session['clipboard'].append(f_id) request.session['clipboard'].append(f_id)
if 'paste' in request.POST.keys(): if 'paste' in request.POST.keys():
for f_id in request.session['clipboard']: for f_id in request.session['clipboard']:
@ -221,6 +232,7 @@ class FileView(CanViewMixin, DetailView, FormMixin):
if 'clipboard' not in request.session.keys(): if 'clipboard' not in request.session.keys():
request.session['clipboard'] = [] request.session['clipboard'] = []
if request.user.can_edit(self.object): if request.user.can_edit(self.object):
# XXX this call can fail!
FileView.handle_clipboard(request, self.object) FileView.handle_clipboard(request, self.object)
self.form = self.get_form() # The form handle only the file upload self.form = self.get_form() # The form handle only the file upload
files = request.FILES.getlist('file_field') files = request.FILES.getlist('file_field')