فهرست منبع

[frontent] Support special and non ascii characters in file download (#2953)

* [frontent] Support special and non ascii characters in file download

Verified downloads with HDFS, S3 and ABFS
Bjorn Alm 3 سال پیش
والد
کامیت
ba36160800

+ 26 - 8
apps/filebrowser/src/filebrowser/templates/display.mako

@@ -227,6 +227,18 @@ ${ fb_components.menubar() }
     return view.contents.match(new RegExp('[\\s\\S]{1,' + chunkSize + '}', 'g'));
   }
 
+  // The python backend incorrectly encodes a couple of characters that we fix
+  // in the frontend here 
+  function fixSpecialCharacterEncoding (url) {
+    // Singel quotes (') encoded as Unicode Hex Character
+    // will cause the $.getJSON call and file downloads to produce an incorrect url, 
+    // so we remove the encoding and use plain single quotes.
+    const modifiedUrl = url.replaceAll(''', "'");
+    // Entity encoded ampersand doesn't work in file or folder names and
+    // needs to be replaced with '&'
+    return modifiedUrl.replaceAll('&', "&");    
+  }
+
   function getContent (callback) {
     var _baseUrl = "${url('filebrowser:filebrowser.views.view', path=path_enc)}";
 
@@ -242,13 +254,8 @@ ${ fb_components.menubar() }
       mode: viewModel.mode()
     };
 
-    // Singel quotes (') encoded as Unicode Hex Character
-    // will cause the $.getJSON call to produce an incorrect url, 
-    // so we remove the encoding and use plain single quotes.
-    let contentUrl = _baseUrl.replaceAll(''', "'");
-    // Entity encoded ampersand doesn't work in file or folder names and
-    // needs to be replaced with '&'
-    contentUrl = contentUrl.replaceAll('&', "&");
+    contentUrl = fixSpecialCharacterEncoding(_baseUrl);
+
     $.getJSON(contentUrl, params, function (data) {
       var _html = "";
 
@@ -387,7 +394,18 @@ ${ fb_components.menubar() }
     }
 
     self.downloadFile = function () {
-      huePubSub.publish('open.link', "${url('filebrowser:filebrowser_views_download', path=path_enc)}");
+      const filePath = "${path}";
+      const correctedFilePath = fixSpecialCharacterEncoding(filePath);
+      // If the hash characters aren't encoded the page library will
+      // split the path on the first occurence and the remaining string will not
+      // be part of the path. Question marks must also be encoded or the string after the first
+      // question mark will be interpreted as the url querystring.
+      const partiallyEncodedFilePath = correctedFilePath.replaceAll('#', encodeURIComponent('#'))
+        .replaceAll('?', encodeURIComponent('?'));
+      const baseUrl = "${url('filebrowser:filebrowser_views_download', path='')}";
+      const fullUrl = baseUrl+partiallyEncodedFilePath;
+
+      huePubSub.publish('open.link', fullUrl);
     };
 
     self.pageChanged = function () {

+ 10 - 1
apps/filebrowser/src/filebrowser/templates/listdir_components.mako

@@ -1394,7 +1394,16 @@ else:
       };
 
       self.downloadFile = function () {
-        huePubSub.publish('open.link', self.selectedFile().url.replace("${url('filebrowser:filebrowser.views.view', path='')}", "${url('filebrowser:filebrowser_views_download', path='')}"));
+        const baseUrl = "${url('filebrowser:filebrowser_views_download', path='')}";
+      // If the hash characters aren't encoded the page library will
+      // split the path on the first occurence and the remaining string will not
+      // be part of the path. Question marks must also be encoded or the string after the first
+      // question mark will be interpreted as the url querystring.      
+        const partiallyEncodedFilePath = self.selectedFile().path.replaceAll('#', encodeURIComponent('#'))
+          .replaceAll('?', encodeURIComponent('?'));;
+        const fullUrl = baseUrl+partiallyEncodedFilePath;
+        
+        huePubSub.publish('open.link', fullUrl);
       };
 
       self.renameFile = function () {

+ 38 - 12
desktop/core/src/desktop/js/onePageViewModel.js

@@ -621,7 +621,8 @@ class OnePageViewModel {
       {
         url: '/filebrowser/download=*',
         app: function (ctx) {
-          location.href = window.HUE_BASE_URL + '/filebrowser/download=' + ctx.params[0];
+          const filePathParam = getModifiedCtxParamForFilePath(ctx, '/filebrowser/download=*');
+          location.href = window.HUE_BASE_URL + '/filebrowser/download=' + filePathParam;
         }
       },
       {
@@ -832,19 +833,41 @@ class OnePageViewModel {
     });
 
     // The page library encodes the plus character (+) as a space in the returning
-    // params object. That causes the path used by the file viewer to break and this
+    // params object. That causes the path used by the file viewer and downloader to break and this
     // is a defensive fix to handle that while still having access to the other ctx params
     // needed to "revert" the incorrect encoding.
-    const fixPlusCharTurnedIntoSpace = (ctx, mapping) => {
-      let correctCtxParams = ctx.params;
-      if (mapping.app === 'filebrowser' && ctx.path.indexOf('+') >= 0) {
-        if (ctx.params[0] && ctx.params[0].indexOf(' ') >= 0) {
-          const pathWithoutParams = mapping.url.slice(0, -1);
-          const modifiedParam = decodeURIComponent(ctx.path.replace(pathWithoutParams, ''));
-          correctCtxParams = { 0: modifiedParam };
-        }
+    const fixPlusCharTurnedIntoSpace = (ctx, pathWithoutParams) => {
+      const pathHasPlus = ctx.path.indexOf('+') >= 0;
+      const paramsHaveSpaces = ctx.params[0] && ctx.params[0].indexOf(' ') >= 0;
+
+      if (pathHasPlus && paramsHaveSpaces) {
+        // The original params with the plus character (+) can still be found in ctx.path
+        // and we use those to create our own encoded version that handles the plus character correctly.
+        const paramsOnly = ctx.path.replace(pathWithoutParams, '');
+        return decodeURIComponent(paramsOnly);
       }
-      return correctCtxParams;
+
+      return ctx?.params[0];
+    };
+
+    // The page library decodes any hash characters (#) in the params object, so unless it is explicitly
+    // encoded again the file download links will break.
+    const fixHashCharInParam = (ctx, filePathParam) => {
+      const pathHasUTF8EncodedHash = ctx.path.indexOf('%23') >= 0;
+      const paramHasHash = ctx.params[0] && ctx.params[0].indexOf('#') >= 0;
+      const encodeHashChar = pathHasUTF8EncodedHash && paramHasHash;
+
+      return encodeHashChar
+        ? filePathParam.replaceAll('#', encodeURIComponent('#'))
+        : filePathParam;
+    };
+
+    const getModifiedCtxParamForFilePath = (ctx, mappingUrl) => {
+      const pathWithoutParams = mappingUrl.slice(0, -1);
+      let filePathParam = fixPlusCharTurnedIntoSpace(ctx, pathWithoutParams);
+      filePathParam = fixHashCharInParam(ctx, filePathParam);
+
+      return filePathParam.replaceAll('?', encodeURIComponent('?'));
     };
 
     pageMapping.forEach(mapping => {
@@ -853,7 +876,10 @@ class OnePageViewModel {
         _.isFunction(mapping.app)
           ? mapping.app
           : ctx => {
-              const ctxParams = fixPlusCharTurnedIntoSpace(ctx, mapping);
+              const ctxParams =
+                mapping.app === 'filebrowser'
+                  ? { 0: getModifiedCtxParamForFilePath(ctx, mapping.url) }
+                  : ctx.params;
               self.currentContextParams(ctxParams);
               self.currentQueryString(ctx.querystring);
               self.loadApp(mapping.app);