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.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/bin/jpsclient +28 -0
  4. data/lib/jpsclient/api/app_level.rb +138 -0
  5. data/lib/jpsclient/api/app_resource.rb +164 -0
  6. data/lib/jpsclient/api/app_resource_version.rb +52 -0
  7. data/lib/jpsclient/api/apple_account.rb +140 -0
  8. data/lib/jpsclient/api/application.rb +268 -0
  9. data/lib/jpsclient/api/application_category.rb +120 -0
  10. data/lib/jpsclient/api/application_design.rb +118 -0
  11. data/lib/jpsclient/api/application_income.rb +54 -0
  12. data/lib/jpsclient/api/application_sales.rb +52 -0
  13. data/lib/jpsclient/api/application_version.rb +77 -0
  14. data/lib/jpsclient/api/assets_category.rb +120 -0
  15. data/lib/jpsclient/api/bug.rb +140 -0
  16. data/lib/jpsclient/api/cert.rb +27 -0
  17. data/lib/jpsclient/api/client.rb +268 -0
  18. data/lib/jpsclient/api/collect.rb +52 -0
  19. data/lib/jpsclient/api/collection.rb +162 -0
  20. data/lib/jpsclient/api/commit_log.rb +104 -0
  21. data/lib/jpsclient/api/creative.rb +52 -0
  22. data/lib/jpsclient/api/custom_application.rb +230 -0
  23. data/lib/jpsclient/api/custom_application_web.rb +52 -0
  24. data/lib/jpsclient/api/document_text.rb +30 -0
  25. data/lib/jpsclient/api/experience.rb +143 -0
  26. data/lib/jpsclient/api/experience_category.rb +97 -0
  27. data/lib/jpsclient/api/fgui_export.rb +52 -0
  28. data/lib/jpsclient/api/file.rb +84 -0
  29. data/lib/jpsclient/api/game_assets.rb +140 -0
  30. data/lib/jpsclient/api/healthy.rb +30 -0
  31. data/lib/jpsclient/api/icon_and_snapshot.rb +54 -0
  32. data/lib/jpsclient/api/idea.rb +121 -0
  33. data/lib/jpsclient/api/jssdk.rb +30 -0
  34. data/lib/jpsclient/api/lark_bitable.rb +30 -0
  35. data/lib/jpsclient/api/lark_chat_group.rb +30 -0
  36. data/lib/jpsclient/api/lark_comment.rb +118 -0
  37. data/lib/jpsclient/api/lark_task.rb +140 -0
  38. data/lib/jpsclient/api/lark_task_list.rb +118 -0
  39. data/lib/jpsclient/api/lark_task_section.rb +74 -0
  40. data/lib/jpsclient/api/lark_user.rb +30 -0
  41. data/lib/jpsclient/api/lark_wiki_node.rb +30 -0
  42. data/lib/jpsclient/api/lark_wiki_space.rb +30 -0
  43. data/lib/jpsclient/api/lazy_client.rb +290 -0
  44. data/lib/jpsclient/api/login.rb +96 -0
  45. data/lib/jpsclient/api/m3u8.rb +30 -0
  46. data/lib/jpsclient/api/menu.rb +143 -0
  47. data/lib/jpsclient/api/modular_client.rb +228 -0
  48. data/lib/jpsclient/api/project.rb +46 -0
  49. data/lib/jpsclient/api/project_package.rb +249 -0
  50. data/lib/jpsclient/api/publisher.rb +165 -0
  51. data/lib/jpsclient/api/publisher_category.rb +142 -0
  52. data/lib/jpsclient/api/publisher_group.rb +118 -0
  53. data/lib/jpsclient/api/publisher_group_category.rb +120 -0
  54. data/lib/jpsclient/api/requirements.rb +143 -0
  55. data/lib/jpsclient/api/requirements_category.rb +97 -0
  56. data/lib/jpsclient/api/resource_category.rb +120 -0
  57. data/lib/jpsclient/api/role.rb +165 -0
  58. data/lib/jpsclient/api/simple_search.rb +162 -0
  59. data/lib/jpsclient/api/sketch.rb +74 -0
  60. data/lib/jpsclient/api/sketch_category.rb +97 -0
  61. data/lib/jpsclient/api/sov.rb +30 -0
  62. data/lib/jpsclient/api/statistics.rb +30 -0
  63. data/lib/jpsclient/api/store.rb +30 -0
  64. data/lib/jpsclient/api/survey.rb +143 -0
  65. data/lib/jpsclient/api/survey_category.rb +97 -0
  66. data/lib/jpsclient/api/tag.rb +142 -0
  67. data/lib/jpsclient/api/tool.rb +121 -0
  68. data/lib/jpsclient/api/tool_category.rb +120 -0
  69. data/lib/jpsclient/api/trending.rb +30 -0
  70. data/lib/jpsclient/api/ud_id.rb +118 -0
  71. data/lib/jpsclient/api/user.rb +99 -0
  72. data/lib/jpsclient/api/util.rb +30 -0
  73. data/lib/jpsclient/api/video_cover.rb +30 -0
  74. data/lib/jpsclient/api/webhook.rb +118 -0
  75. data/lib/jpsclient/api/workflow.rb +118 -0
  76. data/lib/jpsclient/auth/auth.rb +676 -0
  77. data/lib/jpsclient/auth/token.rb +209 -0
  78. data/lib/jpsclient/base/api_config.rb +225 -0
  79. data/lib/jpsclient/base/exception.rb +5 -0
  80. data/lib/jpsclient/http/http_client.rb +148 -0
  81. data/lib/jpsclient/upload/upload_client.rb +334 -0
  82. data/lib/jpsclient/upload/upload_config.rb +128 -0
  83. data/lib/jpsclient/upload/upload_progress.rb +73 -0
  84. data/lib/jpsclient/utils/aes.rb +49 -0
  85. data/lib/jpsclient/utils/logger.rb +38 -0
  86. data/lib/jpsclient/version.rb +3 -0
  87. data/lib/jpsclient.rb +34 -0
  88. 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
@@ -0,0 +1,3 @@
1
+ module JPSClient
2
+ VERSION = "0.2.0"
3
+ 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