FEATURE: Gz to zip for exports (#7889)

FEATURE: Gz to zip for exports (#7889)

diff --git a/Gemfile b/Gemfile
index 6a621c4..ffa12df 100644
--- a/Gemfile
+++ b/Gemfile
@@ -203,6 +203,8 @@ gem "sassc-rails"
 gem 'rotp'
 gem 'rqrcode'
 
+gem 'rubyzip', require: false
+
 gem 'sshkey', require: false
 
 gem 'rchardet', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 898e8b7..194ec34 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -351,6 +351,7 @@ GEM
       guess_html_encoding (>= 0.0.4)
       nokogiri (>= 1.6.0)
     ruby_dep (1.5.0)
+    rubyzip (1.2.3)
     safe_yaml (1.0.5)
     sanitize (5.0.0)
       crass (~> 1.0.2)
@@ -516,6 +517,7 @@ DEPENDENCIES
   rubocop
   ruby-prof
   ruby-readability
+  rubyzip
   sanitize
   sassc
   sassc-rails
diff --git a/app/assets/javascripts/admin/templates/modal/admin-install-theme.hbs b/app/assets/javascripts/admin/templates/modal/admin-install-theme.hbs
index 153dbc9..a41bfa9 100644
--- a/app/assets/javascripts/admin/templates/modal/admin-install-theme.hbs
+++ b/app/assets/javascripts/admin/templates/modal/admin-install-theme.hbs
@@ -44,7 +44,7 @@
 
     {{#if local}}
       <div class="inputs">
-        <input onchange={{action "uploadLocaleFile"}} type="file" id="file-input" accept='.dcstyle.json,application/json,.tar.gz,application/x-gzip'><br>
+        <input onchange={{action "uploadLocaleFile"}} type="file" id="file-input" accept='.dcstyle.json,application/json,.tar.gz,application/x-gzip,.zip,application/zip'><br>
         <span class="description">{{i18n 'admin.customize.theme.import_file_tip'}}</span>
       </div>
     {{/if}}
diff --git a/app/controllers/admin/themes_controller.rb b/app/controllers/admin/themes_controller.rb
index 5389269..4def5bd 100644
--- a/app/controllers/admin/themes_controller.rb
+++ b/app/controllers/admin/themes_controller.rb
@@ -88,7 +88,7 @@ class Admin::ThemesController < Admin::AdminController
       rescue RemoteTheme::ImportError => e
         render_json_error e.message
       end
-    elsif params[:bundle] || (params[:theme] && ["application/x-gzip", "application/gzip"].include?(params[:theme].content_type))
+    elsif params[:bundle] || (params[:theme] && ["application/x-gzip", "application/gzip", "application/zip"].include?(params[:theme].content_type))
       # params[:bundle] used by theme CLI. params[:theme] used by admin UI
       bundle = params[:bundle] || params[:theme]
       theme_id = params[:theme_id]
@@ -252,6 +252,7 @@ class Admin::ThemesController < Admin::AdminController
 
     exporter = ThemeStore::TgzExporter.new(@theme)
     file_path = exporter.package_filename
+
     headers['Content-Length'] = File.size(file_path).to_s
     send_data File.read(file_path),
       filename: File.basename(file_path),
diff --git a/app/jobs/regular/export_csv_file.rb b/app/jobs/regular/export_csv_file.rb
index e1b5739..d8d3a72 100644
--- a/app/jobs/regular/export_csv_file.rb
+++ b/app/jobs/regular/export_csv_file.rb
@@ -1,6 +1,7 @@
 # frozen_string_literal: true
 
 require 'csv'
+require 'zip'
 require_dependency 'system_message'
 require_dependency 'upload_creator'
 
@@ -53,18 +54,19 @@ module Jobs
       # ensure directory exists
       FileUtils.mkdir_p(UserExport.base_directory) unless Dir.exists?(UserExport.base_directory)
 
-      # write to CSV file
-      CSV.open(absolute_path, "w") do |csv|
+      # Generate a compressed CSV file
+      csv_to_export = CSV.generate do |csv|
         csv << get_header if @entity != "report"
         public_send(export_method).each { |d| csv << d }
       end
 
-      # compress CSV file
-      system('gzip', '-5', absolute_path)
+      compressed_file_path = "#{absolute_path}.zip"
+      Zip::File.open(compressed_file_path, Zip::File::CREATE) do |zipfile|
+        zipfile.get_output_stream(file_name) { |f| f.puts csv_to_export }
+      end
 
       # create upload
       upload = nil
-      compressed_file_path = "#{absolute_path}.gz"
 
       if File.exist?(compressed_file_path)
         File.open(compressed_file_path) do |file|
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 4e01d15..47f8dac 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -3517,7 +3517,7 @@ en:
           delete_upload_confirm: "Delete this upload? (Theme CSS may stop working!)"
           import_web_tip: "Repository containing theme"
           import_web_advanced: "Advanced..."
-          import_file_tip: ".tar.gz or .dcstyle.json file containing theme"
+          import_file_tip: ".tar.gz, .zip, or .dcstyle.json file containing theme"
           is_private: "Theme is in a private git repository"
           remote_branch: "Branch name (optional)"
           public_key: "Grant the following public key access to the repo:"
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 0a7a0af..6fa93ed 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -2671,7 +2671,7 @@ en:
 
         The above download link will be valid for 48 hours.
 
-        The data is compressed as a gzip archive. If the archive does not extract itself when you open it, use the tools recommended here: https://www.gzip.org/#faq4
+        The data is compressed as a zip archive. If the archive does not extract itself when you open it, use the tool recommended here: https://www.7-zip.org/
 
     csv_export_failed:
       title: "CSV Export Failed"
diff --git a/config/site_settings.yml b/config/site_settings.yml
index ebe51f6..3b5736c 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -1057,7 +1057,7 @@ files:
     list_type: compact
   export_authorized_extensions:
     hidden: true
-    default: "gz"
+    default: "zip"
     type: list
     list_type: compact
   responsive_post_image_sizes:
diff --git a/lib/import_export/zip_utils.rb b/lib/import_export/zip_utils.rb
new file mode 100644
index 0000000..dc06e0e
--- /dev/null
+++ b/lib/import_export/zip_utils.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'zip'
+
+class ZipUtils
+  def zip_directory(path, export_name)
+    zip_filename = "#{export_name}.zip"
+    absolute_path = "#{path}/#{export_name}"
+    entries = Dir.entries(absolute_path) - %w[. ..]
+
+    Zip::File.open(zip_filename, Zip::File::CREATE) do |zipfile|
+      write_entries(entries, absolute_path, '', zipfile)
+    end
+
+    "#{absolute_path}.zip"
+  end
+
+  def unzip_directory(path, zip_filename, allow_non_root_folder: false)
+    Zip::File.open(zip_filename) do |zip_file|
+      root = root_folder_present?(zip_file, allow_non_root_folder) ? '' : 'unzipped/'
+      zip_file.each do |entry|
+        entry_path = File.join(path, "#{root}#{entry.name}")
+        FileUtils.mkdir_p(File.dirname(entry_path))
+        entry.extract(entry_path)
+      end
+    end
+  end
+
+  private
+
+  def root_folder_present?(filenames, allow_non_root_folder)
+    filenames.map { |p| p.name.split('/').first }.uniq!.size == 1 || allow_non_root_folder
+  end
+
+  # A helper method to make the recursion work.
+  def write_entries(entries, base_path, path, zipfile)
+    entries.each do |e|
+      zipfile_path = path == '' ? e : File.join(path, e)
+      disk_file_path = File.join(base_path, zipfile_path)
+
+      if File.directory? disk_file_path
+        recursively_deflate_directory(disk_file_path, zipfile, base_path, zipfile_path)
+      else
+        put_into_archive(disk_file_path, zipfile, zipfile_path)
+      end
+    end
+  end
+
+  def recursively_deflate_directory(disk_file_path, zipfile, base_path, zipfile_path)
+    zipfile.mkdir zipfile_path
+    subdir = Dir.entries(disk_file_path) - %w[. ..]
+    write_entries subdir, base_path, zipfile_path, zipfile
+  end
+
+  def put_into_archive(disk_file_path, zipfile, zipfile_path)
+    zipfile.add(zipfile_path, disk_file_path)
+  end
+end
diff --git a/lib/theme_store/tgz_exporter.rb b/lib/theme_store/tgz_exporter.rb
index 824a874..75f5bff 100644
--- a/lib/theme_store/tgz_exporter.rb
+++ b/lib/theme_store/tgz_exporter.rb
@@ -56,14 +56,10 @@ class ThemeStore::TgzExporter
   end
 
   private

[... diff too long, it was truncated ...]

GitHub sha: f5c707c9

This should be renamed to something generic, like ArchiveImporter. It now does zip as well as Tgz

1 Like

ThemeStore::TgzExporter should be renamed to ZipExporter, to avoid confusion

Found a few small issues when working on FEATURE: Support downloading themes in zip format · discourse/discourse_theme@de0940b · GitHub. I’ve commented two above, and also the content-type of the download should be changed from application/x-gzip to application/zip

2 Likes