2021-05-24 10:38:42 +08:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
require "yaml"
|
|
|
|
require "zip"
|
|
|
|
|
|
|
|
class ZippedSiteStructure
|
|
|
|
attr_reader :zip
|
|
|
|
|
|
|
|
def initialize(path, create: false)
|
|
|
|
@zip = Zip::File.open(path, create)
|
2021-08-18 20:57:31 +08:00
|
|
|
@uploads = {}
|
2021-05-24 10:38:42 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def close
|
|
|
|
@zip.close
|
|
|
|
end
|
|
|
|
|
|
|
|
def set(name, data)
|
|
|
|
@zip.get_output_stream("#{name}.json") { |file| file.write(data.to_json) }
|
|
|
|
end
|
|
|
|
|
|
|
|
def get(name)
|
|
|
|
data = @zip.get_input_stream("#{name}.json").read
|
|
|
|
JSON.parse(data)
|
|
|
|
end
|
|
|
|
|
|
|
|
def set_upload(upload_or_id_or_url)
|
|
|
|
return nil if upload_or_id_or_url.blank?
|
|
|
|
|
|
|
|
if Integer === upload_or_id_or_url
|
|
|
|
upload = Upload.find_by(id: upload_or_id_or_url)
|
|
|
|
elsif String === upload_or_id_or_url
|
|
|
|
upload = Upload.get_from_url(upload_or_id_or_url)
|
|
|
|
elsif Upload === upload_or_id_or_url
|
|
|
|
upload = upload_or_id_or_url
|
|
|
|
end
|
|
|
|
|
|
|
|
if !upload
|
|
|
|
STDERR.puts "ERROR: Could not find upload #{upload_or_id_or_url.inspect}"
|
|
|
|
return nil
|
|
|
|
end
|
|
|
|
|
2021-08-18 20:57:31 +08:00
|
|
|
if @uploads[upload.id].present?
|
|
|
|
puts " - Already exported upload #{upload_or_id_or_url} to #{@uploads[upload.id][:path]}"
|
|
|
|
return @uploads[upload.id]
|
|
|
|
end
|
|
|
|
|
2021-05-24 10:38:42 +08:00
|
|
|
local_path =
|
|
|
|
upload.local? ? Discourse.store.path_for(upload) : Discourse.store.download(upload).path
|
|
|
|
zip_path = File.join("uploads", File.basename(local_path))
|
2021-08-18 20:57:31 +08:00
|
|
|
zip_path = get_unique_path(zip_path)
|
2021-05-24 10:38:42 +08:00
|
|
|
|
|
|
|
puts " - Exporting upload #{upload_or_id_or_url} to #{zip_path}"
|
|
|
|
@zip.add(zip_path, local_path)
|
|
|
|
|
2021-08-18 20:57:31 +08:00
|
|
|
@uploads[upload.id] ||= { filename: upload.original_filename, path: zip_path }
|
2021-05-24 10:38:42 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def get_upload(upload, opts = {})
|
|
|
|
return nil if upload.blank?
|
|
|
|
|
2021-08-18 20:57:31 +08:00
|
|
|
if @uploads[upload["path"]].present?
|
|
|
|
puts " - Already imported upload #{upload["filename"]} from #{upload["path"]}"
|
|
|
|
return @uploads[upload["path"]]
|
|
|
|
end
|
|
|
|
|
2021-05-24 10:38:42 +08:00
|
|
|
puts " - Importing upload #{upload["filename"]} from #{upload["path"]}"
|
|
|
|
|
|
|
|
tempfile = Tempfile.new(upload["filename"], binmode: true)
|
|
|
|
tempfile.write(@zip.get_input_stream(upload["path"]).read)
|
|
|
|
tempfile.rewind
|
|
|
|
|
2021-08-18 20:57:31 +08:00
|
|
|
@uploads[upload["path"]] ||= UploadCreator.new(tempfile, upload["filename"], opts).create_for(
|
|
|
|
Discourse::SYSTEM_USER_ID,
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def get_unique_path(path)
|
|
|
|
return path if @zip.find_entry(path).blank?
|
|
|
|
|
|
|
|
extname = File.extname(path)
|
|
|
|
basename = File.basename(path, extname)
|
|
|
|
dirname = File.dirname(path)
|
|
|
|
|
|
|
|
i = 0
|
|
|
|
loop do
|
|
|
|
i += 1
|
|
|
|
path = File.join(dirname, "#{basename}_#{i}#{extname}")
|
|
|
|
return path if @zip.find_entry(path).blank?
|
|
|
|
end
|
2021-05-24 10:38:42 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
desc "Exports site structure (settings, groups, categories, tags, themes, etc) to a ZIP file"
|
|
|
|
task "site:export_structure", [:zip_path] => :environment do |task, args|
|
|
|
|
if args[:zip_path].blank?
|
|
|
|
STDERR.puts "ERROR: rake site:export_structure[<path to ZIP file>]"
|
|
|
|
exit 1
|
2022-01-06 01:45:08 +08:00
|
|
|
elsif File.exist?(args[:zip_path])
|
2021-05-24 10:38:42 +08:00
|
|
|
STDERR.puts "ERROR: File '#{args[:zip_path]}' already exists"
|
|
|
|
exit 2
|
|
|
|
end
|
|
|
|
|
|
|
|
data = ZippedSiteStructure.new(args[:zip_path], create: true)
|
|
|
|
|
|
|
|
puts
|
|
|
|
puts "Exporting site settings"
|
|
|
|
puts
|
|
|
|
|
|
|
|
settings = {}
|
|
|
|
|
|
|
|
SiteSetting
|
|
|
|
.all_settings(include_hidden: true)
|
|
|
|
.each do |site_setting|
|
|
|
|
next if site_setting[:default] == site_setting[:value]
|
|
|
|
|
|
|
|
puts "- Site setting #{site_setting[:setting]} -> #{site_setting[:value].inspect}"
|
|
|
|
|
|
|
|
settings[site_setting[:setting]] = if site_setting[:type] == "upload"
|
|
|
|
data.set_upload(site_setting[:value])
|
|
|
|
else
|
|
|
|
site_setting[:value]
|
2023-01-09 20:10:19 +08:00
|
|
|
end
|
2021-05-24 10:38:42 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
data.set("site_settings", settings)
|
|
|
|
|
|
|
|
puts
|
|
|
|
puts "Exporting users"
|
|
|
|
puts
|
|
|
|
|
|
|
|
users = []
|
|
|
|
|
|
|
|
User
|
|
|
|
.real
|
|
|
|
.where(admin: true)
|
|
|
|
.each do |u|
|
|
|
|
puts "- User #{u.username}"
|
2023-01-09 20:10:19 +08:00
|
|
|
|
2021-05-24 10:38:42 +08:00
|
|
|
users << {
|
|
|
|
username: u.username,
|
|
|
|
name: u.name,
|
|
|
|
email: u.email,
|
|
|
|
active: u.active,
|
|
|
|
admin: u.admin,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
data.set("users", users)
|
|
|
|
|
|
|
|
puts
|
|
|
|
puts "Exporting groups"
|
|
|
|
puts
|
|
|
|
|
|
|
|
groups = []
|
|
|
|
|
|
|
|
Group
|
|
|
|
.where(automatic: false)
|
|
|
|
.each do |g|
|
|
|
|
puts "- Group #{g.name}"
|
2023-01-09 20:10:19 +08:00
|
|
|
|
2021-05-24 10:38:42 +08:00
|
|
|
groups << {
|
|
|
|
name: g.name,
|
|
|
|
automatic_membership_email_domains: g.automatic_membership_email_domains,
|
|
|
|
primary_group: g.primary_group,
|
|
|
|
title: g.title,
|
|
|
|
grant_trust_level: g.grant_trust_level,
|
|
|
|
incoming_email: g.incoming_email,
|
|
|
|
has_messages: g.has_messages,
|
|
|
|
flair_bg_color: g.flair_bg_color,
|
|
|
|
flair_color: g.flair_color,
|
|
|
|
bio_raw: g.bio_raw,
|
|
|
|
allow_membership_requests: g.allow_membership_requests,
|
|
|
|
full_name: g.full_name,
|
|
|
|
default_notification_level: g.default_notification_level,
|
|
|
|
visibility_level: g.visibility_level,
|
|
|
|
public_exit: g.public_exit,
|
|
|
|
public_admission: g.public_admission,
|
|
|
|
membership_request_template: g.membership_request_template,
|
|
|
|
messageable_level: g.messageable_level,
|
|
|
|
mentionable_level: g.mentionable_level,
|
|
|
|
publish_read_state: g.publish_read_state,
|
|
|
|
members_visibility_level: g.members_visibility_level,
|
|
|
|
flair_icon: g.flair_icon,
|
|
|
|
flair_upload_id: data.set_upload(g.flair_upload_id),
|
|
|
|
allow_unknown_sender_topic_replies: g.allow_unknown_sender_topic_replies,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
data.set("groups", groups)
|
|
|
|
|
|
|
|
puts
|
|
|
|
puts "Exporting categories"
|
|
|
|
puts
|
|
|
|
|
|
|
|
categories = []
|
|
|
|
|
|
|
|
Category.find_each do |c|
|
|
|
|
puts "- Category #{c.name} (#{c.slug})"
|
|
|
|
|
|
|
|
categories << {
|
|
|
|
name: c.name,
|
|
|
|
color: c.color,
|
|
|
|
slug: c.slug,
|
|
|
|
description: c.description,
|
|
|
|
text_color: c.text_color,
|
|
|
|
read_restricted: c.read_restricted,
|
|
|
|
auto_close_hours: c.auto_close_hours,
|
|
|
|
parent_category: c.parent_category&.slug,
|
|
|
|
position: c.position,
|
|
|
|
email_in: c.email_in,
|
|
|
|
email_in_allow_strangers: c.email_in_allow_strangers,
|
|
|
|
allow_badges: c.allow_badges,
|
|
|
|
auto_close_based_on_last_post: c.auto_close_based_on_last_post,
|
|
|
|
topic_template: c.topic_template,
|
|
|
|
sort_order: c.sort_order,
|
|
|
|
sort_ascending: c.sort_ascending,
|
|
|
|
uploaded_logo_id: data.set_upload(c.uploaded_logo_id),
|
2022-10-07 23:00:44 +08:00
|
|
|
uploaded_logo_dark_id: data.set_upload(c.uploaded_logo_dark_id),
|
2021-05-24 10:38:42 +08:00
|
|
|
uploaded_background_id: data.set_upload(c.uploaded_background_id),
|
|
|
|
topic_featured_link_allowed: c.topic_featured_link_allowed,
|
|
|
|
all_topics_wiki: c.all_topics_wiki,
|
|
|
|
show_subcategory_list: c.show_subcategory_list,
|
|
|
|
default_view: c.default_view,
|
|
|
|
subcategory_list_style: c.subcategory_list_style,
|
|
|
|
default_top_period: c.default_top_period,
|
|
|
|
mailinglist_mirror: c.mailinglist_mirror,
|
|
|
|
minimum_required_tags: c.minimum_required_tags,
|
|
|
|
navigate_to_first_post_after_read: c.navigate_to_first_post_after_read,
|
|
|
|
search_priority: c.search_priority,
|
|
|
|
allow_global_tags: c.allow_global_tags,
|
|
|
|
read_only_banner: c.read_only_banner,
|
|
|
|
default_list_filter: c.default_list_filter,
|
|
|
|
permissions: c.permissions_params,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
data.set("categories", categories)
|
|
|
|
|
|
|
|
puts
|
|
|
|
puts "Exporting tag groups"
|
|
|
|
puts
|
|
|
|
|
|
|
|
tag_groups = []
|
|
|
|
|
|
|
|
TagGroup.all.each do |tg|
|
|
|
|
puts "- Tag group #{tg.name}"
|
|
|
|
|
|
|
|
tag_groups << { name: tg.name, tag_names: tg.tags.map(&:name) }
|
|
|
|
end
|
|
|
|
|
|
|
|
data.set("tag_groups", tag_groups)
|
|
|
|
|
|
|
|
puts
|
|
|
|
puts "Exporting tags"
|
|
|
|
puts
|
|
|
|
|
|
|
|
tags = []
|
|
|
|
|
|
|
|
Tag.find_each do |t|
|
|
|
|
puts "- Tag #{t.name}"
|
|
|
|
|
|
|
|
tag = { name: t.name }
|
|
|
|
tag[:target_tag] = t.target_tag.name if t.target_tag.present?
|
|
|
|
|
|
|
|
tags << tag
|
|
|
|
end
|
|
|
|
|
|
|
|
data.set("tags", tags)
|
|
|
|
|
|
|
|
puts
|
|
|
|
puts "Exporting themes and theme components"
|
|
|
|
puts
|
|
|
|
|
|
|
|
themes = []
|
|
|
|
|
|
|
|
Theme.find_each do |theme|
|
|
|
|
puts "- Theme #{theme.name}"
|
|
|
|
|
|
|
|
if theme.remote_theme.present?
|
|
|
|
themes << {
|
|
|
|
name: theme.name,
|
|
|
|
url: theme.remote_theme.remote_url,
|
|
|
|
private_key: theme.remote_theme.private_key,
|
|
|
|
branch: theme.remote_theme.branch,
|
|
|
|
}
|
|
|
|
else
|
|
|
|
exporter = ThemeStore::ZipExporter.new(theme)
|
|
|
|
file_path = exporter.package_filename
|
|
|
|
file_zip_path = File.join("themes", File.basename(file_path))
|
|
|
|
data.zip.add(file_zip_path, file_path)
|
|
|
|
themes << { name: theme.name, filename: File.basename(file_path), path: file_zip_path }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
data.set("themes", themes)
|
|
|
|
|
|
|
|
puts
|
|
|
|
puts "Exporting theme settings"
|
|
|
|
puts
|
|
|
|
|
|
|
|
theme_settings = []
|
|
|
|
|
|
|
|
ThemeSetting.find_each do |theme_setting|
|
|
|
|
puts "- Theme setting #{theme_setting.name} -> #{theme_setting.value}"
|
|
|
|
|
|
|
|
value =
|
|
|
|
if theme_setting.data_type == ThemeSetting.types[:upload]
|
|
|
|
data.set_upload(theme_setting.value)
|
|
|
|
else
|
|
|
|
theme_setting.value
|
|
|
|
end
|
|
|
|
|
|
|
|
theme_settings << {
|
|
|
|
name: theme_setting.name,
|
|
|
|
data_type: theme_setting.data_type,
|
|
|
|
value: value,
|
|
|
|
theme: theme_setting.theme.name,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
data.set("theme_settings", theme_settings)
|
|
|
|
|
|
|
|
puts
|
|
|
|
puts "Done"
|
|
|
|
puts
|
|
|
|
|
|
|
|
data.close
|
|
|
|
end
|
|
|
|
|
|
|
|
desc "Imports site structure from a ZIP file exported by site:export_structure"
|
|
|
|
task "site:import_structure", [:zip_path] => :environment do |task, args|
|
|
|
|
if args[:zip_path].blank?
|
|
|
|
STDERR.puts "ERROR: rake site:import_structure[<path to ZIP file>]"
|
|
|
|
exit 1
|
2022-01-06 01:45:08 +08:00
|
|
|
elsif !File.exist?(args[:zip_path])
|
2021-05-24 10:38:42 +08:00
|
|
|
STDERR.puts "ERROR: File '#{args[:zip_path]}' does not exist"
|
|
|
|
exit 2
|
|
|
|
end
|
|
|
|
|
|
|
|
data = ZippedSiteStructure.new(args[:zip_path])
|
|
|
|
|
|
|
|
puts
|
|
|
|
puts "Importing site settings"
|
|
|
|
puts
|
|
|
|
|
|
|
|
settings = data.get("site_settings")
|
|
|
|
imported_settings = Set.new
|
|
|
|
|
|
|
|
3.times.each do |try|
|
|
|
|
puts "Loading site settings (try ##{try})"
|
|
|
|
|
|
|
|
settings.each do |key, value|
|
|
|
|
next if imported_settings.include?(key)
|
|
|
|
|
|
|
|
begin
|
|
|
|
if SiteSetting.type_supervisor.get_type(key) == :upload
|
|
|
|
value = data.get_upload(value, for_site_setting: true)
|
|
|
|
end
|
|
|
|
|
|
|
|
if SiteSetting.public_send(key) != value
|
|
|
|
puts "- Site setting #{key} -> #{value}"
|
|
|
|
SiteSetting.set_and_log(key, value)
|
|
|
|
end
|
|
|
|
|
|
|
|
imported_settings << key
|
|
|
|
rescue => e
|
|
|
|
next if try < 2
|
|
|
|
|
|
|
|
STDERR.puts "ERROR: Cannot set #{key} to #{value}"
|
|
|
|
puts e.backtrace
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
puts
|
|
|
|
puts "Importing users"
|
|
|
|
puts
|
|
|
|
|
|
|
|
data
|
|
|
|
.get("users")
|
|
|
|
.each do |u|
|
|
|
|
puts "- User #{u["username"]}"
|
|
|
|
|
|
|
|
begin
|
|
|
|
user = User.find_or_initialize_by(username: u.delete("username"))
|
|
|
|
user.update!(u)
|
|
|
|
rescue => e
|
|
|
|
STDERR.puts "ERROR: Cannot import user: #{e.message}"
|
|
|
|
puts e.backtrace
|
2023-01-09 20:10:19 +08:00
|
|
|
end
|
2021-05-24 10:38:42 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
puts
|
|
|
|
puts "Importing groups"
|
|
|
|
puts
|
|
|
|
|
|
|
|
data
|
|
|
|
.get("groups")
|
|
|
|
.each do |g|
|
|
|
|
puts "- Group #{g["name"]}"
|
|
|
|
|
|
|
|
begin
|
|
|
|
group = Group.find_or_initialize_by(name: g.delete("name"))
|
|
|
|
group.update!(g)
|
|
|
|
rescue => e
|
|
|
|
STDERR.puts "ERROR: Cannot import group: #{e.message}"
|
|
|
|
puts e.backtrace
|
2023-01-09 20:10:19 +08:00
|
|
|
end
|
2021-05-24 10:38:42 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
puts
|
|
|
|
puts "Importing categories"
|
|
|
|
puts
|
|
|
|
|
|
|
|
data
|
|
|
|
.get("categories")
|
|
|
|
.each do |c|
|
|
|
|
puts "- Category #{c["name"]} (#{c["slug"]})"
|
|
|
|
|
|
|
|
begin
|
|
|
|
category = Category.find_or_initialize_by(slug: c.delete("slug"))
|
|
|
|
category.user ||= Discourse.system_user
|
|
|
|
category.parent_category = Category.find_by(slug: c.delete("parent_category"))
|
|
|
|
category.permissions = c.delete("permissions")
|
|
|
|
category.update!(c)
|
|
|
|
rescue => e
|
|
|
|
STDERR.puts "ERROR: Cannot import category: #{e.message}"
|
|
|
|
puts e.backtrace
|
2023-01-09 20:10:19 +08:00
|
|
|
end
|
2021-05-24 10:38:42 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
puts
|
|
|
|
puts "Importing tag groups"
|
|
|
|
puts
|
|
|
|
|
|
|
|
data
|
|
|
|
.get("tag_groups")
|
|
|
|
.each do |tg|
|
|
|
|
puts "- Tag group #{tg["name"]}"
|
|
|
|
|
|
|
|
tag_group = TagGroup.find_or_initialize_by(name: tg.delete("name"))
|
|
|
|
tag_group.update!(tg)
|
|
|
|
end
|
|
|
|
|
|
|
|
puts
|
|
|
|
puts "Importing tags"
|
|
|
|
puts
|
|
|
|
|
|
|
|
data
|
|
|
|
.get("tags")
|
|
|
|
.each do |t|
|
|
|
|
puts "- Tag #{t["name"]}"
|
2023-01-09 20:10:19 +08:00
|
|
|
|
2021-05-24 10:38:42 +08:00
|
|
|
if t["target_tag"].present?
|
2023-01-09 20:10:19 +08:00
|
|
|
begin
|
2021-05-24 10:38:42 +08:00
|
|
|
t["target_tag"] = Tag.find_or_create_by!(name: t.delete("target_tag"))
|
|
|
|
rescue => e
|
|
|
|
STDERR.puts "ERROR: Cannot import target tag: #{e.message}"
|
|
|
|
puts e.backtrace
|
2023-01-09 20:10:19 +08:00
|
|
|
end
|
|
|
|
end
|
2021-05-24 10:38:42 +08:00
|
|
|
|
|
|
|
begin
|
|
|
|
tag = Tag.find_or_initialize_by(name: t.delete("name"))
|
|
|
|
tag.update!(t)
|
|
|
|
rescue => e
|
|
|
|
STDERR.puts "ERROR: Cannot import tag: #{e.message}"
|
|
|
|
puts e.backtrace
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
puts
|
|
|
|
puts "Importing themes and theme components"
|
|
|
|
puts
|
|
|
|
|
|
|
|
data
|
|
|
|
.get("themes")
|
|
|
|
.each do |t|
|
|
|
|
puts "- Theme #{t["name"]}"
|
2023-01-09 20:10:19 +08:00
|
|
|
|
2021-05-24 10:38:42 +08:00
|
|
|
begin
|
|
|
|
if t["url"].present?
|
|
|
|
next if Theme.find_by(name: t["name"]).present?
|
2023-01-09 20:10:19 +08:00
|
|
|
|
2021-05-24 10:38:42 +08:00
|
|
|
RemoteTheme.import_theme(
|
|
|
|
t["url"],
|
|
|
|
Discourse.system_user,
|
|
|
|
private_key: t["private_key"],
|
|
|
|
branch: t["branch"],
|
|
|
|
)
|
|
|
|
elsif t["filename"].present?
|
|
|
|
tempfile = Tempfile.new(t["filename"], binmode: true)
|
|
|
|
tempfile.write(data.zip.get_input_stream(t["path"]).read)
|
|
|
|
tempfile.flush
|
2023-01-09 20:10:19 +08:00
|
|
|
|
2021-05-24 10:38:42 +08:00
|
|
|
RemoteTheme.update_zipped_theme(
|
|
|
|
tempfile.path,
|
|
|
|
t["filename"],
|
|
|
|
user: Discourse.system_user,
|
|
|
|
theme_id: Theme.find_by(name: t["name"])&.id,
|
|
|
|
)
|
2023-01-09 20:10:19 +08:00
|
|
|
end
|
2021-05-24 10:38:42 +08:00
|
|
|
rescue => e
|
|
|
|
STDERR.puts "ERROR: Cannot import theme: #{e.message}"
|
|
|
|
puts e.backtrace
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
puts
|
|
|
|
puts "Importing theme settings"
|
|
|
|
puts
|
|
|
|
|
|
|
|
data
|
|
|
|
.get("theme_settings")
|
|
|
|
.each do |ts|
|
|
|
|
puts "- Theme setting #{ts["name"]} -> #{ts["value"]}"
|
|
|
|
|
|
|
|
begin
|
|
|
|
if ts["data_type"] == ThemeSetting.types[:upload]
|
|
|
|
ts["value"] = data.get_upload(ts["value"], for_theme: true)
|
|
|
|
end
|
|
|
|
|
|
|
|
ThemeSetting.find_or_initialize_by(
|
|
|
|
name: ts["name"],
|
|
|
|
theme: Theme.find_by(name: ts["theme"]),
|
|
|
|
).update!(data_type: ts["data_type"], value: ts["value"])
|
|
|
|
rescue => e
|
|
|
|
STDERR.puts "ERROR: Cannot import theme setting: #{e.message}"
|
|
|
|
puts e.backtrace
|
2023-01-09 20:10:19 +08:00
|
|
|
end
|
2021-05-24 10:38:42 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
puts
|
|
|
|
puts "Done"
|
|
|
|
puts
|
|
|
|
|
|
|
|
data.close
|
|
|
|
end
|