FEATURE: Attach backup log as upload (#13849)

FEATURE: Attach backup log as upload (#13849)

Discourse automatically sends a private message after backup or restore finished. The private message used to contain the log inline even when it was very long. A very long log can create issues because the length of the post will be over the maximum allowed length of a post. When that happens, Discourse will try to create an upload with the logs. If that fails, it will trim the log and inline it.

diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index eb6cb5a..2d4f846 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -2984,9 +2984,7 @@ en:
 
         Here's the log:
 
-        `‍`` text
         %{logs}
-        `‍``
 
     backup_failed:
       title: "Backup Failed"
@@ -2996,9 +2994,7 @@ en:
 
         Here's the log:
 
-        `‍`` text
         %{logs}
-        `‍``
 
     restore_succeeded:
       title: "Restore Succeeded"
@@ -3008,9 +3004,7 @@ en:
 
         Here's the log:
 
-        `‍`` text
         %{logs}
-        `‍``
 
     restore_failed:
       title: "Restore Failed"
@@ -3020,9 +3014,7 @@ en:
 
         Here's the log:
 
-        `‍`` text
         %{logs}
-        `‍``
 
     bulk_invite_succeeded:
       title: "Bulk Invite Succeeded"
diff --git a/lib/backup_restore/backuper.rb b/lib/backup_restore/backuper.rb
index dea11cd..54c69ae 100644
--- a/lib/backup_restore/backuper.rb
+++ b/lib/backup_restore/backuper.rb
@@ -321,9 +321,8 @@ module BackupRestore
       log "Notifying '#{@user.username}' of the end of the backup..."
       status = @success ? :backup_succeeded : :backup_failed
 
-      post = SystemMessage.create_from_system_user(
-        @user, status, logs: Discourse::Utils.pretty_logs(@logs)
-      )
+      logs = Discourse::Utils.logs_markdown(@logs, user: @user)
+      post = SystemMessage.create_from_system_user(@user, status, logs: logs)
 
       if @user.id == Discourse::SYSTEM_USER_ID
         post.topic.invite_group(@user, Group[:admins])
diff --git a/lib/backup_restore/restorer.rb b/lib/backup_restore/restorer.rb
index 05a8233..00bedab 100644
--- a/lib/backup_restore/restorer.rb
+++ b/lib/backup_restore/restorer.rb
@@ -148,10 +148,8 @@ module BackupRestore
         log "Notifying '#{user.username}' of the end of the restore..."
         status = @success ? :restore_succeeded : :restore_failed
 
-        SystemMessage.create_from_system_user(
-          user, status,
-          logs: Discourse::Utils.pretty_logs(@logger.logs)
-        )
+        logs = Discourse::Utils.logs_markdown(@logger.logs, user: user)
+        post = SystemMessage.create_from_system_user(user, status, logs: logs)
       else
         log "Could not send notification to '#{@user_info[:username]}' " \
           "(#{@user_info[:email]}), because the user does not exist."
diff --git a/lib/discourse.rb b/lib/discourse.rb
index 1d20365..c76f4c7 100644
--- a/lib/discourse.rb
+++ b/lib/discourse.rb
@@ -42,6 +42,49 @@ module Discourse
       logs.join("\n")
     end
 
+    def self.logs_markdown(logs, user:, filename: 'log.txt')
+      # Reserve 250 characters for the rest of the text
+      max_logs_length = SiteSetting.max_post_length - 250
+      pretty_logs = Discourse::Utils.pretty_logs(logs)
+
+      # If logs are short, try to inline them
+      if pretty_logs.size < max_logs_length
+        return <<~TEXT
+        `‍``text
+        #{pretty_logs}
+        `‍``
+        TEXT
+      end
+
+      # Try to create an upload for the logs
+      upload = Dir.mktmpdir do |dir|
+        File.write(File.join(dir, filename), pretty_logs)
+        zipfile = Compression::Zip.new.compress(dir, filename)
+        File.open(zipfile) do |file|
+          UploadCreator.new(
+            file,
+            File.basename(zipfile),
+            type: 'backup_logs',
+            for_export: 'true'
+          ).create_for(user.id)
+        end
+      end
+
+      if upload.persisted?
+        return UploadMarkdown.new(upload).attachment_markdown
+      else
+        Rails.logger.warn("Failed to upload the backup logs file: #{upload.errors.full_messages}")
+      end
+
+      # If logs are long and upload cannot be created, show trimmed logs
+      <<~TEXT
+      `‍``text
+      ...
+      #{pretty_logs.last(max_logs_length)}
+      `‍``
+      TEXT
+    end
+
     def self.atomic_write_file(destination, contents)
       begin
         return if File.read(destination) == contents
diff --git a/spec/lib/backup_restore/backuper_spec.rb b/spec/lib/backup_restore/backuper_spec.rb
index 389b8d7..7cbe142 100644
--- a/spec/lib/backup_restore/backuper_spec.rb
+++ b/spec/lib/backup_restore/backuper_spec.rb
@@ -16,4 +16,59 @@ describe BackupRestore::Backuper do
 
     expect(backuper.send(:get_parameterized_title)).to eq("coding-horror")
   end
+
+  describe '#notify_user' do
+    before do
+      freeze_time Time.zone.parse('2010-01-01 12:00')
+    end
+
+    it 'includes logs if short' do
+      SiteSetting.max_export_file_size_kb = 1
+      SiteSetting.export_authorized_extensions = "tar.gz"
+
+      silence_stdout do
+        backuper = BackupRestore::Backuper.new(Discourse.system_user.id)
+
+        expect { backuper.send(:notify_user) }
+          .to change { Topic.private_messages.count }.by(1)
+          .and change { Upload.count }.by(0)
+      end
+
+      expect(Topic.last.first_post.raw).to include("`‍``text\n[2010-01-01 12:00:00] Notifying 'system' of the end of the backup...\n`‍``")
+    end
+
+    it 'include upload if log is long' do
+      SiteSetting.max_post_length = 250
+
+      silence_stdout do
+        backuper = silence_stdout { BackupRestore::Backuper.new(Discourse.system_user.id) }
+
+        expect { backuper.send(:notify_user) }
+          .to change { Topic.private_messages.count }.by(1)
+          .and change { Upload.where(original_filename: "log.txt.zip").count }.by(1)
+      end
+
+      expect(Topic.last.first_post.raw).to include("[log.txt.zip|attachment]")
+    end
+
+    it 'includes trimmed logs if log is long and upload cannot be saved' do
+      SiteSetting.max_post_length = 348
+      SiteSetting.max_export_file_size_kb = 1
+      SiteSetting.export_authorized_extensions = "tar.gz"
+
+      silence_stdout do
+        backuper = BackupRestore::Backuper.new(Discourse.system_user.id)
+
+        1.upto(10).each do |i|
+          backuper.send(:log, "Line #{i}")
+        end
+
+        expect { backuper.send(:notify_user) }
+          .to change { Topic.private_messages.count }.by(1)
+          .and change { Upload.count }.by(0)
+      end
+
+      expect(Topic.last.first_post.raw).to include("`‍``text\n...\n[2010-01-01 12:00:00] Line 10\n[2010-01-01 12:00:00] Notifying 'system' of the end of the backup...\n`‍``")
+    end
+  end
 end

GitHub sha: e2c415457c6527680129475c07458401a54c08e0

This commit appears in #13849 which was approved by tgxworld. It was merged by nbianca.