gridftp: multiple fixes in handling and presenting errors
authorMaciej Tronowski <mtro@man.poznan.pl>
Wed, 22 Apr 2015 14:19:55 +0000 (16:19 +0200)
committerMaciej Tronowski <mtro@man.poznan.pl>
Wed, 22 Apr 2015 14:19:55 +0000 (16:19 +0200)
filex/ftp.py
filex/static/filex/filex.js
filex/templates/filex/source.html
filex/templates/filex/upload.html
filex/templates/filex/upload.js.html
filex/utils.py
filex/views.py
qcg/templates/qcg/download_error.html [new file with mode: 0644]
qcg/views.py

index 2c92f12..d83a55c 100644 (file)
@@ -99,13 +99,14 @@ class FTPOperation:
                 for attr in attrs.split(';'):
                     try:
                         key, value = attr.split('=', 1)
-                        attrs_dict[key] = value
                     except ValueError:
-                        pass
+                        key, value = attr, ''
+
+                    attrs_dict[key] = value
 
                 yield {
                     'name': name,
-                    'type': 'directory' if attrs_dict['Type'] == 'dir' else 'file',
+                    'type': 'directory' if attrs_dict['Type'].endswith('dir') else 'file',
                     'size': int(attrs_dict['Size']),
                     'date': localtime(datetime.strptime(attrs_dict['Modify'], "%Y%m%d%H%M%S").replace(tzinfo=UTC())),
                 }
index eb71ee4..26bec82 100644 (file)
@@ -284,9 +284,20 @@ $(function(){
 
                             $this.modal('hide');
                             $btn.button('reset');
-                        }, 'json').fail(function() {
+                        }, 'json').fail(function(xhr) {
                             console.error(arguments);
-                            $btn.button('error');
+                            var error = (xhr.responseJSON || {}).error || undefined;
+
+                            if (typeof error === 'string') {
+                                $('<div>', {
+                                    'class': 'alert alert-danger',
+                                    html: '<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> ' + error
+                                }).prependTo($this.find('.modal-body'));
+                                $btn.button('reset');
+                            }
+                            else {
+                                $btn.button('error');
+                            }
                         });
                     });
 
index ba88414..551ae51 100644 (file)
@@ -75,7 +75,7 @@
             </div>
             <div class="modal-footer">
                 <button type="button" class="btn btn-default" data-dismiss="modal">Anuluj</button>
-                <button type="submit" class="btn btn-primary" data-loading-text="Zapisywanie..." data-error-text="Błąd">OK</button>
+                <button type="submit" class="btn btn-primary" data-loading-text="Zapisywanie..." data-error-text="Błąd serwera">OK</button>
             </div>
         </div>
     </div>
index 346fc1d..b79d9c0 100644 (file)
@@ -3,43 +3,51 @@
     <p id="path"><span class="text-muted">Lokalizacja:</span> {{ host }}{{ sep }}{{ path }}</p>
 </header>
 
-<form enctype="multipart/form-data" method="post" action="{{ url }}" hidden>
-    {% csrf_token %}
-    <input id="files" type="file" name="files" multiple>
-</form>
-
-<div id="elements"></div>
-
-<div id="drop-overlay" class="fade">Upuść pliki tutaj</div>
-
-<footer class="navbar navbar-default navbar-fixed-bottom">
+{% if error %}
     <div class="container-fluid">
-        <button id="btn-open" class="btn btn-default navbar-btn" onclick="$('#files').click()">Wybierz pliki</button>
-        <p class="navbar-text">lub przeciągnij je w obszar tego okna</p>
-        <div class="navbar-right">
-            <p id="status" class="navbar-text"></p>
-            <button id="btn-close" class="btn btn-default navbar-btn" onclick="window.opener.filex.reloadFiles(); window.close()">Zamknij</button>
+        <div class="alert alert-danger">
+            <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> {{ error }}
         </div>
     </div>
-</footer>
+{% else %}
+    <form enctype="multipart/form-data" method="post" action="{{ url }}" hidden>
+        {% csrf_token %}
+        <input id="files" type="file" name="files" multiple>
+    </form>
 
-<div id="conflict" class="modal" tabindex="-1" role="dialog" aria-labelledby="modal-label" aria-hidden="true">
-    <div class="modal-dialog">
-        <div class="modal-content">
-            <div class="modal-header">
-                <h4 class="modal-title" id="modal-label">Plik już istnieje</h4>
-            </div>
-            <div class="modal-body">
+    <div id="elements"></div>
+
+    <div id="drop-overlay" class="fade">Upuść pliki tutaj</div>
+
+    <footer class="navbar navbar-default navbar-fixed-bottom">
+        <div class="container-fluid">
+            <button id="btn-open" class="btn btn-default navbar-btn" onclick="$('#files').click()">Wybierz pliki</button>
+            <p class="navbar-text">lub przeciągnij je w obszar tego okna</p>
+            <div class="navbar-right">
+                <p id="status" class="navbar-text"></p>
+                <button id="btn-close" class="btn btn-default navbar-btn" onclick="window.opener.filex.reloadFiles(); window.close()">Zamknij</button>
             </div>
-            <div class="modal-footer">
-                <div class="checkbox pull-left">
-                    <label>
-                        <input id="apply-to-all" type="checkbox" autocomplete="off"> Zastosuj do wszystkich
-                    </label>
+        </div>
+    </footer>
+
+    <div id="conflict" class="modal" tabindex="-1" role="dialog" aria-labelledby="modal-label" aria-hidden="true">
+        <div class="modal-dialog">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <h4 class="modal-title" id="modal-label">Plik już istnieje</h4>
+                </div>
+                <div class="modal-body">
+                </div>
+                <div class="modal-footer">
+                    <div class="checkbox pull-left">
+                        <label>
+                            <input id="apply-to-all" type="checkbox" autocomplete="off"> Zastosuj do wszystkich
+                        </label>
+                    </div>
+                    <button id="btn-skip" type="button" class="btn btn-default">Pomiń</button>
+                    <button id="btn-replace" type="button" class="btn btn-default">Zastąp</button>
                 </div>
-                <button id="btn-skip" type="button" class="btn btn-default">Pomiń</button>
-                <button id="btn-replace" type="button" class="btn btn-default">Zastąp</button>
             </div>
         </div>
     </div>
-</div>
+{% endif %}
index 7b64658..678e4a8 100644 (file)
         });
 
         $(window).on('beforeunload', function() {
-            if ($('#files').fileupload('active'))
+            var $files = $('#files');
+            if ($files.length && $files.fileupload('active'))
                 return 'Nie zakończono przesyłania wszystkich plików, czy chcesz kontynuować?';
         });
 
index 9a96949..2b5d72e 100644 (file)
@@ -1,21 +1,23 @@
 from functools import wraps
 
 from django.core.validators import RegexValidator
-from django.http import JsonResponse
+from django.http import JsonResponse, HttpResponse
+from django.template.loader import render_to_string
 from django.views.decorators.csrf import csrf_protect, csrf_exempt
 
 from filex.ftp import FTPError
-from filex.uploadhandler import FtpUploadHandler
 
 
-msg = u'Invalid value'
+_msg = u'Invalid value'
 host_validator = RegexValidator(r'^(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+'
-                                r'(?:[a-zA-Z]{2,6}\.?|[a-zA-Z0-9-]{2,}(?<!-)\.?))(?::\d+)?$', msg)
-path_validator = RegexValidator(r'^~?(?:/[^/\0]*)*$', msg)
-name_validator = RegexValidator(r'^[^/\0]+$', msg)
+                                r'(?:[a-zA-Z]{2,6}\.?|[a-zA-Z0-9-]{2,}(?<!-)\.?))(?::\d+)?$', _msg)
+path_validator = RegexValidator(r'^~?(?:/[^/\0]*)*$', _msg)
+name_validator = RegexValidator(r'^[^/\0]+$', _msg)
 
 
 def with_ftp_upload_handler(view_func):
+    from filex.uploadhandler import FtpUploadHandler
+
     @wraps(view_func)
     def wrapped_view(request, *args, **kwargs):
         request.upload_handlers = [FtpUploadHandler(request)]
@@ -23,12 +25,19 @@ def with_ftp_upload_handler(view_func):
         try:
             return csrf_protect(view_func)(request, *args, **kwargs)
         except FTPError as e:
-            status = 400
-            if 'No such file or directory' in e.message:
-                status = 404
-            elif 'Permission denied' in e.message:
-                status = 403
+            msg, status = parse_ftp_error(e)
 
-            return JsonResponse({'error': e.message}, status=status)
+            return JsonResponse({'error': msg}, status=status)
 
     return csrf_exempt(wrapped_view)
+
+
+def parse_ftp_error(e):
+    msg, status = e.message, 400
+
+    if 'No such file or directory' in msg:
+        status = 404
+    elif 'Permission denied' in msg:
+        status = 403
+
+    return msg, status
index f5aabb8..0d4ab17 100644 (file)
@@ -5,7 +5,7 @@ import os
 from django.contrib.auth.decorators import login_required
 from django.core.exceptions import PermissionDenied
 from django.http import JsonResponse, StreamingHttpResponse
-from django.shortcuts import get_object_or_404
+from django.shortcuts import get_object_or_404, render
 from django.template.defaultfilters import filesizeformat
 from django.utils.formats import date_format
 from django.views.decorators.http import require_POST
@@ -15,12 +15,13 @@ from filex.forms import HostPathNameForm, RenameForm, FavoriteForm, HostPathForm
     CompressForm
 from filex.ftp import FTPOperation, FTPError
 from filex.models import Favorite
-from filex.uploadhandler import with_ftp_upload_handler
+from filex.utils import with_ftp_upload_handler, parse_ftp_error
 
 
 class FTPView(View):
     method = 'get'
     form_class = HostPathForm
+    request = None
 
     @classmethod
     def as_view(cls, **initkwargs):
@@ -35,16 +36,13 @@ class FTPView(View):
             if not form.is_valid():
                 return JsonResponse({'error': form.errors}, status=400)
 
+            self.request = request
             try:
                 return self.handle(FTPOperation(request.session['proxy']), form.cleaned_data)
             except FTPError as e:
-                status = 400
-                if 'No such file or directory' in e.message:
-                    status = 404
-                elif 'Permission denied' in e.message:
-                    status = 403
+                msg, status = parse_ftp_error(e)
 
-                return JsonResponse({'error': e.message}, status=status)
+                return JsonResponse({'error': msg}, status=status)
 
         setattr(cls, cls.method, process)
 
@@ -71,14 +69,23 @@ class ListView(FTPView):
 
 class DownloadView(FTPView):
     def handle(self, ftp, params):
-        data = ftp.get(make_url(params, 'path'))
+        url = make_url(params, 'path')
+
+        try:
+            stats = ftp.info(url)
+        except FTPError as e:
+            msg, status = parse_ftp_error(e)
+
+            return render(self.request, 'qcg/download_error.html', {'msg': msg, 'url': url}, status=status)
+
+        data = ftp.get(url)
 
         name = os.path.basename(params['path'])
         mime_type, encoding = mimetypes.guess_type(name)
 
         response = StreamingHttpResponse(data, content_type=mime_type or 'application/octet-stream')
         response['Content-Disposition'] = u'attachment; filename={}'.format(name)
-        # TODO Content-Length (?)
+        response['Content-Length'] = stats['size']
 
         if encoding:
             response['Content-Encoding'] = encoding
@@ -184,14 +191,20 @@ def fav_add(request):
 
     form = FavoriteForm(data)
 
-    # TODO check if path exists
-    if form.is_valid():
-        instance = form.save()
+    if not form.is_valid():
+        return JsonResponse({'error': form.errors}, status=400)
+
+    try:
+        FTPOperation(request.session['proxy']).info(make_url(form.cleaned_data, 'path'))
+    except FTPError as e:
+        msg, status = parse_ftp_error(e)
+
+        return JsonResponse({'error': msg}, status=status)
 
-        return JsonResponse({'group': 'usr', 'host': instance.host, 'path': instance.path,
-                             'value': instance.host + '/' + instance.path})
+    instance = form.save()
 
-    return JsonResponse({'error': form.errors}, status=400)
+    return JsonResponse({'group': 'usr', 'host': instance.host, 'path': instance.path,
+                         'value': instance.host + '/' + instance.path})
 
 
 @require_POST
diff --git a/qcg/templates/qcg/download_error.html b/qcg/templates/qcg/download_error.html
new file mode 100644 (file)
index 0000000..d9c5146
--- /dev/null
@@ -0,0 +1,8 @@
+{% extends 'qcg/base.html' %}
+
+{% block container %}
+    <h1 class="page-header">{% block title %}Błąd pobierania{% endblock %}</h1>
+
+    <h3>{{ msg }}</h3>
+    <h2><small>{{ url }}</small></h2>
+{% endblock container %}
index adc4676..a1986ad 100644 (file)
@@ -17,6 +17,8 @@ from django_openid_auth.views import make_consumer
 from openid.extensions import ax
 
 from filex.forms import HostPathNameForm, RenameForm, ArchiveForm, HostPathForm
+from filex.ftp import FTPOperation, FTPError
+from filex.views import make_url
 from qcg.forms import FiltersForm, ColumnsForm, JobDescriptionForm, EnvFormSet
 from qcg.utils import paginator_context
 from qcg.service import update_user_data, submit_job
@@ -185,7 +187,7 @@ def job_new(request):
 @login_required
 def gridftp(request):
     return render(request, 'qcg/gridftp.html',
-                  {'new_dir_form': HostPathNameForm(), 'rename_form': RenameForm(),  'archive_form': ArchiveForm()})
+                  {'new_dir_form': HostPathNameForm(), 'rename_form': RenameForm(), 'archive_form': ArchiveForm()})
 
 
 @login_required
@@ -195,7 +197,13 @@ def gridftp_upload(request):
     if not form.is_valid():
         raise SuspiciousOperation('Invalid parameters for `gridftp_upload`!')
 
+    error = None
+    try:
+        FTPOperation(request.session['proxy']).info(make_url(form.cleaned_data, 'path'))
+    except FTPError as e:
+        error = e.message
+
     return render(request, 'qcg/gridftp_upload.html',
-                  {'url': reverse('filex:upload') + '?' + urlencode(form.cleaned_data),
+                  {'error': error, 'url': reverse('filex:upload') + '?' + urlencode(form.cleaned_data),
                    'host': form.cleaned_data['host'], 'path': form.cleaned_data['path'],
                    'sep': '/' if form.cleaned_data['path'].startswith('~') else ''})