Skip to content

Commit 87336c2

Browse files
committed
feat(导入导出): 实现用户数据导入导出功能
添加导出控制器和服务,支持将用户数据导出为JSON文件 添加导入控制器和服务,支持从JSON文件导入用户数据 更新路由配置,添加导入导出相关端点 更新测试夹具和添加控制器测试
1 parent 7d7fb2c commit 87336c2

8 files changed

Lines changed: 1066 additions & 9 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
class ExportsController < ApplicationController
2+
before_action :authorize_request
3+
4+
def create
5+
export_service = UserExportService.new(@current_user)
6+
export_data = export_service.export
7+
8+
filename = "everydocs_export_#{Time.current.strftime('%Y%m%d_%H%M%S')}.json"
9+
10+
send_data export_data.to_json,
11+
filename: filename,
12+
type: 'application/json',
13+
disposition: 'attachment'
14+
end
15+
end
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
class ImportsController < ApplicationController
2+
before_action :authorize_request
3+
4+
DRY_RUN_HEADER = 'X-Dry-Run'
5+
6+
def create
7+
import_data = params[:import] || params[:file]
8+
9+
if import_data.blank?
10+
render json: {
11+
success: false,
12+
errors: ['Missing import data. Please provide either: ' \
13+
'1) JSON file as multipart/form-data with key "file", or ' \
14+
'2) JSON object in request body with key "import"']
15+
}, status: :unprocessable_entity
16+
return
17+
end
18+
19+
json_data = parse_import_data(import_data)
20+
21+
if json_data.nil?
22+
render json: {
23+
success: false,
24+
errors: ['Invalid import data format']
25+
}, status: :unprocessable_entity
26+
return
27+
end
28+
29+
dry_run = request.headers[DRY_RUN_HEADER] == 'true' ||
30+
params[:dry_run] == 'true' ||
31+
params[:dry_run] == true
32+
33+
import_service = UserImportService.new(@current_user, json_data, dry_run: dry_run)
34+
35+
valid = import_service.validate
36+
result = import_service.execute if valid
37+
38+
if valid && result
39+
render_success_response(import_service, dry_run)
40+
else
41+
render_error_response(import_service)
42+
end
43+
rescue JSON::ParserError => e
44+
render json: {
45+
success: false,
46+
errors: ["Invalid JSON format: #{e.message}"]
47+
}, status: :unprocessable_entity
48+
end
49+
50+
def preview
51+
import_data = params[:import] || params[:file]
52+
53+
if import_data.blank?
54+
render json: {
55+
success: false,
56+
errors: ['Missing import data']
57+
}, status: :unprocessable_entity
58+
return
59+
end
60+
61+
json_data = parse_import_data(import_data)
62+
63+
if json_data.nil?
64+
render json: {
65+
success: false,
66+
errors: ['Invalid import data format']
67+
}, status: :unprocessable_entity
68+
return
69+
end
70+
71+
import_service = UserImportService.new(@current_user, json_data, dry_run: true)
72+
import_service.validate
73+
74+
data = json_data['data'] || {}
75+
76+
render json: {
77+
success: true,
78+
dry_run: true,
79+
preview: {
80+
schema_version: json_data['schema_version'],
81+
exported_at: json_data['exported_at'],
82+
source_user: json_data['user'],
83+
data_counts: {
84+
folders: data['folders']&.size || 0,
85+
tags: data['tags']&.size || 0,
86+
people: data['people']&.size || 0,
87+
states: data['states']&.size || 0,
88+
documents: data['documents']&.size || 0
89+
},
90+
encrypted_documents: data['documents']&.count { |d| d['encrypted_flag'] } || 0
91+
},
92+
warnings: import_service.warnings,
93+
errors: import_service.errors
94+
}, status: :ok
95+
rescue JSON::ParserError => e
96+
render json: {
97+
success: false,
98+
errors: ["Invalid JSON format: #{e.message}"]
99+
}, status: :unprocessable_entity
100+
end
101+
102+
private
103+
104+
def parse_import_data(data)
105+
if data.is_a?(ActionDispatch::Http::UploadedFile)
106+
JSON.parse(data.read)
107+
elsif data.is_a?(String)
108+
JSON.parse(data)
109+
elsif data.is_a?(Hash)
110+
data
111+
else
112+
nil
113+
end
114+
end
115+
116+
def render_success_response(import_service, dry_run)
117+
response = {
118+
success: true,
119+
dry_run: dry_run,
120+
imported_data: import_service.imported_data,
121+
warnings: import_service.warnings
122+
}
123+
124+
if dry_run
125+
response[:message] = 'Dry run completed successfully. ' \
126+
'No data was actually imported. ' \
127+
'Send request without X-Dry-Run header to perform actual import.'
128+
end
129+
130+
render json: response, status: :ok
131+
end
132+
133+
def render_error_response(import_service)
134+
render json: {
135+
success: false,
136+
errors: import_service.errors,
137+
warnings: import_service.warnings
138+
}, status: :unprocessable_entity
139+
end
140+
end
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
class UserExportService
2+
SCHEMA_VERSION = 1
3+
4+
def initialize(user)
5+
@user = user
6+
end
7+
8+
def export
9+
{
10+
schema_version: SCHEMA_VERSION,
11+
exported_at: Time.current.iso8601,
12+
user: {
13+
name: @user.name,
14+
email: @user.email
15+
},
16+
data: {
17+
folders: export_folders,
18+
tags: export_tags,
19+
people: export_people,
20+
states: export_states,
21+
documents: export_documents
22+
}
23+
}
24+
end
25+
26+
def export_as_json
27+
export.to_json
28+
end
29+
30+
private
31+
32+
def export_folders
33+
folders = @user.folders.order(:created_at)
34+
folders.map do |folder|
35+
{
36+
name: folder.name,
37+
parent_folder_name: folder.folder&.name,
38+
created_at: folder.created_at.iso8601,
39+
updated_at: folder.updated_at.iso8601
40+
}
41+
end
42+
end
43+
44+
def export_tags
45+
tags = @user.tags.order(:created_at)
46+
tags.map do |tag|
47+
{
48+
name: tag.name,
49+
color: tag.color,
50+
created_at: tag.created_at.iso8601,
51+
updated_at: tag.updated_at.iso8601
52+
}
53+
end
54+
end
55+
56+
def export_people
57+
people = @user.people.order(:created_at)
58+
people.map do |person|
59+
{
60+
name: person.name,
61+
created_at: person.created_at.iso8601,
62+
updated_at: person.updated_at.iso8601
63+
}
64+
end
65+
end
66+
67+
def export_states
68+
states = @user.states.order(:created_at)
69+
states.map do |state|
70+
{
71+
name: state.name,
72+
created_at: state.created_at.iso8601,
73+
updated_at: state.updated_at.iso8601
74+
}
75+
end
76+
end
77+
78+
def export_documents
79+
documents = @user.documents.order(:created_at)
80+
documents.map do |doc|
81+
doc_data = {
82+
title: doc.title,
83+
description: doc.description,
84+
document_date: doc.document_date.iso8601,
85+
version: doc.version&.to_s,
86+
encrypted_flag: doc.encrypted_flag,
87+
folder_name: doc.folder&.name,
88+
state_name: doc.state&.name,
89+
person_name: doc.person&.name,
90+
tag_names: doc.tags.pluck(:name),
91+
created_at: doc.created_at.iso8601,
92+
updated_at: doc.updated_at.iso8601
93+
}
94+
95+
unless doc.encrypted_flag
96+
doc_data[:document_text] = doc.document_text
97+
end
98+
99+
doc_data
100+
end
101+
end
102+
end

0 commit comments

Comments
 (0)