Преглед изворни кода

HUE-2925 [fb] Allow to use absolute URI

Ivan Orlov пре 10 година
родитељ
комит
bff291a

+ 3 - 3
apps/filebrowser/src/filebrowser/forms.py

@@ -21,12 +21,11 @@ import urllib
 from django import forms
 from django.contrib.auth.models import User, Group
 from django.forms import FileField, CharField, BooleanField, Textarea
-from django.forms.formsets import formset_factory, BaseFormSet, ManagementForm
+from django.forms.formsets import formset_factory, BaseFormSet
 
 from desktop.lib import i18n
+from desktop.lib.fs.utils import normpath
 from filebrowser.lib import rwx
-from hadoop.fs import normpath
-
 
 from django.utils.translation import ugettext_lazy as _
 
@@ -63,6 +62,7 @@ class PathField(CharField):
   def clean(self, value):
     return normpath(CharField.clean(self, value))
 
+
 class EditorForm(forms.Form):
   path = PathField(label=_("File to edit"))
   contents = CharField(widget=Textarea, label=_("Contents"), required=False)

+ 12 - 7
apps/filebrowser/src/filebrowser/templates/fb_components.mako

@@ -26,8 +26,8 @@ from django.utils.translation import ugettext as _
         <li>
             <span style="float:right; margin-top:10px;"><i id="editBreadcrumb" class="fa fa-pencil hand" rel="tooltip" title="${_('Edit path')}"></i></span>
             <ul class="hueBreadcrumb" data-bind="foreach: breadcrumbs" style="padding-right:40px; padding-top: 12px">
-                <li data-bind="visible: label == '/'"><a href="#" data-bind="click: show"><span class="divider" data-bind="text: label"></span></a></li>
-                <li data-bind="visible: label != '/'"><a href="#" data-bind="text: label, click: show"></a><span class="divider">/</span></li>
+                <li data-bind="visible: label.slice(-1) == '/'"><a href="#" data-bind="click: show"><span class="divider" data-bind="text: label"></span></a></li>
+                <li data-bind="visible: label.slice(-1) != '/'"><a href="#" data-bind="text: label, click: show"></a><span class="divider">/</span></li>
             </ul>
             <input id="hueBreadcrumbText" type="text" class="input-xxlarge" style="margin-top:4px;margin-right:4px;display:none" data-bind="value: currentPath" autocomplete="off" />
         </li>
@@ -48,12 +48,17 @@ from django.utils.translation import ugettext as _
         <li>
             <ul class="hueBreadcrumb" style="padding-right:40px; padding-top: 12px">
                     % for breadcrumb_item in breadcrumbs:
-                    <% label = breadcrumb_item['label'] %>
-                    %if label == '/':
-                            <li><a href="/filebrowser/view=${breadcrumb_item['url']}"><span
-                                    class="divider">${label}</span></a></li>
+                    <% label, f_url = breadcrumb_item['label'], breadcrumb_item['url'] %>
+                    %if label[-1] == '/':
+                            <li>
+                              <a href="${url('filebrowser.views.view', path=f_url)}">
+                                <span class="divider">${label}</span>
+                              </a>
+                            </li>
                     %else:
-                            <li><a href="/filebrowser/view=${breadcrumb_item['url']}">${label}</a><span class="divider">/</span></li>
+                            <li>
+                              <a href="${url('filebrowser.views.view', path=f_url)}">${label}</a>
+                              <span class="divider">/</span></li>
                     %endif
                     % endfor
             </ul>

+ 20 - 20
apps/filebrowser/src/filebrowser/templates/listdir_components.mako

@@ -602,7 +602,7 @@ from django.utils.translation import ugettext as _
       $('.history').removeClass('no-history');
       var history = getHistory();
       if (path != '/filebrowser/') {
-        var _basePath = '${url('filebrowser.views.view', path=urlencode('/'))}';
+        var _basePath = '${url('filebrowser.views.view', path='')}';
         if (path.indexOf(_basePath) > -1) {
           path = path.substr(_basePath.length - 1);
         }
@@ -748,7 +748,7 @@ from django.utils.translation import ugettext as _
           }
 
           viewModel.targetPageNum(1);
-          viewModel.targetPath("${url('filebrowser.views.view', path=urlencode('/'))}" + stripHashes(this.url));
+          viewModel.targetPath("${url('filebrowser.views.view', path='')}" + stripHashes(this.url));
           location.hash = this.url;
         }
       }
@@ -998,7 +998,7 @@ from django.utils.translation import ugettext as _
           // Reset page number so that we don't hit a page that doesn't exist
           self.targetPageNum(1);
           self.searchQuery("");
-          self.targetPath("${url('filebrowser.views.view', path=urlencode('/'))}" + "." + stripHashes(file.path));
+          self.targetPath("${url('filebrowser.views.view', path='')}" + stripHashes(file.path));
           location.hash = stripHashes(file.path);
         } else {
           location.href = file.url;
@@ -1006,11 +1006,11 @@ from django.utils.translation import ugettext as _
       };
 
       self.editFile = function () {
-        location.href = "${url('filebrowser.views.edit', path=urlencode('/'))}" + self.selectedFile().path;
+        location.href = "${url('filebrowser.views.edit', path='')}" + self.selectedFile().path;
       };
 
       self.downloadFile = function () {
-        location.href = "${url('filebrowser.views.download', path=urlencode('/'))}" + self.selectedFile().path;
+        location.href = "${url('filebrowser.views.download', path='')}" + self.selectedFile().path;
       };
 
       self.renameFile = function () {
@@ -1020,7 +1020,7 @@ from django.utils.translation import ugettext as _
 
         $("#newNameInput").val(self.selectedFile().name);
 
-        $("#renameForm").attr("action", "/filebrowser/rename?next=${url('filebrowser.views.view', path=urlencode('/'))}" + "." + self.currentPath());
+        $("#renameForm").attr("action", "/filebrowser/rename?next=${url('filebrowser.views.view', path='')}" + self.currentPath());
 
         $("#renameModal").modal({
           keyboard:true,
@@ -1042,7 +1042,7 @@ from django.utils.translation import ugettext as _
         if (!isMoveOnSelf){
           hiddenFields($("#moveForm"), "src_path", paths);
 
-          $("#moveForm").attr("action", "/filebrowser/move?next=${url('filebrowser.views.view', path=urlencode('/'))}" + "." + self.currentPath());
+          $("#moveForm").attr("action", "/filebrowser/move?next=${url('filebrowser.views.view', path='')}" + self.currentPath());
 
           if (mode === 'nomodal') {
             $.jHueNotify.info('${ _('Items moving to') } "' + $('#moveDestination').val() + '"');
@@ -1082,7 +1082,7 @@ from django.utils.translation import ugettext as _
 
         hiddenFields($("#copyForm"), "src_path", paths);
 
-        $("#copyForm").attr("action", "/filebrowser/copy?next=${url('filebrowser.views.view', path=urlencode('/'))}" + "." + self.currentPath());
+        $("#copyForm").attr("action", "/filebrowser/copy?next=${url('filebrowser.views.view', path='')}" + self.currentPath());
 
         $("#copyModal").modal({
           keyboard:true,
@@ -1116,7 +1116,7 @@ from django.utils.translation import ugettext as _
 
           hiddenFields($("#chownForm"), 'path', paths);
 
-          $("#chownForm").attr("action", "/filebrowser/chown?next=${url('filebrowser.views.view', path=urlencode('/'))}" + "." + self.currentPath());
+          $("#chownForm").attr("action", "/filebrowser/chown?next=${url('filebrowser.views.view', path='')}" + self.currentPath());
 
           $("select[name=user]").val(self.selectedFile().stats.user);
 
@@ -1153,7 +1153,7 @@ from django.utils.translation import ugettext as _
 
           hiddenFields($("#chmodForm"), 'path', paths);
 
-          $("#chmodForm").attr("action", "/filebrowser/chmod?next=${url('filebrowser.views.view', path=urlencode('/'))}" + "." + self.currentPath());
+          $("#chmodForm").attr("action", "/filebrowser/chmod?next=${url('filebrowser.views.view', path='')}" + self.currentPath());
 
           $("#changePermissionModal").modal({
             keyboard: true,
@@ -1196,7 +1196,7 @@ from django.utils.translation import ugettext as _
 
         $("#deleteForm").attr("action", "/filebrowser/rmtree" + "?" +
           (skip_trash ? "skip_trash=true&" : "") +
-          "next=${url('filebrowser.views.view', path=urlencode('/'))}" + "." + self.currentPath());
+          "next=${url('filebrowser.views.view', path='')}" + self.currentPath());
 
         $("#deleteModal").modal({
           keyboard:true,
@@ -1224,12 +1224,12 @@ from django.utils.translation import ugettext as _
       };
 
       self.createDirectory = function (formElement) {
-        $(formElement).attr("action", "/filebrowser/mkdir?next=${url('filebrowser.views.view', path=urlencode('/'))}" + "." + self.currentPath());
+        $(formElement).attr("action", "/filebrowser/mkdir?next=${url('filebrowser.views.view', path='')}" + self.currentPath());
         return true;
       };
 
       self.createFile = function (formElement) {
-        $(formElement).attr("action", "/filebrowser/touch?next=${url('filebrowser.views.view', path=urlencode('/'))}" + "." + self.currentPath());
+        $(formElement).attr("action", "/filebrowser/touch?next=${url('filebrowser.views.view', path='')}" + self.currentPath());
         return true;
       };
 
@@ -1242,7 +1242,7 @@ from django.utils.translation import ugettext as _
 
         hiddenFields($("#restoreTrashForm"), 'path', paths);
 
-        $("#restoreTrashForm").attr("action", "/filebrowser/trash/restore?next=${url('filebrowser.views.view', path=urlencode('/'))}" + "." + self.currentPath());
+        $("#restoreTrashForm").attr("action", "/filebrowser/trash/restore?next=${url('filebrowser.views.view', path='')}" + self.currentPath());
 
         $("#restoreTrashModal").modal({
           keyboard:true,
@@ -1259,7 +1259,7 @@ from django.utils.translation import ugettext as _
 
         hiddenFields($("#purgeTrashForm"), 'path', paths);
 
-        $("#purgeTrashForm").attr("action", "/filebrowser/trash/purge?next=${url('filebrowser.views.view', path=urlencode('/'))}" + viewModel.homeDir().slice(1) + "/.Trash");
+        $("#purgeTrashForm").attr("action", "/filebrowser/trash/purge?next=${url('filebrowser.views.view', path='')}" + viewModel.homeDir() + "/.Trash");
 
         $("#purgeTrashModal").modal({
           keyboard:true,
@@ -1783,9 +1783,9 @@ from django.utils.translation import ugettext as _
         var targetPath = "";
         var hash = window.location.hash.substring(1).replace(/(<([^>]+)>)/ig, "");
         if (hash != null && hash != "") {
-          targetPath = "${url('filebrowser.views.view', path=urlencode('/'))}";
+          targetPath = "${url('filebrowser.views.view', path='')}";
           if (hash.indexOf("!!") != 0) {
-            targetPath += stripHashes(hash.substring(1));
+            targetPath += stripHashes(hash);
           }
           else {
             targetPath = viewModel.targetPath() + hash;
@@ -1825,7 +1825,7 @@ from django.utils.translation import ugettext as _
         home: "/user/${ user }/",
         skipKeydownEvents: true,
         onEnter: function (el) {
-          viewModel.targetPath("${url('filebrowser.views.view', path=urlencode('/'))}" + stripHashes(el.val().substring(1)));
+          viewModel.targetPath("${url('filebrowser.views.view', path='')}" + stripHashes(el.val()));
           viewModel.getStats(function (data) {
             if (data.type != null && data.type == "file") {
               location.href = data.url;
@@ -1860,9 +1860,9 @@ from django.utils.translation import ugettext as _
         if (hash != null && hash != "") {
           addPathToHistory(hash);
 
-          targetPath = "${url('filebrowser.views.view', path=urlencode('/'))}";
+          targetPath = "${url('filebrowser.views.view', path='')}";
           if (hash.indexOf("!!") != 0) {
-            targetPath += stripHashes(hash.substring(1));
+            targetPath += stripHashes(hash);
           }
           else {
             targetPath = viewModel.targetPath() + hash;

+ 18 - 20
apps/filebrowser/src/filebrowser/views.py

@@ -31,7 +31,6 @@ from datetime import datetime
 
 from django.contrib import messages
 from django.contrib.auth.models import User, Group
-from django.core import urlresolvers
 from django.core.urlresolvers import reverse
 from django.template.defaultfilters import stringformat, filesizeformat
 from django.http import Http404, HttpResponse, HttpResponseNotModified
@@ -50,12 +49,13 @@ from avro import datafile, io
 from desktop import appmanager
 from desktop.lib import i18n, paginator
 from desktop.lib.conf import coerce_bool
-from desktop.lib.django_util import make_absolute, render, render_json, format_preserving_redirect
+from desktop.lib.django_util import make_absolute, render, format_preserving_redirect
 from desktop.lib.django_util import JsonResponse
 from desktop.lib.exceptions_renderable import PopupException
+from desktop.lib.fs import splitpath
 from hadoop.fs.hadoopfs import Hdfs
 from hadoop.fs.exceptions import WebHdfsException
-from hadoop.fs.fsutils import do_newfile_save, do_overwrite_save
+from hadoop.fs.fsutils import do_overwrite_save
 
 from filebrowser.conf import MAX_SNAPPY_DECOMPRESSION_SIZE
 from filebrowser.conf import SHOW_DOWNLOAD_BUTTON
@@ -262,14 +262,13 @@ def save_file(request):
     if not is_valid:
         return edit(request, path, form=form)
 
+    encoding = form.cleaned_data['encoding']
+    data = form.cleaned_data['contents'].encode(encoding)
+
     if request.fs.exists(path):
-        do_overwrite_save(request.fs, path,
-                           form.cleaned_data['contents'],
-                           form.cleaned_data['encoding'])
+        do_overwrite_save(request.fs, path, data)
     else:
-        do_newfile_save(request.fs, path,
-                         form.cleaned_data['contents'],
-                         form.cleaned_data['encoding'])
+        request.fs.create(path, overwrite=False, data=data)
 
     messages.info(request, _('Saved %(path)s.') % {'path': os.path.basename(path)})
     request.path = reverse("filebrowser.views.edit", kwargs=dict(path=path))
@@ -277,14 +276,13 @@ def save_file(request):
 
 
 def parse_breadcrumbs(path):
-    breadcrumbs_parts = Hdfs.normpath(path).split('/')
-    i = 1
-    breadcrumbs = [{'url': '', 'label': '/'}]
-    while (i < len(breadcrumbs_parts)):
-        breadcrumb_url = breadcrumbs[i - 1]['url'] + '/' + breadcrumbs_parts[i]
-        if breadcrumb_url != '/':
-            breadcrumbs.append({'url': breadcrumb_url, 'label': breadcrumbs_parts[i]})
-        i = i + 1
+    parts = splitpath(path)
+    url, breadcrumbs = '', []
+    for part in parts:
+      if url and not url.endswith('/'):
+        url += '/'
+      url += part
+      breadcrumbs.append({'url': url, 'label': part})
     return breadcrumbs
 
 
@@ -327,7 +325,7 @@ def listdir(request, path, chooser):
     stats = request.fs.listdir_stats(path)
 
     # Include parent dir, unless at filesystem root.
-    if Hdfs.normpath(path) != posixpath.sep:
+    if not request.fs.isroot(path):
         parent_path = request.fs.join(path, "..")
         parent_stat = request.fs.stats(parent_path)
         # The 'path' field would be absolute, but we want its basename to be
@@ -411,7 +409,7 @@ def listdir_paged(request, path):
     shown_stats = page.object_list
 
     # Include parent dir always as second option, unless at filesystem root.
-    if Hdfs.normpath(path) != posixpath.sep:
+    if not request.fs.isroot(path):
         parent_path = request.fs.join(path, "..")
         parent_stat = request.fs.stats(parent_path)
         # The 'path' field would be absolute, but we want its basename to be
@@ -483,7 +481,7 @@ def _massage_stats(request, stats):
     into the format that the views would like it in.
     """
     path = stats['path']
-    normalized = Hdfs.normpath(path)
+    normalized = request.fs.normpath(path)
     return {
         'path': normalized,
         'name': stats['name'],

+ 15 - 0
desktop/core/src/desktop/lib/fs/__init__.py

@@ -16,6 +16,21 @@
 
 from __future__ import absolute_import
 
+import posixpath
+import urlparse
+
 from desktop.lib.fs.proxyfs import ProxyFS
 
 
+def splitpath(path):
+  split = urlparse.urlparse(path)
+  if split.scheme:
+    parts = [split.scheme + '://', split.netloc] + split.path.split('/')
+  else:
+    parts = ['/'] + posixpath.normpath(path).split('/')
+  # Filter empty parts out
+  return filter(len, parts)
+
+
+
+

+ 36 - 0
desktop/core/src/desktop/lib/fs/fs_test.py

@@ -0,0 +1,36 @@
+# Licensed to Cloudera, Inc. under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  Cloudera, Inc. licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+
+from nose.tools import eq_
+
+from desktop.lib import fs
+
+
+def test_splitpath():
+  s = fs.splitpath
+
+  eq_(s('s3://'), ['s3://'])
+  eq_(s('s3://bucket'), ['s3://', 'bucket'])
+  eq_(s('s3://bucket/key'), ['s3://', 'bucket', 'key'])
+  eq_(s('s3://bucket/key/'), ['s3://', 'bucket', 'key'])
+  eq_(s('s3://bucket/bar/foo'), ['s3://', 'bucket', 'bar', 'foo'])
+
+  eq_(s('/'), ['/'])
+  eq_(s('/dir'), ['/', 'dir'])
+  eq_(s('/dir/file'), ['/', 'dir', 'file'])
+  eq_(s('/dir/file/'), ['/', 'dir', 'file'])

+ 9 - 11
desktop/core/src/desktop/static/desktop/js/jquery.hdfsautocomplete.js

@@ -211,18 +211,16 @@
     var BASE_PATH = "/filebrowser/view=";
     var _currentFiles = [];
 
+    function prepareAutocompletePath(path) {
+      if (path.indexOf('/') == 0 || /^([a-zA-Z0-9]+):\/\//.test(path))
+        return path.substr(0, path.lastIndexOf("/") + 1);
+      if (path.indexOf("/") > 0)
+        return _this.options.home + path.substr(0, path.lastIndexOf("/"));
+      return _this.options.home;
+    }
+
     function showHdfsAutocomplete(callback) {
-      var path = _el.val();
-      var autocompleteUrl = BASE_PATH;
-      if (path.indexOf("/") == 0) {
-        autocompleteUrl += path.substr(0, path.lastIndexOf("/"));
-      }
-      else if (path.indexOf("/") > 0) {
-        autocompleteUrl += _this.options.home + path.substr(0, path.lastIndexOf("/"));
-      }
-      else {
-        autocompleteUrl += _this.options.home;
-      }
+      autocompleteUrl = BASE_PATH + prepareAutocompletePath(_el.val());
       $.getJSON(autocompleteUrl + "?pagesize=1000&format=json", function (data) {
         _currentFiles = [];
         if (data.error == null) {

+ 5 - 21
desktop/libs/hadoop/src/hadoop/fs/fsutils.py

@@ -23,21 +23,17 @@ import stat as stat_module
 logger = logging.getLogger(__name__)
 
 
-def do_overwrite_save(fs, path, data, encoding):
+def do_overwrite_save(fs, path, data):
 
     def copy_data(path_dest):
-        new_file = fs.open(path_dest, "w")
         try:
-            try:
-                new_file.write(data.encode(encoding))
-                logging.info("Wrote to " + path_dest)
-            finally:
-                new_file.close()
+            fs.create(path_dest, overwrite=False, data=data)
+            logging.info("Wrote to " + path_dest)
         except Exception, e:
             # An error occurred in writing, we should clean up
             # the tmp file if it exists, before re-raising
             try:
-                fs.remove(path_dest)
+                fs.remove(path_dest, skip_trash=True)
             except:
                 logger.exception('failed to remove %s' % path_dest)
             raise e
@@ -84,7 +80,7 @@ def _do_overwrite(fs, path, copy_data):
         # but not the end of the world - keep going
 
     # Now delete the old - nothing we can do here to recover
-    fs.remove(path)
+    fs.remove(path, skip_trash=True)
 
     # Now move the new one into place
     # If this fails, then we have no reason to assume
@@ -93,15 +89,3 @@ def _do_overwrite(fs, path, copy_data):
     fs.rename(path_dest, path)
 
 
-def do_newfile_save(fs, path, data, encoding):
-    """
-    Save data to the path 'path' on the filesystem 'fs'.
-
-    There must not be a pre-existing file at that path.
-    """
-    new_file = fs.open(path, "w")
-    try:
-        new_file.write(data.encode(encoding))
-    finally:
-        new_file.close()
-

+ 2 - 2
desktop/libs/hadoop/src/hadoop/fs/fsutils_tests.py

@@ -18,7 +18,7 @@
 import logging
 import unittest
 
-from nose.tools import assert_equals, assert_true, assert_not_equal
+from nose.tools import assert_equals, assert_not_equal
 
 from desktop.lib import i18n
 
@@ -60,7 +60,7 @@ curacao\t?"""
     f.close()
 
     encoding = i18n.get_site_encoding()
-    do_overwrite_save(fs, path, data, encoding)
+    do_overwrite_save(fs, path, data.encode(encoding))
 
     assert_not_equal(data_body, fs.open(path).read())