ambari-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From srima...@apache.org
Subject ambari git commit: AMBARI-Files View: Enable to preview files in WEBHDFS (Pallav Kulshreshtha via srimanth)
Date Wed, 04 Nov 2015 19:42:15 GMT
Repository: ambari
Updated Branches:
  refs/heads/trunk af56de5ac -> 03a7b9d0e


AMBARI-Files View: Enable to preview files in WEBHDFS (Pallav Kulshreshtha via srimanth)


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/03a7b9d0
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/03a7b9d0
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/03a7b9d0

Branch: refs/heads/trunk
Commit: 03a7b9d0ef4b814b27778e5479ddd4a50a632ac4
Parents: af56de5
Author: Srimanth Gunturi <sgunturi@hortonworks.com>
Authored: Wed Nov 4 11:23:24 2015 -0800
Committer: Srimanth Gunturi <sgunturi@hortonworks.com>
Committed: Wed Nov 4 11:38:34 2015 -0800

----------------------------------------------------------------------
 .../view/filebrowser/FileBrowserService.java    | 10 +++
 .../view/filebrowser/FilePreviewService.java    | 92 ++++++++++++++++++++
 .../files/src/main/resources/ui/app/adapter.js  |  2 +-
 .../main/resources/ui/app/controllers/file.js   | 13 ++-
 .../main/resources/ui/app/controllers/files.js  | 10 ++-
 .../ui/app/controllers/previewModal.js          | 87 ++++++++++++++++++
 .../src/main/resources/ui/app/initialize.js     |  3 +
 .../src/main/resources/ui/app/routes/file.js    | 23 ++++-
 .../ui/app/templates/modal/preview.hbs          | 33 +++++++
 .../main/resources/ui/app/views/modalPreview.js | 51 +++++++++++
 .../files/src/main/resources/ui/bower.json      |  3 +-
 .../files/src/main/resources/ui/package.json    |  3 +-
 12 files changed, 324 insertions(+), 6 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/03a7b9d0/contrib/views/files/src/main/java/org/apache/ambari/view/filebrowser/FileBrowserService.java
----------------------------------------------------------------------
diff --git a/contrib/views/files/src/main/java/org/apache/ambari/view/filebrowser/FileBrowserService.java
b/contrib/views/files/src/main/java/org/apache/ambari/view/filebrowser/FileBrowserService.java
index 9224331..fd1c710 100644
--- a/contrib/views/files/src/main/java/org/apache/ambari/view/filebrowser/FileBrowserService.java
+++ b/contrib/views/files/src/main/java/org/apache/ambari/view/filebrowser/FileBrowserService.java
@@ -68,4 +68,14 @@ public class FileBrowserService {
     return new HelpService(context);
   }
 
+
+  /**
+   * @see org.apache.ambari.view.filebrowser.FilePreviewService
+   * @return service
+   */
+  @Path("/preview")
+  public FilePreviewService preview() {
+    return new FilePreviewService(context);
+  }
+
 }

http://git-wip-us.apache.org/repos/asf/ambari/blob/03a7b9d0/contrib/views/files/src/main/java/org/apache/ambari/view/filebrowser/FilePreviewService.java
----------------------------------------------------------------------
diff --git a/contrib/views/files/src/main/java/org/apache/ambari/view/filebrowser/FilePreviewService.java
b/contrib/views/files/src/main/java/org/apache/ambari/view/filebrowser/FilePreviewService.java
new file mode 100644
index 0000000..0c1344d
--- /dev/null
+++ b/contrib/views/files/src/main/java/org/apache/ambari/view/filebrowser/FilePreviewService.java
@@ -0,0 +1,92 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.
+ */
+
+package org.apache.ambari.view.filebrowser;
+
+import org.apache.ambari.view.ViewContext;
+import org.apache.ambari.view.filebrowser.utils.NotFoundFormattedException;
+import org.apache.ambari.view.filebrowser.utils.ServiceFormattedException;
+import org.apache.ambari.view.utils.hdfs.HdfsApi;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.FileStatus;
+import org.apache.hadoop.io.compress.CompressionCodec;
+import org.apache.hadoop.io.compress.CompressionCodecFactory;
+import org.json.simple.JSONObject;
+
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+
+/**
+ * File Preview Service
+ */
+public class FilePreviewService extends HdfsService{
+
+  private CompressionCodecFactory compressionCodecFactory;
+
+  public FilePreviewService(ViewContext context) {
+    super(context);
+
+    Configuration conf = new Configuration();
+    conf.set("io.compression.codecs","org.apache.hadoop.io.compress.GzipCodec," +
+      "org.apache.hadoop.io.compress.DefaultCodec,org.apache.hadoop.io.compress.SnappyCodec,"
+
+      "org.apache.hadoop.io.compress.BZip2Codec");
+
+    compressionCodecFactory = new CompressionCodecFactory(conf);
+  }
+
+  @GET
+  @Path("/file")
+  @Produces(MediaType.APPLICATION_JSON)
+  public Response previewFile(@QueryParam("path") String path,@QueryParam("start") int start,@QueryParam("end")
int end) {
+
+    try {
+      HdfsApi api = getApi(context);
+      FileStatus status = api.getFileStatus(path);
+
+      CompressionCodec codec = compressionCodecFactory.getCodec(status.getPath());
+
+      // check if we have a compression codec we need to use
+      InputStream stream = (codec != null) ? codec.createInputStream(api.open(path)) : api.open(path);
+
+      int length = end - start;
+      byte[] bytes = new byte[length];
+     // ((Seekable)stream).seek(start); //seek(start);
+      stream.skip(start);
+      int readBytes = stream.read(bytes, 0, length);
+      boolean isFileEnd = false;
+
+      if (readBytes < length) isFileEnd = true;
+
+      JSONObject response = new JSONObject();
+      response.put("data", new String(bytes));
+      response.put("readbytes", readBytes);
+      response.put("isFileEnd", isFileEnd);
+
+      return Response.ok(response).build();
+    } catch (WebApplicationException ex) {
+      throw ex;
+    } catch (FileNotFoundException ex) {
+      throw new NotFoundFormattedException(ex.getMessage(), ex);
+    } catch (Exception ex) {
+      throw new ServiceFormattedException(ex.getMessage(), ex);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/03a7b9d0/contrib/views/files/src/main/resources/ui/app/adapter.js
----------------------------------------------------------------------
diff --git a/contrib/views/files/src/main/resources/ui/app/adapter.js b/contrib/views/files/src/main/resources/ui/app/adapter.js
index 6ec763b..74cb988 100644
--- a/contrib/views/files/src/main/resources/ui/app/adapter.js
+++ b/contrib/views/files/src/main/resources/ui/app/adapter.js
@@ -296,7 +296,7 @@ App.ApplicationStore = DS.Store.extend({
         option = option || "browse";
 
     if (option == 'browse') {
-      query = { "path": files.get('firstObject.path'), "download": download };
+      query = { "path": (files.get('firstObject.path') || files.get('id')), "download": download
};
       resolver.resolve(adapter.downloadUrl('browse',query));
       return resolver.promise;
     }

http://git-wip-us.apache.org/repos/asf/ambari/blob/03a7b9d0/contrib/views/files/src/main/resources/ui/app/controllers/file.js
----------------------------------------------------------------------
diff --git a/contrib/views/files/src/main/resources/ui/app/controllers/file.js b/contrib/views/files/src/main/resources/ui/app/controllers/file.js
index cf87041..b5054e4 100644
--- a/contrib/views/files/src/main/resources/ui/app/controllers/file.js
+++ b/contrib/views/files/src/main/resources/ui/app/controllers/file.js
@@ -21,6 +21,13 @@ var App = require('app');
 App.FileController = Ember.ObjectController.extend({
   needs:['files'],
   actions:{
+    confirmPreview:function (option) {
+      if (this.get('content.readAccess')) {
+        this.store.linkFor([this.get('content')],option).then(function (link) {
+          window.location.href = link;
+        },Em.run.bind(this,this.sendAlert));
+      }
+    },
     download:function (option) {
       if (this.get('content.readAccess')) {
         this.store.linkFor([this.get('content')],option).then(function (link) {
@@ -28,6 +35,9 @@ App.FileController = Ember.ObjectController.extend({
         },Em.run.bind(this,this.sendAlert));
       }
     },
+    preview:function (option) {
+      this.send('showPreviewModal',this.get('content'));
+    },
     showChmod:function () {
       this.send('showChmodModal',this.get('content'));
     },
@@ -55,7 +65,8 @@ App.FileController = Ember.ObjectController.extend({
       if (this.get('content.isDirectory')) {
         return this.transitionToRoute('files',{queryParams: {path: this.get('content.id')}});
       } else{
-        return this.send('download');
+        //return this.send('download');
+        return this.send('preview');
       }
     },
     deleteFile:function (deleteForever) {

http://git-wip-us.apache.org/repos/asf/ambari/blob/03a7b9d0/contrib/views/files/src/main/resources/ui/app/controllers/files.js
----------------------------------------------------------------------
diff --git a/contrib/views/files/src/main/resources/ui/app/controllers/files.js b/contrib/views/files/src/main/resources/ui/app/controllers/files.js
index 22cbb7a..7fb55bd 100644
--- a/contrib/views/files/src/main/resources/ui/app/controllers/files.js
+++ b/contrib/views/files/src/main/resources/ui/app/controllers/files.js
@@ -87,10 +87,12 @@ App.FilesController = Ember.ArrayController.extend({
     },
     download:function (option) {
       var files = this.get('selectedFiles').filterBy('readAccess',true);
-      this.store.linkFor(files,option).then(function (link) {
+      var content = this.get('content');
+      this.store.linkFor(content, option).then(function (link) {
         window.location.href = link;
       });
     },
+
     mkdir:function (newDirName) {
       this.store.mkdir(newDirName)
         .then(bind(this,this.mkdirSuccessCalback),bind(this,this.throwAlert));
@@ -118,6 +120,12 @@ App.FilesController = Ember.ArrayController.extend({
         .chmod(file)
         .then(null,Em.run.bind(this,this.chmodErrorCallback,file));
     },
+    confirmPreview:function (file) {
+      //this.send('download');
+      this.store.linkFor(file, "browse").then(function (link) {
+        window.location.href = link;
+      });
+    },
     clearSearchField:function () {
       this.set('searchString','');
     }

http://git-wip-us.apache.org/repos/asf/ambari/blob/03a7b9d0/contrib/views/files/src/main/resources/ui/app/controllers/previewModal.js
----------------------------------------------------------------------
diff --git a/contrib/views/files/src/main/resources/ui/app/controllers/previewModal.js b/contrib/views/files/src/main/resources/ui/app/controllers/previewModal.js
new file mode 100644
index 0000000..a2d0c9e
--- /dev/null
+++ b/contrib/views/files/src/main/resources/ui/app/controllers/previewModal.js
@@ -0,0 +1,87 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.
+ */
+
+var App = require('app');
+
+App.PreviewModalController = Em.ObjectController.extend({
+    needs:['files'],
+    offset: 3000 ,
+    startIndex:0,
+    file:Em.computed.alias('content'),
+    filePageText:'',
+    pagecontent: Ember.computed('file','startIndex', 'endIndex', function() {
+        var file = this.get('file');
+        var filepath = file.get('path');
+        var filePageText = this.get('filePageText');
+
+        var self = this,
+            defer = Ember.RSVP.defer(),
+            startIndex = this.get('startIndex'),
+            endIndex  = this.get('endIndex');
+
+        var pathName = window.location.pathname;
+        var pathNameArray = pathName.split("/");
+        var ViewVersion = pathNameArray[3];
+        var viewName = pathNameArray[4];
+        var previewServiceURL = "/api/v1/views/FILES/versions/"+ ViewVersion + "/instances/"
+ viewName + "/resources/files/preview/file" + '?path=' + filepath + '&start='+ startIndex
+'&end='+ endIndex;
+
+        var previousText = $('.preview-content').text();
+
+        $.ajax({
+            url: previewServiceURL,
+            dataType: "json",
+            type: 'get',
+            async: false,
+            contentType: 'application/json',
+            success: function( response, textStatus, jQxhr ){
+                self.set('filePageText', previousText + response.data);
+                self.set('isFileEnd',response.isFileEnd);
+            },
+            error: function( jqXhr, textStatus, errorThrown ){
+                console.log( "Preview Fail pagecontent : " + errorThrown );
+            }
+        });
+
+        if(self.get('isFileEnd') == true){
+           this.set('showNext', false);
+        }
+        return self.get('filePageText');
+    }),
+    endIndex: Ember.computed('startIndex', 'offset', function() {
+        var startIndex = this.get('startIndex'),
+            offset  = this.get('offset');
+        return startIndex + offset;
+    }),
+    showPrev : Ember.computed('startIndex', function() {
+        var startIndex = this.get('startIndex');
+        this.set('showNext', true);
+        return ((startIndex == 0) ? false : true );
+    }),
+    showNext : true,
+    actions:{
+        next: function(){
+            console.log('Next');
+            this.set('startIndex', this.get('startIndex') + this.get('offset'));
+            return self.get('filePageText');
+        },
+        prev: function(){
+            console.log('Prev');
+            this.set('startIndex', (this.get('startIndex') - this.get('offset')) > 0 ?
(this.get('startIndex') - this.get('offset')) : 0);
+        }
+    }
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/03a7b9d0/contrib/views/files/src/main/resources/ui/app/initialize.js
----------------------------------------------------------------------
diff --git a/contrib/views/files/src/main/resources/ui/app/initialize.js b/contrib/views/files/src/main/resources/ui/app/initialize.js
index fc6cbc7..7790397 100644
--- a/contrib/views/files/src/main/resources/ui/app/initialize.js
+++ b/contrib/views/files/src/main/resources/ui/app/initialize.js
@@ -31,6 +31,7 @@ require('templates/index');
 require('templates/files');
 require('templates/error');
 require('templates/modal/chmod');
+require('templates/modal/preview');
 require('templates/util/errorRow');
 require('templates/util/fileRow');
 
@@ -56,6 +57,7 @@ require('controllers/file');
 require('controllers/error');
 require('controllers/filesAlert');
 require('controllers/chmodModal');
+require('controllers/previewModal');
 
 /////////////////////////////////
 // Components
@@ -81,6 +83,7 @@ require('views/file');
 require('views/files');
 require('views/filesAlert');
 require('views/modalChmod');
+require('views/modalPreview');
 
 /////////////////////////////////
 // Routes

http://git-wip-us.apache.org/repos/asf/ambari/blob/03a7b9d0/contrib/views/files/src/main/resources/ui/app/routes/file.js
----------------------------------------------------------------------
diff --git a/contrib/views/files/src/main/resources/ui/app/routes/file.js b/contrib/views/files/src/main/resources/ui/app/routes/file.js
index ee6a45a..207b57a 100644
--- a/contrib/views/files/src/main/resources/ui/app/routes/file.js
+++ b/contrib/views/files/src/main/resources/ui/app/routes/file.js
@@ -48,13 +48,16 @@ App.FilesRoute = Em.Route.extend({
     },
     willTransition:function (argument) {
       var hasModal = this.router._lookupActiveView('modal.chmod'),
-          hasAlert = this.router._lookupActiveView('files.alert');
+          hasAlert = this.router._lookupActiveView('files.alert'),
+          hasPreviewModal = this.router._lookupActiveView('modal.preview');
 
       Em.run.next(function(){
         if (hasAlert) this.send('removeAlert');
         if (hasModal) this.send('removeChmodModal');
+        if (hasPreviewModal) this.send('removePreviewModal');
       }.bind(this));
     },
+
     showChmodModal:function (content) {
       this.controllerFor('chmodModal').set('content',content);
       this.render('modal.chmod',{
@@ -63,12 +66,30 @@ App.FilesRoute = Em.Route.extend({
         controller:'chmodModal'
       });
     },
+
+    showPreviewModal :function (content) {
+      this.controllerFor('previewModal').set('content',content);
+      this.controllerFor('previewModal').set('startIndex',0);
+
+      this.render('modal.preview',{
+        into:'files',
+        outlet:'modal',
+        controller:'previewModal'
+      });
+    },
+
     removeChmodModal:function () {
       this.disconnectOutlet({
         outlet: 'modal',
         parentView: 'files'
       });
     },
+    removePreviewModal:function () {
+      this.disconnectOutlet({
+        outlet: 'modal',
+        parentView: 'files'
+      });
+    },
     showAlert:function (error) {
       this.controllerFor('filesAlert').set('content',error);
       this.render('files.alert',{

http://git-wip-us.apache.org/repos/asf/ambari/blob/03a7b9d0/contrib/views/files/src/main/resources/ui/app/templates/modal/preview.hbs
----------------------------------------------------------------------
diff --git a/contrib/views/files/src/main/resources/ui/app/templates/modal/preview.hbs b/contrib/views/files/src/main/resources/ui/app/templates/modal/preview.hbs
new file mode 100644
index 0000000..d619bd9
--- /dev/null
+++ b/contrib/views/files/src/main/resources/ui/app/templates/modal/preview.hbs
@@ -0,0 +1,33 @@
+{{!
+   Licensed to the Apache Software Foundation (ASF) under one
+   or more contributor license agreements.  See the NOTICE file
+   distributed with this work for additional information
+   regarding copyright ownership.  The ASF 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.
+}}
+<div class="modal preview" tabindex="-1" role="dialog" aria-hidden="true" data-backdrop="static">
+    <div class="modal-dialog modal-lg">
+        <div class="modal-content">
+            <div class="modal-header">
+                <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span
class="sr-only">Close</span></button>
+                <h4 class="modal-title">File Preview</h4>
+                {{ file.path }}
+            </div>
+            <pre class="modal-body preview-content" style="white-space:pre;margin: 10px;
padding: 10px;overflow-y: auto; height: 350px">{{ pagecontent }}</pre>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-default" {{action 'close' target="view"}}>Close</button>
+                <button type="button" class="btn btn-primary" {{action 'confirm' target="view"}}>Download
File</button>
+            </div>
+        </div>
+    </div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/03a7b9d0/contrib/views/files/src/main/resources/ui/app/views/modalPreview.js
----------------------------------------------------------------------
diff --git a/contrib/views/files/src/main/resources/ui/app/views/modalPreview.js b/contrib/views/files/src/main/resources/ui/app/views/modalPreview.js
new file mode 100644
index 0000000..927e267
--- /dev/null
+++ b/contrib/views/files/src/main/resources/ui/app/views/modalPreview.js
@@ -0,0 +1,51 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.
+ */
+
+var App = require('app');
+
+App.ModalPreviewView = Em.View.extend({
+  actions:{
+    confirm:function (file) {
+      this.get('controller.controllers.files').send('confirmPreview', this.get('controller.file'));
+      this.$('.preview').modal('hide');
+    },
+    close:function () {
+      this.$('.preview').modal('hide');
+    }
+  },
+  didInsertElement:function (argument) {
+    var self = this;
+
+    this.$('.preview').modal();
+
+    this.$('.preview').on('hidden.bs.modal',function  () {
+      this.get('controller.controllers.files').send('removePreviewModal');
+    }.bind(this));
+
+    this.$('.preview-content').on('scroll', function() {
+      if($(this).scrollTop() + $(this).innerHeight() >= this.scrollHeight) {
+        self.get('controller').send('next');
+      }
+    });
+
+  },
+  willClearRender:function  () {
+    this.$('.preview').off('hidden.bs.modal');
+    this.$('.preview').modal('hide');
+  }
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/03a7b9d0/contrib/views/files/src/main/resources/ui/bower.json
----------------------------------------------------------------------
diff --git a/contrib/views/files/src/main/resources/ui/bower.json b/contrib/views/files/src/main/resources/ui/bower.json
index b7cb52a..c0a4049 100644
--- a/contrib/views/files/src/main/resources/ui/bower.json
+++ b/contrib/views/files/src/main/resources/ui/bower.json
@@ -12,7 +12,8 @@
     "moment": "~2.5.1",
     "ember-i18n": "~1.6.0",
     "bootstrap-contextmenu": "~0.2.0",
-    "font-awesome": "~4.0.3"
+    "font-awesome": "~4.0.3",
+    "ivy-codemirror": "~1.0.0"
   },
   "overrides": {
     "ember-uploader": {

http://git-wip-us.apache.org/repos/asf/ambari/blob/03a7b9d0/contrib/views/files/src/main/resources/ui/package.json
----------------------------------------------------------------------
diff --git a/contrib/views/files/src/main/resources/ui/package.json b/contrib/views/files/src/main/resources/ui/package.json
index 1fab7e8..f172af8 100644
--- a/contrib/views/files/src/main/resources/ui/package.json
+++ b/contrib/views/files/src/main/resources/ui/package.json
@@ -30,7 +30,8 @@
     "phantomjs": "^1.9.2",
     "karma": "*",
     "karma-qunit": "*",
-    "karma-phantomjs-launcher": "~0.1.2"
+    "karma-phantomjs-launcher": "~0.1.2",
+    "ivy-codemirror": "^1.2.0"
   },
   "ignore": [
     "**/.*",


Mime
View raw message