pindo 5.0.5 → 5.0.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3875a278d39ee97be6efefd2e2691f398cb0ec145c079e6ff261181e1f7c4cde
4
- data.tar.gz: 463a3b8a6b68376a35017ed3fe9457c86d06ee60eeb3404d02e3aaaca2f152d5
3
+ metadata.gz: da7db67cae593054d94da87ec555ccd0d6ed2691ca9d534729a386b4cc55f360
4
+ data.tar.gz: 25b281165febd17ac56dfcadd64dcf2e55674e326b28575ff3952007b7dbdc2d
5
5
  SHA512:
6
- metadata.gz: 693df9ad920466ab0f93c13e0f5774d970fa24fefb66ff7f0722ca56e25d687098cddb8808113cc4c02feb97d75d7e02ee4b78a6de9f9ec33c545bf8748c910d
7
- data.tar.gz: ac047d01af53a8b1be66faf6e6aa667e64d67135d32c1d4c8fc4313a59f4df2bb5b18032d2f659eec81f21c3c43a4f8bccfbc6aa0fe94030b86f82e42cf6facd
6
+ metadata.gz: '089d77424e3f53c88de5d9839b6849a11c868e6bf637b485403cec677a0e320cc2292e09fe8c868ecec3950a28e21b060a0867eadcd9b01dd1cab7aaaf8b878d'
7
+ data.tar.gz: 34a13c1bf0691285cd4d7c76c284c288c445ec2f3e67650d504ccdf1d3c2baa599c91492943ddee6078245372e5ae42746d2cd2bb881f9da0075305f7bd03d66
@@ -0,0 +1,313 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'webrick'
4
+ require 'json'
5
+ require 'net/http'
6
+ require 'uri'
7
+ require 'securerandom'
8
+ require 'digest'
9
+ require 'base64'
10
+ require 'fileutils'
11
+ require 'pindo/base/aeshelper'
12
+ require 'pindo/config/pindouserlocalconfig'
13
+
14
+ module Pindo
15
+
16
+ class PgyerFeishuOAuthCLI
17
+ attr_reader :access_token, :refresh_token, :user_info
18
+
19
+ def initialize(client_id)
20
+ @client_id = client_id
21
+ @feishu_auth_url = 'https://open.feishu.cn/open-apis/authen/v1/index'
22
+ @redirect_uri = 'https://jps-new.devtestapp.com/auth/jwt/login'
23
+ # @state = SecureRandom.hex(16)
24
+ @state = 'success_login'
25
+ @larkScopeList = [
26
+ 'offline_access',
27
+ 'task:task:write',
28
+ 'task:section:write',
29
+ 'task:custom_field:write',
30
+ 'task:tasklist:write',
31
+ ];
32
+
33
+ @access_token = nil
34
+ @refresh_token = nil
35
+ @token_expired_at = nil
36
+
37
+ @pgyer_token_file = File.join(File::expand_path(Pindoconfig.instance.pindo_dir), ".pgyer_token")
38
+
39
+ # 尝试读取配置文件中的AES密钥
40
+ begin
41
+ config_file = File.join(File::expand_path(Pindoconfig.instance.pindo_common_configdir), "pgyer_client_config.json")
42
+ if File.exist?(config_file)
43
+ config_json = JSON.parse(File.read(config_file))
44
+ @pgyer_aes_key = config_json["pgyerapps_aes_key"] if config_json["pgyerapps_aes_key"]
45
+ end
46
+ rescue => e
47
+ puts "读取配置文件失败: #{e.message}"
48
+ end
49
+
50
+ # 如果配置文件中没有找到密钥,则使用默认密钥
51
+ @pgyer_aes_key ||= ENV['PGYER_AES_KEY'] || "pgyerOauthToken2024"
52
+
53
+ # Pgyer API endpoints
54
+ @pgyer_api_endpoint = 'https://jps-api.devtestapp.com/api/lark_login'
55
+ end
56
+
57
+ # 启动授权流程
58
+ def authorize
59
+ # 先检查是否有存储的令牌
60
+ if load_token
61
+ puts "找到已保存的Pgyer令牌,尝试验证..."
62
+ if validate_pgyer_token
63
+ puts "已有令牌验证成功!"
64
+ return true
65
+ else
66
+ puts "已有令牌已失效,需要重新授权。"
67
+ end
68
+ end
69
+
70
+ authorization_uri = build_authorization_uri
71
+ puts "正在打开浏览器进行飞书OAuth授权..."
72
+ puts "授权URI: #{authorization_uri}"
73
+
74
+ # 在浏览器中打开授权URL
75
+ open_browser(authorization_uri)
76
+
77
+ # 启动本地服务器处理回调
78
+ code = start_callback_server
79
+
80
+ if code
81
+ puts "正在使用飞书身份登录Pgyer..."
82
+ if login_pgyer_with_feishu(code:code)
83
+ store_token
84
+ return true
85
+ end
86
+ return false
87
+ else
88
+ puts "授权失败"
89
+ return false
90
+ end
91
+ end
92
+
93
+ # 验证Pgyer令牌
94
+ def validate_pgyer_token
95
+ return false unless @access_token
96
+
97
+ # 这里应该调用Pgyer的API验证令牌有效性
98
+ # 由于没有具体API文档,这里仅做示例
99
+ uri = URI("https://www.pgyer.com/api/user/profile")
100
+ request = Net::HTTP::Get.new(uri)
101
+ request['Authorization'] = "Bearer #{@access_token}"
102
+
103
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
104
+ http.request(request)
105
+ end
106
+
107
+ return response.code == '200'
108
+ end
109
+
110
+ # 使用code登录Pgyer, 并且Pgyer返回token
111
+ def login_pgyer_with_feishu(code: nil)
112
+ return false unless code
113
+
114
+ uri = URI(@pgyer_api_endpoint)
115
+ request = Net::HTTP::Post.new(uri)
116
+ request['Content-Type'] = 'application/json'
117
+
118
+ # 根据Pgyer的API要求构造请求体
119
+ request.body = {
120
+ code: code,
121
+ redirectUri: @redirect_uri,
122
+ scope: @larkScopeList.join(' ')
123
+ }.to_json
124
+
125
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
126
+ http.request(request)
127
+ end
128
+
129
+ if response.code == '200'
130
+ begin
131
+ result = JSON.parse(response.body)
132
+ if !result['meta'].nil? && result['meta']['code'] == 200 && !result['data'].nil? && !result['data']['token'].nil?
133
+ @access_token = result['data']['token']
134
+ @username = result['data']['token']
135
+ @user_id = result['data']['user_id']
136
+
137
+ return true
138
+ else
139
+ puts "Pgyer登录失败: #{result['message'] || '未知错误'}"
140
+ return false
141
+ end
142
+ rescue JSON::ParserError
143
+ puts "解析Pgyer响应失败"
144
+ return false
145
+ end
146
+ else
147
+ puts "Pgyer登录请求失败: HTTP #{response.code}"
148
+ return false
149
+ end
150
+
151
+ end
152
+
153
+
154
+ # 加载存储的令牌
155
+ def load_token
156
+ return false unless File.exist?(@pgyer_token_file)
157
+
158
+ begin
159
+ encrypted_data = File.read(@pgyer_token_file)
160
+ json_data = aes_decrypt(encrypted_data, @pgyer_aes_key)
161
+ token_data = JSON.parse(json_data)
162
+
163
+ @access_token = token_data['token']
164
+ @user_info = token_data['user_info'] if token_data['user_info']
165
+ @token_expired_at = Time.at(token_data['expires_at']) if token_data['expires_at']
166
+
167
+ # 检查令牌是否过期
168
+ if @token_expired_at && Time.now >= @token_expired_at
169
+ puts "令牌已过期,需要重新授权"
170
+ return false
171
+ end
172
+
173
+ return true
174
+ rescue => e
175
+ puts "加载令牌失败: #{e.message}"
176
+ return false
177
+ end
178
+ end
179
+
180
+ # 存储令牌
181
+ def store_token
182
+ return false unless @access_token
183
+
184
+ token_data = {
185
+ 'token' => @access_token,
186
+ 'user_info' => @user_info,
187
+ 'expires_at' => Time.now.to_i + 7*24*60*60 # 假设令牌有效期为7天
188
+ }
189
+
190
+ encrypted_data = aes_encrypt(token_data.to_json, @pgyer_aes_key)
191
+
192
+ begin
193
+ File.open(@pgyer_token_file, 'w') do |f|
194
+ f.write(encrypted_data)
195
+ end
196
+ puts "令牌已安全存储"
197
+ return true
198
+ rescue => e
199
+ puts "存储令牌失败: #{e.message}"
200
+ return false
201
+ end
202
+ end
203
+
204
+ private
205
+
206
+ # AES加密
207
+ def aes_encrypt(data, key)
208
+ # 使用Pindo的AESHelper类进行真正的AES加密
209
+ AESHelper::aes_128_ecb_encrypt(key, data)
210
+ end
211
+
212
+ # AES解密
213
+ def aes_decrypt(encrypted_data, key)
214
+ # 使用Pindo的AESHelper类进行真正的AES解密
215
+ AESHelper::aes_128_ecb_decrypt(key, encrypted_data)
216
+ end
217
+
218
+ # 构建授权URI
219
+ def build_authorization_uri
220
+ uri = URI(@feishu_auth_url)
221
+
222
+
223
+
224
+ params = {
225
+ 'app_id' => @client_id,
226
+ 'redirect_uri' => @redirect_uri,
227
+ 'response_type' => 'code',
228
+ 'state' => @state,
229
+ 'scope' => @larkScopeList.join(' ')
230
+ }
231
+
232
+ uri.query = URI.encode_www_form(params)
233
+ uri.to_s
234
+ end
235
+
236
+ # 在浏览器中打开URL
237
+ def open_browser(url)
238
+ if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
239
+ system("start", url)
240
+ elsif RbConfig::CONFIG['host_os'] =~ /darwin/
241
+ system("open", url)
242
+ elsif RbConfig::CONFIG['host_os'] =~ /linux|bsd/
243
+ system("xdg-open", url)
244
+ else
245
+ puts "无法自动打开浏览器,请手动访问: #{url}"
246
+ end
247
+ end
248
+
249
+ # 启动本地服务器处理回调
250
+ def start_callback_server
251
+ code = nil
252
+ uri = URI(@redirect_uri)
253
+ server = WEBrick::HTTPServer.new(
254
+ Port: uri.port,
255
+ Logger: WEBrick::Log.new("/dev/null"),
256
+ AccessLog: []
257
+ )
258
+
259
+ path = uri.path.empty? ? '/' : uri.path
260
+ server.mount_proc path do |req, res|
261
+ query_params = URI.decode_www_form(req.query_string || '').to_h
262
+
263
+ if query_params['state'] != @state
264
+ res.body = "状态不匹配,可能存在CSRF攻击风险"
265
+ elsif query_params['error']
266
+ res.body = "授权错误: #{query_params['error']}"
267
+ elsif query_params['code']
268
+ code = query_params['code']
269
+ res.body = <<-HTML
270
+ <!DOCTYPE html>
271
+ <html>
272
+ <head>
273
+ <title>飞书授权成功</title>
274
+ <style>
275
+ body { font-family: Arial, sans-serif; text-align: center; padding: 40px; }
276
+ .container { max-width: 600px; margin: 0 auto; }
277
+ .success { color: #4CAF50; }
278
+ </style>
279
+ </head>
280
+ <body>
281
+ <div class="container">
282
+ <h1 class="success">授权成功!</h1>
283
+ <p>飞书OAuth授权已完成,您现在可以关闭此窗口并返回命令行。</p>
284
+ </div>
285
+ </body>
286
+ </html>
287
+ HTML
288
+ else
289
+ res.body = "未获取到授权码"
290
+ end
291
+
292
+ server.shutdown
293
+ end
294
+
295
+ # 捕获Ctrl+C以允许用户中断
296
+ trap('INT') { server.shutdown }
297
+
298
+ # 在线程中运行服务器,最多等待2分钟
299
+ thread = Thread.new { server.start }
300
+ begin
301
+ thread.join(120) # 最多等待2分钟
302
+ rescue Timeout::Error
303
+ puts "授权超时"
304
+ server.shutdown
305
+ end
306
+
307
+ code
308
+ end
309
+
310
+
311
+ end
312
+
313
+ end
@@ -1,10 +1,10 @@
1
-
2
1
  require 'uri'
3
2
  require 'json'
4
3
  require 'faraday'
5
4
  require 'securerandom'
6
5
  require 'pindo/base/aeshelper'
7
6
  require 'typhoeus'
7
+ require 'thread'
8
8
 
9
9
  module Pindo
10
10
 
@@ -44,6 +44,13 @@ module Pindo
44
44
  @pgyer_aes_key = config_json["pgyerapps_aes_key"]
45
45
 
46
46
  @token = load_token
47
+
48
+ # 添加互斥锁用于线程安全
49
+ @upload_eTags_mutex = Mutex.new
50
+ @tasks_queue_mutex = Mutex.new
51
+ @active_tasks_mutex = Mutex.new
52
+ @upload_failed_mutex = Mutex.new # 为上传失败状态添加互斥锁
53
+ @upload_failed = false
47
54
 
48
55
  rescue => error
49
56
  raise Informative, "PgyerUploadClient 初始化失败!"
@@ -72,9 +79,12 @@ module Pindo
72
79
 
73
80
  def upload_file(binary_file:nil, isAttach:false)
74
81
 
82
+ raise Informative, "上传文件不能为空" if binary_file.nil? || !File.exist?(binary_file)
83
+
75
84
  @upload_binary_file = binary_file
76
85
  @file_size = File.size(@upload_binary_file)
77
86
  @progress_bar = PgyerUploadProgressBar.new(upload_total_size:@file_size)
87
+ @upload_failed = false # 重置上传失败标志
78
88
 
79
89
  extension = File.extname(@upload_binary_file)
80
90
  filename = File.basename(@upload_binary_file)
@@ -104,135 +114,231 @@ module Pindo
104
114
 
105
115
  upload_result = nil
106
116
 
107
- file_size_param = 1.00 * @file_size / 1024 /1024
108
- result_data = post_upload_url_req(upload_path_key:upload_path_key, file_ceil_size:file_size_param.ceil)
109
-
110
- upload_id= result_data["data"]["uploadId"]
111
- @upload_params_list = result_data["data"]["uploadParamsList"]
112
- task_num = @upload_params_list.length
113
- retry_count = 5
114
- if task_num < 2
115
- task_num = 2
116
- end
117
-
118
- if task_num > 100
119
- task_num = 100
120
- end
121
-
122
- puts "线程个数: #{task_num}"
123
- puts "重试次数: #{retry_count}"
124
- puts
125
-
126
- Funlog.instance.fancyinfo_start("开始上传...")
127
- @upload_eTags = []
117
+ begin
118
+ file_size_param = 1.00 * @file_size / 1024 /1024
119
+ result_data = post_upload_url_req(upload_path_key:upload_path_key, file_ceil_size:file_size_param.ceil)
120
+
121
+ if result_data.nil? || !result_data.has_key?("data") || !result_data["data"].has_key?("uploadId")
122
+ raise Informative, "获取上传ID失败,请检查网络或服务器状态"
123
+ end
124
+
125
+ upload_id = result_data["data"]["uploadId"]
126
+ # 创建统一的任务队列
127
+ @tasks_queue = Queue.new
128
+ @worker_threads = []
129
+ upload_params_list = result_data["data"]["uploadParamsList"]
130
+ upload_item_num = upload_params_list.length
131
+ @expected_parts = upload_item_num # 保存预期的分片数量
132
+ task_num = upload_item_num
133
+ retry_count = 5
134
+
135
+ # 合理限制线程数
136
+ if task_num < 2
137
+ task_num = 2
138
+ end
128
139
 
129
- # single_task_upload_data_req( )
140
+ if task_num > 30
141
+ task_num = 30
142
+ end
130
143
 
144
+ # 根据系统CPU核心数自动调整线程数
145
+ available_cores = Etc.respond_to?(:nprocessors) ? Etc.nprocessors : 4
146
+ task_num = [task_num, available_cores * 2].min
147
+
148
+ # 设置重试次数并将所有任务加入队列
149
+ upload_params_list.each do |item|
150
+ item["retryCount"] = retry_count
151
+ @tasks_queue.push(item)
152
+ end
131
153
 
132
- multi_task_upload_data_req(task_num:task_num, retry_count:retry_count)
154
+ puts "切分个数: #{upload_item_num}"
155
+ puts "线程个数: #{task_num}"
156
+ puts "重试次数: #{retry_count}"
157
+ puts
158
+
159
+ Funlog.instance.fancyinfo_start("开始上传...")
160
+ @upload_eTags = []
161
+ @active_tasks = 0 # 跟踪活动任务数量
162
+
163
+ continuous_upload_data_req(concurrency:task_num)
164
+
165
+ # 检查上传是否全部成功
166
+ if upload_failed? || @upload_eTags.length != @expected_parts
167
+ upload_result = nil
168
+ Funlog.instance.fancyinfo_error("文件#{@upload_binary_file} 上传失败! 😭😭😭")
169
+ return upload_result
170
+ end
133
171
 
134
- result_data = post_upload_finish_req(upload_path_key:upload_path_key, upload_id:upload_id, eTags:@upload_eTags)
172
+ result_data = post_upload_finish_req(upload_path_key:upload_path_key, upload_id:upload_id, eTags:@upload_eTags)
135
173
 
136
- if result_data["code"] == 200
137
- upload_result = upload_path_key
138
- Funlog.instance.fancyinfo_success("文件#{@upload_binary_file} 上传成功! 😎😎😎")
139
- else
140
- upload_result = nil
141
- Funlog.instance.fancyinfo_error("文件#{@upload_binary_file} 上传失败! 😭😭😭")
174
+ if result_data && result_data["code"] == 200
175
+ upload_result = upload_path_key
176
+ Funlog.instance.fancyinfo_success("文件#{@upload_binary_file} 上传成功! 😎😎😎")
177
+ else
178
+ upload_result = nil
179
+ error_msg = result_data && result_data["msg"] ? result_data["msg"] : "未知错误"
180
+ Funlog.instance.fancyinfo_error("文件#{@upload_binary_file} 上传失败: #{error_msg} 😭😭😭")
181
+ end
182
+
183
+ rescue => e
184
+ upload_result = nil
185
+ Funlog.instance.fancyinfo_error("文件上传过程发生异常: #{e.message} 😭😭😭")
186
+ ensure
187
+ # 确保所有工作线程都被清理
188
+ cleanup_worker_threads
142
189
  end
143
190
 
144
191
  return upload_result
145
192
 
146
193
  end
147
194
 
148
-
149
- def multi_task_upload_data_req(task_num:1, retry_count: 3)
150
-
151
- for i in 0..@upload_params_list.length-1 do
152
- @upload_params_list[i]["retryCount"] = retry_count
153
- end
154
-
155
- while @upload_params_list.size > 0
156
- upload_params_list_temp = []
157
- #每次最大5个线程上传
158
- for i in 1..task_num do
159
- upload_params_list_temp << @upload_params_list.shift
160
- end
161
- hydra = Typhoeus::Hydra.new
162
- while upload_params_list_temp.size > 0
163
- upload_params_item = upload_params_list_temp.shift
164
- unless upload_params_item.nil?
165
- single_task_upload_part_data_req(upload_params_item:upload_params_item, hydra_handle:hydra)
166
- end
167
- end
168
- hydra.run
169
- end
170
-
195
+ # 安全地检查上传失败状态
196
+ def upload_failed?
197
+ @upload_failed_mutex.synchronize { @upload_failed }
171
198
  end
172
-
173
- def single_task_upload_data_req()
174
-
175
- for i in 0..@upload_params_list.length-1 do
176
- @upload_params_list[i]["retryCount"] = 3
199
+
200
+ # 安全地设置上传失败状态
201
+ def set_upload_failed(error_msg = nil)
202
+ @upload_failed_mutex.synchronize do
203
+ @upload_failed = true
204
+ Funlog.instance.fancyinfo_error("上传失败: #{error_msg}") if error_msg
177
205
  end
178
-
179
- while @upload_params_list.size > 0
180
- upload_params_item = @upload_params_list.shift
181
- single_task_upload_part_data_req(upload_params_item:upload_params_item)
206
+ end
207
+
208
+ # 清理所有工作线程
209
+ def cleanup_worker_threads
210
+ @worker_threads.each do |thread|
211
+ # 尝试安全终止线程
212
+ thread.exit if thread.alive?
182
213
  end
183
-
184
-
185
- return @eTags
214
+ @worker_threads.clear
186
215
  end
187
216
 
188
- def single_task_upload_part_data_req(upload_params_item:nil, hydra_handle:nil)
189
-
217
+ def continuous_upload_data_req(concurrency:1)
218
+ # 初始化活动任务计数和条件变量
219
+ @active_tasks = 0
220
+ @task_complete_cv = ConditionVariable.new
221
+
222
+ # 初始化连续上传,最多启动concurrency个并发任务
223
+ start_tasks = [concurrency, @tasks_queue.size].min
224
+ start_tasks.times { schedule_next_task }
225
+
226
+ # 设置超时保护
227
+ timeout_seconds = 300 # 5分钟超时
228
+ start_time = Time.now
229
+
230
+ # 等待所有任务完成
231
+ @tasks_queue_mutex.synchronize do
232
+ while (@active_tasks > 0 || !@tasks_queue.empty?) && !upload_failed?
233
+ # 添加超时保护
234
+ remaining_time = timeout_seconds - (Time.now - start_time)
235
+ if remaining_time <= 0
236
+ set_upload_failed("上传任务超时")
237
+ break
238
+ end
239
+
240
+ # 等待任务完成通知,最多等待30秒
241
+ @task_complete_cv.wait(@tasks_queue_mutex, [remaining_time, 30].min)
242
+ end
243
+ end
244
+
245
+ # 检查所有分片是否都上传成功
246
+ if @upload_eTags.length != @expected_parts && !upload_failed?
247
+ set_upload_failed("部分分片上传失败,已上传#{@upload_eTags.length}/#{@expected_parts}")
248
+ end
249
+ end
250
+
251
+ def schedule_next_task
252
+ # 检查是否应该停止调度
253
+ return if upload_failed?
254
+
255
+ # 尝试从队列中获取下一个任务
256
+ @tasks_queue_mutex.synchronize do
257
+ unless @tasks_queue.empty?
258
+ upload_params_item = @tasks_queue.pop
259
+ @active_tasks_mutex.synchronize { @active_tasks += 1 }
260
+
261
+ # 异步处理任务,不阻塞主线程
262
+ worker_thread = Thread.new do
263
+ begin
264
+ process_upload_task(upload_params_item)
265
+ rescue => e
266
+ # 捕获并记录任务处理过程中的异常
267
+ set_upload_failed("处理分片#{upload_params_item["partNo"]}时出错: #{e.message}")
268
+ ensure
269
+ # 任务完成后,减少活动任务计数并通知等待线程
270
+ @active_tasks_mutex.synchronize { @active_tasks -= 1 }
271
+
272
+ # 如果队列不为空,调度下一个任务
273
+ schedule_next_task if !upload_failed?
274
+
275
+ # 通知等待线程任务已完成
276
+ @tasks_queue_mutex.synchronize { @task_complete_cv.broadcast }
277
+ end
278
+ end
279
+
280
+ # 保存线程引用以便后续清理
281
+ @worker_threads << worker_thread
282
+ end
283
+ end
284
+ end
285
+
286
+ def process_upload_task(upload_params_item)
190
287
  upload_url = upload_params_item["signedUrl"]
191
288
  part_no = upload_params_item["partNo"]
192
-
289
+
193
290
  file_size_ele = 1024 * 1024 * 5 #5M
194
- start_position = file_size_ele * (part_no -1)
291
+ start_position = file_size_ele * (part_no - 1)
195
292
  if part_no * file_size_ele > @file_size
196
- read_length = @file_size - start_position
293
+ read_length = @file_size - start_position
197
294
  else
198
- read_length = file_size_ele
295
+ read_length = file_size_ele
199
296
  end
200
-
297
+
201
298
  file = File.open(@upload_binary_file, "rb")
202
- file.seek(start_position)
203
- put_data = file.read(read_length)
204
-
205
- request = create_req(upload_url:upload_url, body_data:put_data, read_length:read_length)
206
- request.on_progress do |dltotal, dlnow, ultotal, ulnow|
207
- if ulnow
208
- @progress_bar.update_upload_index(upload_part:part_no, upload_size:ulnow)
209
- @progress_bar.update_upload_progress()
299
+ begin
300
+ file.seek(start_position)
301
+ put_data = file.read(read_length)
302
+
303
+ request = create_req(upload_url:upload_url, body_data:put_data, read_length:read_length)
304
+
305
+ # 设置上传进度回调
306
+ upload_size_last = 0
307
+ request.on_progress do |dltotal, dlnow, ultotal, ulnow|
308
+ if ulnow && ulnow > upload_size_last
309
+ upload_size_last = ulnow
310
+ @progress_bar.update_upload_index(upload_part:part_no, upload_size:ulnow)
311
+ @progress_bar.update_upload_progress()
312
+ end
210
313
  end
211
- end
212
-
213
- request.on_complete do |response|
314
+
315
+ # 设置请求超时
316
+ request.options[:timeout] = 300 # 5分钟超时
317
+
318
+ # 执行请求并等待完成
319
+ response = request.run
320
+
321
+ # 处理响应结果
214
322
  if response.success?
215
- @progress_bar.complete_upload_index(upload_part:part_no, complete_size:read_length)
216
- etag = response.headers["ETag"]
217
- eTag_item = { partNumber: part_no, tag: etag}
218
- @upload_eTags << eTag_item
323
+ @progress_bar.complete_upload_index(upload_part:part_no, complete_size:read_length)
324
+ etag = response.headers["ETag"]
325
+ if etag.nil? || etag.empty?
326
+ raise "服务器返回的ETag为空"
327
+ end
328
+ eTag_item = { partNumber: part_no, tag: etag}
329
+ @upload_eTags_mutex.synchronize { @upload_eTags << eTag_item }
219
330
  else
220
- @progress_bar.delete_upload_index(upload_part:part_no)
221
- upload_params_item["retryCount"] = upload_params_item["retryCount"] - 1
222
- if upload_params_item["retryCount"] > 0
223
- # @upload_params_list.push(upload_params_item)
224
- single_task_upload_part_data_req(upload_params_item:upload_params_item, hydra_handle:hydra_handle)
225
- else
226
- Funlog.instance.fancyinfo_error("文件#{@upload_binary_file} 上传失败! 😭😭😭")
227
- raise Informative, "上传文件失败"
228
- end
331
+ @progress_bar.delete_upload_index(upload_part:part_no)
332
+ upload_params_item["retryCount"] = upload_params_item["retryCount"] - 1
333
+ if upload_params_item["retryCount"] > 0
334
+ # 重试任务
335
+ @tasks_queue_mutex.synchronize { @tasks_queue.push(upload_params_item) }
336
+ else
337
+ set_upload_failed("文件#{@upload_binary_file} 分片#{part_no}上传失败: HTTP #{response.code}")
338
+ end
229
339
  end
230
- end
231
-
232
- if hydra_handle.nil?
233
- request.run
234
- else
235
- hydra_handle.queue(request)
340
+ ensure
341
+ file.close
236
342
  end
237
343
  end
238
344
 
@@ -284,26 +390,30 @@ module Pindo
284
390
  tags:eTags
285
391
  }
286
392
 
287
- con = HttpClient.create_instance_with_proxy
288
- res = con.post do |req|
289
- req.url boss_url
290
- req.headers['Content-Type'] = 'application/json'
291
- req.headers['token'] = @token["token"]
292
- req.body = body_params.to_json
293
- end
393
+ begin
394
+ con = HttpClient.create_instance_with_proxy
395
+ res = con.post do |req|
396
+ req.url boss_url
397
+ req.headers['Content-Type'] = 'application/json'
398
+ req.headers['token'] = @token["token"]
399
+ req.body = body_params.to_json
400
+ req.options.timeout = 120 # 设置2分钟超时
401
+ end
294
402
 
403
+ result_data = nil
404
+ if !res.body.nil?
405
+ result_data = JSON.parse(res.body)
406
+ end
295
407
 
296
- result_data = nil
297
- if !res.body.nil?
298
- result_data = JSON.parse(res.body)
408
+ return result_data
409
+ rescue => e
410
+ Funlog.instance.fancyinfo_error("完成上传请求失败: #{e.message}")
411
+ return nil
299
412
  end
300
-
301
- return result_data
302
-
303
413
  end
304
414
 
305
415
 
306
- def post_upload_url_req(upload_path_key:nil, file_ceil_size:nil)
416
+ def post_upload_url_req(upload_path_key:nil, file_ceil_size:nil)
307
417
 
308
418
  boss_url = @baseurl + @request_config["multi_signed_url_upload"]
309
419
 
@@ -313,22 +423,26 @@ module Pindo
313
423
  fileSize:file_ceil_size
314
424
  }
315
425
 
316
- con = HttpClient.create_instance_with_proxy
317
- res = con.post do |req|
318
- req.url boss_url
319
- req.headers['Content-Type'] = 'application/json'
320
- req.headers['token'] = @token["token"]
321
- req.body = body_params.to_json
322
- end
426
+ begin
427
+ con = HttpClient.create_instance_with_proxy
428
+ res = con.post do |req|
429
+ req.url boss_url
430
+ req.headers['Content-Type'] = 'application/json'
431
+ req.headers['token'] = @token["token"]
432
+ req.body = body_params.to_json
433
+ req.options.timeout = 60 # 设置1分钟超时
434
+ end
323
435
 
436
+ result_data = nil
437
+ if !res.body.nil?
438
+ result_data = JSON.parse(res.body)
439
+ end
324
440
 
325
- result_data = nil
326
- if !res.body.nil?
327
- result_data = JSON.parse(res.body)
441
+ return result_data
442
+ rescue => e
443
+ Funlog.instance.fancyinfo_error("获取上传URL失败: #{e.message}")
444
+ return nil
328
445
  end
329
-
330
- return result_data
331
-
332
446
  end
333
447
 
334
448
  class PgyerUploadProgressBar
@@ -341,50 +455,58 @@ module Pindo
341
455
  attr_accessor :is_done
342
456
 
343
457
  def initialize(upload_total_size:nil, draw_char:'>')
344
-
345
-
346
458
  @upload_total_size = upload_total_size
347
459
  @draw_char = draw_char
348
460
  @last_update_time = (Time.now.to_f * 1000).to_i #毫秒
349
461
 
350
462
  @complete_size = 0
351
463
  @update_ing_size = {}
352
-
353
464
  @is_done = false
465
+
466
+ # 添加互斥锁来保护进度条更新
467
+ @mutex = Mutex.new
354
468
  end
355
469
 
356
470
  def update_upload_index(upload_part:nil, upload_size:nil)
357
- @update_ing_size[upload_part] = upload_size
471
+ @mutex.synchronize do
472
+ @update_ing_size[upload_part] = upload_size
473
+ end
358
474
  end
359
475
 
360
476
  def delete_upload_index(upload_part:nil)
361
- @update_ing_size[upload_part] = 0
477
+ @mutex.synchronize do
478
+ @update_ing_size[upload_part] = 0
479
+ end
362
480
  end
363
481
 
364
482
  def complete_upload_index(upload_part:nil, complete_size:nil)
365
- @complete_size = @complete_size + complete_size
366
- @update_ing_size[upload_part] = 0
483
+ @mutex.synchronize do
484
+ @complete_size = @complete_size + complete_size
485
+ @update_ing_size[upload_part] = 0
486
+ end
367
487
  end
368
488
 
369
489
  def update_upload_progress()
370
490
  time_now = (Time.now.to_f * 1000).to_i #毫秒
371
491
  if time_now - @last_update_time > 80
372
- @last_update_time = time_now
373
- total_num = @upload_total_size
374
- index_num = @complete_size
375
- @update_ing_size.each do |key, value|
376
- index_num = index_num + value
377
- end
378
-
379
- progress_str = sprintf("%.2f", 100.0 * index_num / total_num )
380
- total_size = sprintf("%.2f", 1.00 * total_num / 1024 /1024 )
381
- upload_size = sprintf("%.2f", 1.00 * index_num / 1024 /1024 )
382
- index = 40.0 * index_num / total_num
383
- upload_message = "已上传:#{upload_size}MB|#{progress_str}\%【" + (@draw_char * (index/1).floor).ljust(40.0, '_') + "】Total:#{total_size}MB"
384
- Funlog.instance.fancyinfo_update(upload_message)
385
- if index_num == total_num && !@is_done
386
- @is_done = true
387
- Funlog.instance.fancyinfo_success(upload_message)
492
+ @mutex.synchronize do
493
+ @last_update_time = time_now
494
+ total_num = @upload_total_size
495
+ index_num = @complete_size
496
+ @update_ing_size.each do |key, value|
497
+ index_num = index_num + value
498
+ end
499
+
500
+ progress_str = sprintf("%.2f", 100.0 * index_num / total_num )
501
+ total_size = sprintf("%.2f", 1.00 * total_num / 1024 /1024 )
502
+ upload_size = sprintf("%.2f", 1.00 * index_num / 1024 /1024 )
503
+ index = 40.0 * index_num / total_num
504
+ upload_message = "已上传:#{upload_size}MB|#{progress_str}\%【" + (@draw_char * (index/1).floor).ljust(40.0, '_') + "】Total:#{total_size}MB"
505
+ Funlog.instance.fancyinfo_update(upload_message)
506
+ if index_num == total_num && !@is_done
507
+ @is_done = true
508
+ Funlog.instance.fancyinfo_success(upload_message)
509
+ end
388
510
  end
389
511
  end
390
512
  end
data/lib/pindo/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module Pindo
2
2
 
3
- VERSION = "5.0.5"
3
+ VERSION = "5.0.6"
4
4
 
5
5
  class VersionCheck
6
6
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pindo
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.5
4
+ version: 5.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - wade
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-20 00:00:00.000000000 Z
10
+ date: 2025-04-16 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: claide
@@ -133,16 +133,22 @@ dependencies:
133
133
  name: faraday-retry
134
134
  requirement: !ruby/object:Gem::Requirement
135
135
  requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '1.0'
136
139
  - - ">="
137
140
  - !ruby/object:Gem::Version
138
- version: '0'
141
+ version: 1.0.3
139
142
  type: :runtime
140
143
  prerelease: false
141
144
  version_requirements: !ruby/object:Gem::Requirement
142
145
  requirements:
146
+ - - "~>"
147
+ - !ruby/object:Gem::Version
148
+ version: '1.0'
143
149
  - - ">="
144
150
  - !ruby/object:Gem::Version
145
- version: '0'
151
+ version: 1.0.3
146
152
  - !ruby/object:Gem::Dependency
147
153
  name: typhoeus
148
154
  requirement: !ruby/object:Gem::Requirement
@@ -183,6 +189,26 @@ dependencies:
183
189
  - - ">="
184
190
  - !ruby/object:Gem::Version
185
191
  version: 1.15.4
192
+ - !ruby/object:Gem::Dependency
193
+ name: webrick
194
+ requirement: !ruby/object:Gem::Requirement
195
+ requirements:
196
+ - - "~>"
197
+ - !ruby/object:Gem::Version
198
+ version: '1.8'
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: 1.8.1
202
+ type: :runtime
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - "~>"
207
+ - !ruby/object:Gem::Version
208
+ version: '1.8'
209
+ - - ">="
210
+ - !ruby/object:Gem::Version
211
+ version: 1.8.1
186
212
  - !ruby/object:Gem::Dependency
187
213
  name: bundler
188
214
  requirement: !ruby/object:Gem::Requirement
@@ -207,30 +233,36 @@ dependencies:
207
233
  name: rake
208
234
  requirement: !ruby/object:Gem::Requirement
209
235
  requirements:
236
+ - - "~>"
237
+ - !ruby/object:Gem::Version
238
+ version: '13.0'
210
239
  - - ">="
211
240
  - !ruby/object:Gem::Version
212
- version: '0'
241
+ version: 13.0.6
213
242
  type: :development
214
243
  prerelease: false
215
244
  version_requirements: !ruby/object:Gem::Requirement
216
245
  requirements:
246
+ - - "~>"
247
+ - !ruby/object:Gem::Version
248
+ version: '13.0'
217
249
  - - ">="
218
250
  - !ruby/object:Gem::Version
219
- version: '0'
251
+ version: 13.0.6
220
252
  - !ruby/object:Gem::Dependency
221
253
  name: rspec
222
254
  requirement: !ruby/object:Gem::Requirement
223
255
  requirements:
224
- - - ">="
256
+ - - "~>"
225
257
  - !ruby/object:Gem::Version
226
- version: '0'
258
+ version: '3.12'
227
259
  type: :development
228
260
  prerelease: false
229
261
  version_requirements: !ruby/object:Gem::Requirement
230
262
  requirements:
231
- - - ">="
263
+ - - "~>"
232
264
  - !ruby/object:Gem::Version
233
- version: '0'
265
+ version: '3.12'
234
266
  description: easy work for deploy, dev
235
267
  email:
236
268
  - wade@gmail.com
@@ -258,6 +290,7 @@ files:
258
290
  - lib/pindo/client/feishuclient.rb
259
291
  - lib/pindo/client/giteeclient.rb
260
292
  - lib/pindo/client/httpclient.rb
293
+ - lib/pindo/client/pgyer_feishu_oauth_cli.rb
261
294
  - lib/pindo/client/pgyerclient.rb
262
295
  - lib/pindo/client/pgyeruploadclient.rb
263
296
  - lib/pindo/client/tgateclient.rb