jpsclient 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/bin/jpsclient +28 -0
- data/lib/jpsclient/api/app_level.rb +138 -0
- data/lib/jpsclient/api/app_resource.rb +164 -0
- data/lib/jpsclient/api/app_resource_version.rb +52 -0
- data/lib/jpsclient/api/apple_account.rb +140 -0
- data/lib/jpsclient/api/application.rb +268 -0
- data/lib/jpsclient/api/application_category.rb +120 -0
- data/lib/jpsclient/api/application_design.rb +118 -0
- data/lib/jpsclient/api/application_income.rb +54 -0
- data/lib/jpsclient/api/application_sales.rb +52 -0
- data/lib/jpsclient/api/application_version.rb +77 -0
- data/lib/jpsclient/api/assets_category.rb +120 -0
- data/lib/jpsclient/api/bug.rb +140 -0
- data/lib/jpsclient/api/cert.rb +27 -0
- data/lib/jpsclient/api/client.rb +268 -0
- data/lib/jpsclient/api/collect.rb +52 -0
- data/lib/jpsclient/api/collection.rb +162 -0
- data/lib/jpsclient/api/commit_log.rb +104 -0
- data/lib/jpsclient/api/creative.rb +52 -0
- data/lib/jpsclient/api/custom_application.rb +230 -0
- data/lib/jpsclient/api/custom_application_web.rb +52 -0
- data/lib/jpsclient/api/document_text.rb +30 -0
- data/lib/jpsclient/api/experience.rb +143 -0
- data/lib/jpsclient/api/experience_category.rb +97 -0
- data/lib/jpsclient/api/fgui_export.rb +52 -0
- data/lib/jpsclient/api/file.rb +84 -0
- data/lib/jpsclient/api/game_assets.rb +140 -0
- data/lib/jpsclient/api/healthy.rb +30 -0
- data/lib/jpsclient/api/icon_and_snapshot.rb +54 -0
- data/lib/jpsclient/api/idea.rb +121 -0
- data/lib/jpsclient/api/jssdk.rb +30 -0
- data/lib/jpsclient/api/lark_bitable.rb +30 -0
- data/lib/jpsclient/api/lark_chat_group.rb +30 -0
- data/lib/jpsclient/api/lark_comment.rb +118 -0
- data/lib/jpsclient/api/lark_task.rb +140 -0
- data/lib/jpsclient/api/lark_task_list.rb +118 -0
- data/lib/jpsclient/api/lark_task_section.rb +74 -0
- data/lib/jpsclient/api/lark_user.rb +30 -0
- data/lib/jpsclient/api/lark_wiki_node.rb +30 -0
- data/lib/jpsclient/api/lark_wiki_space.rb +30 -0
- data/lib/jpsclient/api/lazy_client.rb +290 -0
- data/lib/jpsclient/api/login.rb +96 -0
- data/lib/jpsclient/api/m3u8.rb +30 -0
- data/lib/jpsclient/api/menu.rb +143 -0
- data/lib/jpsclient/api/modular_client.rb +228 -0
- data/lib/jpsclient/api/project.rb +46 -0
- data/lib/jpsclient/api/project_package.rb +249 -0
- data/lib/jpsclient/api/publisher.rb +165 -0
- data/lib/jpsclient/api/publisher_category.rb +142 -0
- data/lib/jpsclient/api/publisher_group.rb +118 -0
- data/lib/jpsclient/api/publisher_group_category.rb +120 -0
- data/lib/jpsclient/api/requirements.rb +143 -0
- data/lib/jpsclient/api/requirements_category.rb +97 -0
- data/lib/jpsclient/api/resource_category.rb +120 -0
- data/lib/jpsclient/api/role.rb +165 -0
- data/lib/jpsclient/api/simple_search.rb +162 -0
- data/lib/jpsclient/api/sketch.rb +74 -0
- data/lib/jpsclient/api/sketch_category.rb +97 -0
- data/lib/jpsclient/api/sov.rb +30 -0
- data/lib/jpsclient/api/statistics.rb +30 -0
- data/lib/jpsclient/api/store.rb +30 -0
- data/lib/jpsclient/api/survey.rb +143 -0
- data/lib/jpsclient/api/survey_category.rb +97 -0
- data/lib/jpsclient/api/tag.rb +142 -0
- data/lib/jpsclient/api/tool.rb +121 -0
- data/lib/jpsclient/api/tool_category.rb +120 -0
- data/lib/jpsclient/api/trending.rb +30 -0
- data/lib/jpsclient/api/ud_id.rb +118 -0
- data/lib/jpsclient/api/user.rb +99 -0
- data/lib/jpsclient/api/util.rb +30 -0
- data/lib/jpsclient/api/video_cover.rb +30 -0
- data/lib/jpsclient/api/webhook.rb +118 -0
- data/lib/jpsclient/api/workflow.rb +118 -0
- data/lib/jpsclient/auth/auth.rb +676 -0
- data/lib/jpsclient/auth/token.rb +209 -0
- data/lib/jpsclient/base/api_config.rb +225 -0
- data/lib/jpsclient/base/exception.rb +5 -0
- data/lib/jpsclient/http/http_client.rb +148 -0
- data/lib/jpsclient/upload/upload_client.rb +334 -0
- data/lib/jpsclient/upload/upload_config.rb +128 -0
- data/lib/jpsclient/upload/upload_progress.rb +73 -0
- data/lib/jpsclient/utils/aes.rb +49 -0
- data/lib/jpsclient/utils/logger.rb +38 -0
- data/lib/jpsclient/version.rb +3 -0
- data/lib/jpsclient.rb +34 -0
- metadata +269 -0
@@ -0,0 +1,334 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'typhoeus'
|
3
|
+
require 'thread'
|
4
|
+
require 'etc'
|
5
|
+
require 'digest'
|
6
|
+
require 'jpsclient/base/exception'
|
7
|
+
require 'jpsclient/upload/upload_config'
|
8
|
+
require 'jpsclient/upload/upload_progress'
|
9
|
+
require 'jpsclient/utils/logger'
|
10
|
+
|
11
|
+
module JPSClient
|
12
|
+
|
13
|
+
|
14
|
+
class UploadClient
|
15
|
+
|
16
|
+
attr_accessor :upload_binary_file
|
17
|
+
attr_accessor :file_size
|
18
|
+
attr_accessor :progress_bar
|
19
|
+
attr_reader :jps_client
|
20
|
+
|
21
|
+
def initialize(jps_client)
|
22
|
+
raise ExceptionError, "必须提供 Client 实例" unless jps_client
|
23
|
+
|
24
|
+
@jps_client = jps_client
|
25
|
+
|
26
|
+
# 从 Client 获取所需资源
|
27
|
+
config_json = @jps_client.config_json
|
28
|
+
|
29
|
+
# 加载上传配置
|
30
|
+
@upload_config = UploadConfig.from_json(config_json["upload_config"])
|
31
|
+
unless @upload_config && @upload_config.valid?
|
32
|
+
raise ExceptionError, "上传配置无效或不完整"
|
33
|
+
end
|
34
|
+
# 添加互斥锁用于线程安全
|
35
|
+
@upload_eTags_mutex = Mutex.new
|
36
|
+
@tasks_queue_mutex = Mutex.new
|
37
|
+
@active_tasks_mutex = Mutex.new
|
38
|
+
@upload_failed_mutex = Mutex.new
|
39
|
+
@upload_failed = false
|
40
|
+
end
|
41
|
+
|
42
|
+
def upload_file(binary_file:nil, isAttach:false)
|
43
|
+
|
44
|
+
raise ExceptionError, "上传文件不能为空" if binary_file.nil? || !File.exist?(binary_file)
|
45
|
+
|
46
|
+
@upload_binary_file = binary_file
|
47
|
+
@file_size = File.size(@upload_binary_file)
|
48
|
+
@progress_bar = UploadProgress.new(upload_total_size:@file_size)
|
49
|
+
@upload_failed = false # 重置上传失败标志
|
50
|
+
|
51
|
+
# 生成S3 Key (保持UUID方式)
|
52
|
+
file_uuid = SecureRandom.uuid
|
53
|
+
extension = File.extname(@upload_binary_file)
|
54
|
+
filename = File.basename(@upload_binary_file)
|
55
|
+
|
56
|
+
# 使用配置中的路径前缀
|
57
|
+
path_prefix = isAttach ? @upload_config.attach_url : @upload_config.default_url
|
58
|
+
s3_key = "#{path_prefix}#{file_uuid}#{extension}"
|
59
|
+
|
60
|
+
# 计算文件大小和分片信息
|
61
|
+
total_mb = sprintf("%.2f", @file_size / 1024.0 / 1024.0)
|
62
|
+
chunk_size = @upload_config.chunk_size_bytes
|
63
|
+
chunks = (@file_size.to_f / chunk_size).ceil
|
64
|
+
|
65
|
+
# 使用配置的并发工作线程数和重试次数
|
66
|
+
task_num = @upload_config.concurrent_workers
|
67
|
+
retry_count = @upload_config.max_retry_times
|
68
|
+
|
69
|
+
puts "文件路径: #{@upload_binary_file}"
|
70
|
+
puts "文件大小: #{total_mb}M"
|
71
|
+
puts "上传路径: #{s3_key}"
|
72
|
+
puts
|
73
|
+
puts "切片大小: #{@upload_config.chunk_size_mb}MB"
|
74
|
+
|
75
|
+
upload_result = nil
|
76
|
+
|
77
|
+
begin
|
78
|
+
# 步骤1: 初始化多部分上传
|
79
|
+
Logger.instance.fancyinfo_start("初始化上传...")
|
80
|
+
init_result = @jps_client.init_file_multipart(
|
81
|
+
s3_key: s3_key,
|
82
|
+
content_type: "",
|
83
|
+
upload_config: @upload_config
|
84
|
+
)
|
85
|
+
|
86
|
+
if init_result.nil? || !init_result.dig("data", "upload_id")
|
87
|
+
raise ExceptionError, "初始化上传失败,请检查网络或服务器状态"
|
88
|
+
end
|
89
|
+
|
90
|
+
upload_id = init_result["data"]["upload_id"]
|
91
|
+
|
92
|
+
# 准备分片任务队列
|
93
|
+
@tasks_queue = Queue.new
|
94
|
+
@worker_threads = []
|
95
|
+
@expected_parts = chunks # 保存预期的分片数量
|
96
|
+
|
97
|
+
# 创建分片任务
|
98
|
+
chunks.times do |i|
|
99
|
+
task_item = {
|
100
|
+
"partNo" => i + 1,
|
101
|
+
"s3Key" => s3_key,
|
102
|
+
"uploadId" => upload_id,
|
103
|
+
"retryCount" => retry_count
|
104
|
+
}
|
105
|
+
@tasks_queue.push(task_item)
|
106
|
+
end
|
107
|
+
|
108
|
+
puts "文件分片数: #{chunks}"
|
109
|
+
puts "并发上传线程数: #{task_num} (CPU核心数: #{Etc.nprocessors})"
|
110
|
+
puts "失败重试次数: #{retry_count}"
|
111
|
+
puts
|
112
|
+
|
113
|
+
Logger.instance.fancyinfo_start("开始上传...")
|
114
|
+
@upload_eTags = []
|
115
|
+
@active_tasks = 0 # 跟踪活动任务数量
|
116
|
+
|
117
|
+
continuous_upload_data_req(concurrency: task_num)
|
118
|
+
|
119
|
+
# 检查上传是否全部成功
|
120
|
+
if upload_failed? || @upload_eTags.length != @expected_parts
|
121
|
+
upload_result = nil
|
122
|
+
Logger.instance.fancyinfo_error("文件#{@upload_binary_file} 上传失败! 😭😭😭")
|
123
|
+
return upload_result
|
124
|
+
end
|
125
|
+
|
126
|
+
# 步骤3: 完成多部分上传
|
127
|
+
Logger.instance.fancyinfo_start("完成上传...")
|
128
|
+
complete_result = @jps_client.complete_file_multipart(
|
129
|
+
s3_key: s3_key,
|
130
|
+
upload_id: upload_id,
|
131
|
+
upload_config: @upload_config
|
132
|
+
)
|
133
|
+
|
134
|
+
if complete_result && complete_result["code"] == 200
|
135
|
+
upload_result = complete_result.dig("data", "url") || s3_key
|
136
|
+
Logger.instance.fancyinfo_success("文件#{@upload_binary_file} 上传成功! 😎😎😎")
|
137
|
+
else
|
138
|
+
upload_result = nil
|
139
|
+
error_msg = complete_result["msg"] || complete_result["message"] || "未知错误"
|
140
|
+
Logger.instance.fancyinfo_error("文件#{@upload_binary_file} 上传失败: #{error_msg} 😭😭😭")
|
141
|
+
end
|
142
|
+
|
143
|
+
rescue => e
|
144
|
+
upload_result = nil
|
145
|
+
Logger.instance.fancyinfo_error("文件上传过程发生异常: #{e.message} 😭😭😭")
|
146
|
+
ensure
|
147
|
+
# 确保所有工作线程都被清理
|
148
|
+
cleanup_worker_threads
|
149
|
+
end
|
150
|
+
|
151
|
+
return upload_result
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
# 安全地检查上传失败状态
|
156
|
+
def upload_failed?
|
157
|
+
@upload_failed_mutex.synchronize { @upload_failed }
|
158
|
+
end
|
159
|
+
|
160
|
+
# 安全地设置上传失败状态
|
161
|
+
def set_upload_failed(error_msg = nil)
|
162
|
+
@upload_failed_mutex.synchronize do
|
163
|
+
@upload_failed = true
|
164
|
+
Logger.instance.fancyinfo_error("上传失败: #{error_msg}") if error_msg
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# 清理所有工作线程
|
169
|
+
def cleanup_worker_threads
|
170
|
+
@worker_threads.each do |thread|
|
171
|
+
# 尝试安全终止线程
|
172
|
+
thread.exit if thread.alive?
|
173
|
+
end
|
174
|
+
@worker_threads.clear
|
175
|
+
end
|
176
|
+
|
177
|
+
def continuous_upload_data_req(concurrency:1)
|
178
|
+
# 使用固定大小的线程池,避免线程无限增长
|
179
|
+
@worker_threads = []
|
180
|
+
@active_tasks = 0
|
181
|
+
@task_complete_cv = ConditionVariable.new
|
182
|
+
|
183
|
+
# 创建固定数量的工作线程
|
184
|
+
concurrency.times do
|
185
|
+
@worker_threads << Thread.new { worker_loop }
|
186
|
+
end
|
187
|
+
|
188
|
+
# 设置超时保护
|
189
|
+
timeout_seconds = 300 # 5分钟超时
|
190
|
+
start_time = Time.now
|
191
|
+
|
192
|
+
# 主线程等待所有任务完成
|
193
|
+
@tasks_queue_mutex.synchronize do
|
194
|
+
while (@active_tasks > 0 || !@tasks_queue.empty?) && !upload_failed?
|
195
|
+
remaining_time = timeout_seconds - (Time.now - start_time)
|
196
|
+
if remaining_time <= 0
|
197
|
+
set_upload_failed("上传任务超时")
|
198
|
+
break
|
199
|
+
end
|
200
|
+
|
201
|
+
# 等待任务完成通知
|
202
|
+
@task_complete_cv.wait(@tasks_queue_mutex, [remaining_time, 30].min)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# 停止所有工作线程
|
207
|
+
@worker_threads.each { |t| t.kill }
|
208
|
+
|
209
|
+
# 检查所有分片是否都上传成功
|
210
|
+
if @upload_eTags.length != @expected_parts && !upload_failed?
|
211
|
+
set_upload_failed("部分分片上传失败,已上传#{@upload_eTags.length}/#{@expected_parts}")
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# 工作线程的主循环
|
216
|
+
def worker_loop
|
217
|
+
loop do
|
218
|
+
upload_params_item = nil
|
219
|
+
|
220
|
+
# 从队列获取任务
|
221
|
+
@tasks_queue_mutex.synchronize do
|
222
|
+
return if upload_failed? && @tasks_queue.empty?
|
223
|
+
|
224
|
+
if @tasks_queue.empty?
|
225
|
+
# 队列为空,等待新任务
|
226
|
+
@task_complete_cv.wait(@tasks_queue_mutex, 1)
|
227
|
+
next
|
228
|
+
end
|
229
|
+
|
230
|
+
upload_params_item = @tasks_queue.pop
|
231
|
+
@active_tasks_mutex.synchronize { @active_tasks += 1 }
|
232
|
+
end
|
233
|
+
|
234
|
+
# 处理任务
|
235
|
+
if upload_params_item
|
236
|
+
begin
|
237
|
+
process_upload_task(upload_params_item)
|
238
|
+
rescue => e
|
239
|
+
set_upload_failed("处理分片#{upload_params_item["partNo"]}时出错: #{e.message}")
|
240
|
+
ensure
|
241
|
+
@active_tasks_mutex.synchronize { @active_tasks -= 1 }
|
242
|
+
@tasks_queue_mutex.synchronize { @task_complete_cv.broadcast }
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# 检查是否应该退出
|
247
|
+
break if upload_failed?
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def process_upload_task(upload_params_item)
|
252
|
+
part_no = upload_params_item["partNo"]
|
253
|
+
s3_key = upload_params_item["s3Key"]
|
254
|
+
upload_id = upload_params_item["uploadId"]
|
255
|
+
|
256
|
+
# 步骤2: 获取分片的预签名URL
|
257
|
+
sign_result = @jps_client.get_file_sign_url(
|
258
|
+
s3_key: s3_key,
|
259
|
+
upload_id: upload_id,
|
260
|
+
part_id: part_no,
|
261
|
+
upload_config: @upload_config
|
262
|
+
)
|
263
|
+
|
264
|
+
if sign_result.nil? || !sign_result.dig("data", "url")
|
265
|
+
raise ExceptionError, "获取分片#{part_no}的上传URL失败"
|
266
|
+
end
|
267
|
+
|
268
|
+
upload_url = sign_result["data"]["url"]
|
269
|
+
|
270
|
+
# 计算分片数据范围(使用配置的分片大小)
|
271
|
+
chunk_size = @upload_config.chunk_size_bytes
|
272
|
+
start_position = chunk_size * (part_no - 1)
|
273
|
+
if part_no * chunk_size > @file_size
|
274
|
+
read_length = @file_size - start_position
|
275
|
+
else
|
276
|
+
read_length = chunk_size
|
277
|
+
end
|
278
|
+
|
279
|
+
file = File.open(@upload_binary_file, "rb")
|
280
|
+
begin
|
281
|
+
file.seek(start_position)
|
282
|
+
put_data = file.read(read_length)
|
283
|
+
|
284
|
+
# 创建上传请求(直接使用预签名URL)
|
285
|
+
request = Typhoeus::Request.new(
|
286
|
+
upload_url,
|
287
|
+
method: :put,
|
288
|
+
body: put_data,
|
289
|
+
headers: {
|
290
|
+
'Content-Type' => 'application/octet-stream',
|
291
|
+
'Content-Length' => read_length.to_s
|
292
|
+
},
|
293
|
+
timeout: 300 # 5分钟超时
|
294
|
+
)
|
295
|
+
|
296
|
+
# 设置上传进度回调
|
297
|
+
upload_size_last = 0
|
298
|
+
request.on_progress do |dltotal, dlnow, ultotal, ulnow|
|
299
|
+
if ulnow && ulnow > upload_size_last
|
300
|
+
upload_size_last = ulnow
|
301
|
+
@progress_bar.update_upload_index(upload_part:part_no, upload_size:ulnow)
|
302
|
+
@progress_bar.update_upload_progress()
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
# 设置请求超时
|
307
|
+
request.options[:timeout] = 300 # 5分钟超时
|
308
|
+
|
309
|
+
# 执行请求并等待完成
|
310
|
+
response = request.run
|
311
|
+
|
312
|
+
# 处理响应结果
|
313
|
+
if response.success?
|
314
|
+
@progress_bar.complete_upload_index(upload_part:part_no, complete_size:read_length)
|
315
|
+
# 新API不再需要收集ETag,只记录成功的分片号
|
316
|
+
@upload_eTags_mutex.synchronize { @upload_eTags << part_no }
|
317
|
+
else
|
318
|
+
@progress_bar.delete_upload_index(upload_part:part_no)
|
319
|
+
upload_params_item["retryCount"] = upload_params_item["retryCount"] - 1
|
320
|
+
if upload_params_item["retryCount"] > 0
|
321
|
+
# 重试任务
|
322
|
+
@tasks_queue_mutex.synchronize { @tasks_queue.push(upload_params_item) }
|
323
|
+
else
|
324
|
+
set_upload_failed("文件#{@upload_binary_file} 分片#{part_no}上传失败: HTTP #{response.code}")
|
325
|
+
end
|
326
|
+
end
|
327
|
+
ensure
|
328
|
+
file.close
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
|
333
|
+
end
|
334
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'etc'
|
2
|
+
|
3
|
+
module JPSClient
|
4
|
+
# 上传配置类
|
5
|
+
class UploadConfig
|
6
|
+
attr_accessor :region
|
7
|
+
attr_accessor :bucket_name
|
8
|
+
attr_accessor :access_key_id
|
9
|
+
attr_accessor :access_key_secret
|
10
|
+
attr_accessor :default_url
|
11
|
+
attr_accessor :attach_url
|
12
|
+
attr_accessor :upload_type # 新增:上传类型
|
13
|
+
attr_accessor :concurrent_workers # 新增:并发上传工作线程数
|
14
|
+
attr_accessor :chunk_size_mb # 新增:分片大小(MB)
|
15
|
+
attr_accessor :max_retry_times # 新增:最大重试次数
|
16
|
+
|
17
|
+
def initialize(
|
18
|
+
region: nil,
|
19
|
+
bucket_name: nil,
|
20
|
+
access_key_id: nil,
|
21
|
+
access_key_secret: nil,
|
22
|
+
default_url: nil,
|
23
|
+
attach_url: nil,
|
24
|
+
upload_type: nil,
|
25
|
+
concurrent_workers: nil,
|
26
|
+
chunk_size_mb: nil,
|
27
|
+
max_retry_times: nil
|
28
|
+
)
|
29
|
+
@region = region
|
30
|
+
@bucket_name = bucket_name
|
31
|
+
@access_key_id = access_key_id
|
32
|
+
@access_key_secret = access_key_secret
|
33
|
+
@default_url = default_url
|
34
|
+
@attach_url = attach_url
|
35
|
+
@upload_type = upload_type
|
36
|
+
@concurrent_workers = concurrent_workers
|
37
|
+
@chunk_size_mb = chunk_size_mb
|
38
|
+
@max_retry_times = max_retry_times
|
39
|
+
end
|
40
|
+
|
41
|
+
# 从 JSON 配置创建实例
|
42
|
+
def self.from_json(json_config)
|
43
|
+
return nil unless json_config
|
44
|
+
|
45
|
+
# 动态计算并发数
|
46
|
+
concurrent_workers = determine_concurrent_workers(json_config['concurrent_workers'])
|
47
|
+
|
48
|
+
new(
|
49
|
+
region: json_config['region'] || "ap-southeast-1",
|
50
|
+
bucket_name: json_config['bucket_name'] || "pgy-resource-new",
|
51
|
+
access_key_id: json_config['access_key_id'],
|
52
|
+
access_key_secret: json_config['access_key_secret'],
|
53
|
+
default_url: json_config['default_url'] || "resource/",
|
54
|
+
attach_url: json_config['attach_url'] || "attach_file/",
|
55
|
+
upload_type: json_config['upload_type'] || "s3",
|
56
|
+
concurrent_workers: concurrent_workers,
|
57
|
+
chunk_size_mb: json_config['chunk_size_mb'] || 5,
|
58
|
+
max_retry_times: json_config['max_retry_times'] || 3
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
# 智能确定并发工作线程数
|
63
|
+
# 策略:使用 min(CPU自适应值, 配置最大值)
|
64
|
+
def self.determine_concurrent_workers(config_value)
|
65
|
+
cpu_count = Etc.nprocessors
|
66
|
+
|
67
|
+
# 根据CPU计算推荐值(I/O密集型任务,使用CPU核心数的2倍)
|
68
|
+
cpu_recommended = cpu_count * 2
|
69
|
+
|
70
|
+
# 处理不同的配置值
|
71
|
+
if config_value.nil?
|
72
|
+
# 没有配置,使用CPU推荐值,但限制在4-16之间
|
73
|
+
return [[cpu_recommended, 4].max, 16].min
|
74
|
+
elsif config_value.to_s.downcase == 'auto'
|
75
|
+
# 自动模式:使用CPU推荐值,限制在4-16之间
|
76
|
+
return [[cpu_recommended, 4].max, 16].min
|
77
|
+
elsif config_value.is_a?(String) && config_value.include?('cpu')
|
78
|
+
# 支持 "cpu*2", "cpu*3" 这样的配置
|
79
|
+
if config_value =~ /cpu\s*\*\s*(\d+)/
|
80
|
+
multiplier = $1.to_i
|
81
|
+
calculated = cpu_count * multiplier
|
82
|
+
# 限制在 2-30 之间
|
83
|
+
return [[calculated, 2].max, 30].min
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# 配置值是数字时,作为最大限制
|
88
|
+
max_workers = config_value.to_i
|
89
|
+
if max_workers > 0
|
90
|
+
# 实际使用:min(CPU推荐值, 配置最大值)
|
91
|
+
# 但至少保证2个线程
|
92
|
+
actual_workers = [cpu_recommended, max_workers].min
|
93
|
+
return [actual_workers, 2].max
|
94
|
+
end
|
95
|
+
|
96
|
+
# 默认值
|
97
|
+
return 5
|
98
|
+
end
|
99
|
+
|
100
|
+
# 验证配置完整性
|
101
|
+
def valid?
|
102
|
+
!@region.nil? && !@bucket_name.nil? &&
|
103
|
+
!@access_key_id.nil? && !@access_key_secret.nil? &&
|
104
|
+
!@default_url.nil? && !@attach_url.nil?
|
105
|
+
end
|
106
|
+
|
107
|
+
# 获取分片大小(字节)
|
108
|
+
def chunk_size_bytes
|
109
|
+
@chunk_size_mb * 1024 * 1024
|
110
|
+
end
|
111
|
+
|
112
|
+
# 转换为 Hash
|
113
|
+
def to_h
|
114
|
+
{
|
115
|
+
'region' => @region,
|
116
|
+
'bucket_name' => @bucket_name,
|
117
|
+
'access_key_id' => @access_key_id,
|
118
|
+
'access_key_secret' => @access_key_secret,
|
119
|
+
'default_url' => @default_url,
|
120
|
+
'attach_url' => @attach_url,
|
121
|
+
'upload_type' => @upload_type,
|
122
|
+
'concurrent_workers' => @concurrent_workers,
|
123
|
+
'chunk_size_mb' => @chunk_size_mb,
|
124
|
+
'max_retry_times' => @max_retry_times
|
125
|
+
}
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'jpsclient/utils/logger'
|
3
|
+
|
4
|
+
module JPSClient
|
5
|
+
# 上传进度条管理类
|
6
|
+
class UploadProgress
|
7
|
+
|
8
|
+
attr_accessor :draw_char
|
9
|
+
attr_accessor :complete_size
|
10
|
+
attr_accessor :upload_total_size
|
11
|
+
attr_accessor :last_update_time
|
12
|
+
attr_accessor :update_ing_size
|
13
|
+
attr_accessor :is_done
|
14
|
+
|
15
|
+
def initialize(upload_total_size:nil, draw_char:'>')
|
16
|
+
@upload_total_size = upload_total_size
|
17
|
+
@draw_char = draw_char
|
18
|
+
@last_update_time = (Time.now.to_f * 1000).to_i #毫秒
|
19
|
+
|
20
|
+
@complete_size = 0
|
21
|
+
@update_ing_size = {}
|
22
|
+
@is_done = false
|
23
|
+
|
24
|
+
# 添加互斥锁来保护进度条更新
|
25
|
+
@mutex = Mutex.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def update_upload_index(upload_part:nil, upload_size:nil)
|
29
|
+
@mutex.synchronize do
|
30
|
+
@update_ing_size[upload_part] = upload_size
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def delete_upload_index(upload_part:nil)
|
35
|
+
@mutex.synchronize do
|
36
|
+
@update_ing_size[upload_part] = 0
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def complete_upload_index(upload_part:nil, complete_size:nil)
|
41
|
+
@mutex.synchronize do
|
42
|
+
@complete_size = @complete_size + complete_size
|
43
|
+
@update_ing_size[upload_part] = 0
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def update_upload_progress()
|
48
|
+
time_now = (Time.now.to_f * 1000).to_i #毫秒
|
49
|
+
if time_now - @last_update_time > 80
|
50
|
+
@mutex.synchronize do
|
51
|
+
@last_update_time = time_now
|
52
|
+
total_num = @upload_total_size
|
53
|
+
index_num = @complete_size
|
54
|
+
@update_ing_size.each do |key, value|
|
55
|
+
index_num = index_num + value
|
56
|
+
end
|
57
|
+
|
58
|
+
progress_str = sprintf("%.2f", 100.0 * index_num / total_num )
|
59
|
+
total_size = sprintf("%.2f", 1.00 * total_num / 1024 /1024 )
|
60
|
+
upload_size = sprintf("%.2f", 1.00 * index_num / 1024 /1024 )
|
61
|
+
index = 40.0 * index_num / total_num
|
62
|
+
upload_message = "已上传:#{upload_size}MB|#{progress_str}\%【" + (@draw_char * (index/1).floor).ljust(40.0, '_') + "】Total:#{total_size}MB"
|
63
|
+
Logger.instance.fancyinfo_update(upload_message)
|
64
|
+
if index_num == total_num && !@is_done
|
65
|
+
@is_done = true
|
66
|
+
Logger.instance.fancyinfo_success(upload_message)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
module JPSClient
|
5
|
+
# 独立的 AES 加密解密类
|
6
|
+
class AES
|
7
|
+
CIPHER = 'AES-128-ECB'
|
8
|
+
|
9
|
+
def initialize(key)
|
10
|
+
@key = key
|
11
|
+
validate_key!
|
12
|
+
end
|
13
|
+
|
14
|
+
# 加密字符串
|
15
|
+
def encrypt(plain_text)
|
16
|
+
return nil if plain_text.nil? || plain_text.empty?
|
17
|
+
|
18
|
+
cipher = OpenSSL::Cipher.new(CIPHER)
|
19
|
+
cipher.encrypt
|
20
|
+
cipher.key = @key
|
21
|
+
|
22
|
+
encrypted = cipher.update(plain_text) + cipher.final
|
23
|
+
Base64.strict_encode64(encrypted)
|
24
|
+
rescue => e
|
25
|
+
raise ExceptionError, "加密失败: #{e.message}"
|
26
|
+
end
|
27
|
+
|
28
|
+
# 解密字符串
|
29
|
+
def decrypt(encrypted_text)
|
30
|
+
return nil if encrypted_text.nil? || encrypted_text.empty?
|
31
|
+
|
32
|
+
cipher = OpenSSL::Cipher.new(CIPHER)
|
33
|
+
cipher.decrypt
|
34
|
+
cipher.key = @key
|
35
|
+
|
36
|
+
decoded = Base64.strict_decode64(encrypted_text)
|
37
|
+
cipher.update(decoded) + cipher.final
|
38
|
+
rescue => e
|
39
|
+
raise ExceptionError, "解密失败: #{e.message}"
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def validate_key!
|
45
|
+
raise ExceptionError, "AES密钥不能为空" if @key.nil? || @key.empty?
|
46
|
+
raise ExceptionError, "AES密钥长度必须为16字节" if @key.bytesize != 16
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module JPSClient
|
2
|
+
# 简单的Logger实现,替代Funlog
|
3
|
+
class Logger
|
4
|
+
def self.instance
|
5
|
+
@instance ||= new
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@verbose = ENV['JPS_DEBUG'] == 'true'
|
10
|
+
end
|
11
|
+
|
12
|
+
def fancyinfo_start(message)
|
13
|
+
puts "▶ #{message}" if @verbose || true # 总是显示开始信息
|
14
|
+
end
|
15
|
+
|
16
|
+
def fancyinfo_success(message)
|
17
|
+
puts "✅ #{message}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def fancyinfo_error(message)
|
21
|
+
puts "❌ #{message}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def fancyinfo_update(message)
|
25
|
+
# 进度更新,使用 \r 实现覆盖
|
26
|
+
print "\r#{message}"
|
27
|
+
$stdout.flush
|
28
|
+
end
|
29
|
+
|
30
|
+
def info(message)
|
31
|
+
puts message if @verbose
|
32
|
+
end
|
33
|
+
|
34
|
+
def error(message)
|
35
|
+
$stderr.puts "ERROR: #{message}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/jpsclient.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# 主入口文件
|
2
|
+
require 'jpsclient/version'
|
3
|
+
|
4
|
+
# 基础模块
|
5
|
+
require 'jpsclient/base/exception'
|
6
|
+
require 'jpsclient/base/api_config'
|
7
|
+
|
8
|
+
# API 客户端
|
9
|
+
require 'jpsclient/api/client'
|
10
|
+
|
11
|
+
# 工具模块
|
12
|
+
require 'jpsclient/utils/logger'
|
13
|
+
require 'jpsclient/utils/aes'
|
14
|
+
|
15
|
+
# HTTP 模块
|
16
|
+
require 'jpsclient/http/http_client'
|
17
|
+
|
18
|
+
# 认证模块
|
19
|
+
require 'jpsclient/auth/token'
|
20
|
+
require 'jpsclient/auth/auth'
|
21
|
+
|
22
|
+
# 上传模块
|
23
|
+
require 'jpsclient/upload/upload_config'
|
24
|
+
require 'jpsclient/upload/upload_progress'
|
25
|
+
require 'jpsclient/upload/upload_client'
|
26
|
+
|
27
|
+
module JPSClient
|
28
|
+
class << self
|
29
|
+
# 便捷方法:创建客户端实例
|
30
|
+
def new(config_file:)
|
31
|
+
Client.new(config_file: config_file)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|