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 +4 -4
- data/lib/pindo/client/pgyer_feishu_oauth_cli.rb +313 -0
- data/lib/pindo/client/pgyeruploadclient.rb +275 -153
- data/lib/pindo/version.rb +1 -1
- metadata +43 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: da7db67cae593054d94da87ec555ccd0d6ed2691ca9d534729a386b4cc55f360
|
4
|
+
data.tar.gz: 25b281165febd17ac56dfcadd64dcf2e55674e326b28575ff3952007b7dbdc2d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
172
|
+
result_data = post_upload_finish_req(upload_path_key:upload_path_key, upload_id:upload_id, eTags:@upload_eTags)
|
135
173
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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
|
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
|
-
|
174
|
-
|
175
|
-
|
176
|
-
@
|
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
|
-
|
180
|
-
|
181
|
-
|
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
|
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
|
-
|
293
|
+
read_length = @file_size - start_position
|
197
294
|
else
|
198
|
-
|
295
|
+
read_length = file_size_ele
|
199
296
|
end
|
200
|
-
|
297
|
+
|
201
298
|
file = File.open(@upload_binary_file, "rb")
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
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
|
-
|
212
|
-
|
213
|
-
|
314
|
+
|
315
|
+
# 设置请求超时
|
316
|
+
request.options[:timeout] = 300 # 5分钟超时
|
317
|
+
|
318
|
+
# 执行请求并等待完成
|
319
|
+
response = request.run
|
320
|
+
|
321
|
+
# 处理响应结果
|
214
322
|
if response.success?
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
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
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
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
|
-
|
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
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
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
|
-
|
297
|
-
|
298
|
-
|
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
|
-
|
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
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
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
|
-
|
326
|
-
|
327
|
-
|
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
|
-
@
|
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
|
-
@
|
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
|
-
@
|
366
|
-
|
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
|
-
@
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
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
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.
|
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-
|
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:
|
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:
|
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:
|
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:
|
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: '
|
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: '
|
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
|