jpsclient 2.7.3 → 2.7.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/jpsclient/api/file.rb +27 -6
- data/lib/jpsclient/api/project_package.rb +0 -1
- data/lib/jpsclient/upload/upload_client.rb +62 -16
- data/lib/jpsclient/upload/upload_config.rb +24 -0
- data/lib/jpsclient/upload/upload_media_client.rb +153 -37
- data/lib/jpsclient/utils/content_type.rb +43 -0
- data/lib/jpsclient/version.rb +1 -1
- data/lib/jpsclient.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0d61ea8f97063390fa28c05be9b39979a18f9eb671aead52d2f075f9be04ee8c
|
|
4
|
+
data.tar.gz: 05f18f0cb982cca4bc27233aaef5cdf2456138723de40ab3d89ea5909995b0b1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 45ce3c0e73d8384391fece49e0b8256697a99b371aa98859fd6e649baadd527a32ccf1af93b4db14545f7a769000f9484d629bd908bf4382656977bea70026b7
|
|
7
|
+
data.tar.gz: 6612908940d4ce6a89e4503b9cf54529fcbb9a9d9c1bb5f6dd4a51814ab8c41aee69165cf25e56d612bfa68dadbb3892044504c666299f461ad13ee92024689e
|
data/lib/jpsclient/api/file.rb
CHANGED
|
@@ -6,7 +6,10 @@ module JPSClient
|
|
|
6
6
|
|
|
7
7
|
# 初始化多部分上传
|
|
8
8
|
def init_file_multipart(s3_key:, content_type: "", upload_config:)
|
|
9
|
-
|
|
9
|
+
config = @request_config && @request_config["file_init_multipart"]
|
|
10
|
+
raise JPSClient::ExceptionError, "Missing config for file_init_multipart" unless config && config["url"]
|
|
11
|
+
|
|
12
|
+
path = config["url"]
|
|
10
13
|
|
|
11
14
|
body_params = {
|
|
12
15
|
s3Key: s3_key,
|
|
@@ -20,14 +23,23 @@ module JPSClient
|
|
|
20
23
|
end
|
|
21
24
|
|
|
22
25
|
# 获取简单上传预签名URL(用于小文件直接上传,使用 media 配置)
|
|
26
|
+
#
|
|
27
|
+
# 传了 contentType 时服务端会把 content-type 纳入签名(X-Amz-SignedHeaders 含
|
|
28
|
+
# content-type),调用方 PUT 时必须发送完全相同的值,否则 S3 返回 403
|
|
29
|
+
#
|
|
23
30
|
# @param s3_key [String] S3 文件路径
|
|
31
|
+
# @param content_type [String] 文件 Content-Type,决定对象最终类型
|
|
24
32
|
# @param upload_config [UploadConfig] 上传配置
|
|
25
33
|
# @return [Hash] 包含预签名URL的响应
|
|
26
|
-
def get_simple_sign_url(s3_key:, upload_config:)
|
|
27
|
-
|
|
34
|
+
def get_simple_sign_url(s3_key:, content_type: "", upload_config:)
|
|
35
|
+
config = @request_config && @request_config["file_sign_url"]
|
|
36
|
+
raise JPSClient::ExceptionError, "Missing config for file_sign_url" unless config && config["url"]
|
|
37
|
+
|
|
38
|
+
path = config["url"]
|
|
28
39
|
|
|
29
40
|
body_params = {
|
|
30
41
|
s3Key: s3_key,
|
|
42
|
+
contentType: content_type,
|
|
31
43
|
uploadType: upload_config.upload_type,
|
|
32
44
|
bucketName: upload_config.media_bucket_name,
|
|
33
45
|
region: upload_config.media_region
|
|
@@ -37,11 +49,17 @@ module JPSClient
|
|
|
37
49
|
end
|
|
38
50
|
|
|
39
51
|
# 获取分片预签名URL
|
|
40
|
-
|
|
41
|
-
|
|
52
|
+
#
|
|
53
|
+
# @param content_type [String] 文件 Content-Type,与 init_file_multipart 保持一致
|
|
54
|
+
def get_file_sign_url(s3_key:, upload_id:, part_id:, content_type: "", upload_config:)
|
|
55
|
+
config = @request_config && @request_config["file_sign_url"]
|
|
56
|
+
raise JPSClient::ExceptionError, "Missing config for file_sign_url" unless config && config["url"]
|
|
57
|
+
|
|
58
|
+
path = config["url"]
|
|
42
59
|
|
|
43
60
|
body_params = {
|
|
44
61
|
s3Key: s3_key,
|
|
62
|
+
contentType: content_type,
|
|
45
63
|
uploadType: upload_config.upload_type,
|
|
46
64
|
uploadId: upload_id,
|
|
47
65
|
partId: part_id,
|
|
@@ -54,7 +72,10 @@ module JPSClient
|
|
|
54
72
|
|
|
55
73
|
# 完成多部分上传
|
|
56
74
|
def complete_file_multipart(s3_key:, upload_id:, upload_config:)
|
|
57
|
-
|
|
75
|
+
config = @request_config && @request_config["file_complete_multipart"]
|
|
76
|
+
raise JPSClient::ExceptionError, "Missing config for file_complete_multipart" unless config && config["url"]
|
|
77
|
+
|
|
78
|
+
path = config["url"]
|
|
58
79
|
|
|
59
80
|
body_params = {
|
|
60
81
|
s3Key: s3_key,
|
|
@@ -61,7 +61,6 @@ module JPSClient
|
|
|
61
61
|
# @option params [String] :projectPackageId 项目包ID
|
|
62
62
|
# @return [Hash] API响应
|
|
63
63
|
def upload_nuget_package(projectId:nil, params:nil)
|
|
64
|
-
puts "========1111 upload_nuget_package" if JPSClient.debug?
|
|
65
64
|
config = @request_config && @request_config["project_package_upload_nuget"]
|
|
66
65
|
raise JPSClient::ExceptionError, "Missing config for project_package_upload_nuget" unless config && config["url"]
|
|
67
66
|
path = config["url"]
|
|
@@ -10,6 +10,7 @@ require 'jpsclient/http/http_client'
|
|
|
10
10
|
require 'jpsclient/upload/upload_config'
|
|
11
11
|
require 'jpsclient/upload/upload_progress'
|
|
12
12
|
require 'jpsclient/utils/logger'
|
|
13
|
+
require 'jpsclient/utils/content_type'
|
|
13
14
|
|
|
14
15
|
module JPSClient
|
|
15
16
|
|
|
@@ -20,8 +21,12 @@ module JPSClient
|
|
|
20
21
|
attr_accessor :file_size
|
|
21
22
|
attr_accessor :progress_bar
|
|
22
23
|
attr_reader :jps_client
|
|
24
|
+
attr_reader :s3_key # 本次上传使用的 S3 Key,供调用方自行拼装访问地址
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
# @param jps_client [Client] JPS 客户端实例
|
|
27
|
+
# @param upload_config [UploadConfig, nil] 上传配置,传入时覆盖客户端配置。
|
|
28
|
+
# 媒体大文件走分片上传时由 UploadMediaClient 注入 media 桶配置
|
|
29
|
+
def initialize(jps_client, upload_config: nil)
|
|
25
30
|
raise ExceptionError, "必须提供 Client 实例" unless jps_client
|
|
26
31
|
|
|
27
32
|
@jps_client = jps_client
|
|
@@ -30,7 +35,7 @@ module JPSClient
|
|
|
30
35
|
config_json = @jps_client.config_json
|
|
31
36
|
|
|
32
37
|
# 加载上传配置
|
|
33
|
-
@upload_config = UploadConfig.from_json(config_json["upload_config"])
|
|
38
|
+
@upload_config = upload_config || UploadConfig.from_json(config_json["upload_config"])
|
|
34
39
|
unless @upload_config && @upload_config.valid?
|
|
35
40
|
raise ExceptionError, "上传配置无效或不完整"
|
|
36
41
|
end
|
|
@@ -66,6 +71,7 @@ module JPSClient
|
|
|
66
71
|
# 使用配置中的路径前缀
|
|
67
72
|
path_prefix = isAttach ? @upload_config.attach_url : @upload_config.default_url
|
|
68
73
|
s3_key = "#{path_prefix}#{file_uuid}#{extension}"
|
|
74
|
+
@s3_key = s3_key
|
|
69
75
|
|
|
70
76
|
# 计算文件大小和分片信息
|
|
71
77
|
total_mb = sprintf("%.2f", @file_size / 1024.0 / 1024.0)
|
|
@@ -79,17 +85,36 @@ module JPSClient
|
|
|
79
85
|
puts "文件路径: #{@upload_binary_file}"
|
|
80
86
|
puts "文件大小: #{total_mb}M"
|
|
81
87
|
puts "上传路径: #{s3_key}"
|
|
88
|
+
puts "存储桶: #{@upload_config.bucket_name}"
|
|
89
|
+
puts "区域: #{@upload_config.region}"
|
|
82
90
|
puts
|
|
83
91
|
puts "切片大小: #{@upload_config.chunk_size_mb}MB"
|
|
84
92
|
|
|
85
93
|
upload_result = nil
|
|
86
94
|
|
|
95
|
+
# contentType 决定 CreateMultipartUpload 后对象的最终类型,按扩展名推导,
|
|
96
|
+
# 让 apk/zip 等安装包不再一律落成 application/octet-stream。
|
|
97
|
+
# 分片 PUT 也发同一个值:预签名 URL 若把 content-type 纳入签名,两边不一致会 403
|
|
98
|
+
@content_type = ContentType.for_file(@upload_binary_file)
|
|
99
|
+
content_type = @content_type
|
|
100
|
+
|
|
101
|
+
if JPSClient.debug?
|
|
102
|
+
puts [
|
|
103
|
+
"[JPS_DEBUG] 分片上传参数: #{filename}",
|
|
104
|
+
"[JPS_DEBUG] s3Key: #{s3_key}",
|
|
105
|
+
"[JPS_DEBUG] bucketName: #{@upload_config.bucket_name}",
|
|
106
|
+
"[JPS_DEBUG] region: #{@upload_config.region}",
|
|
107
|
+
"[JPS_DEBUG] uploadType: #{@upload_config.upload_type}",
|
|
108
|
+
"[JPS_DEBUG] contentType: #{content_type}"
|
|
109
|
+
].join("\n")
|
|
110
|
+
end
|
|
111
|
+
|
|
87
112
|
begin
|
|
88
113
|
# 步骤1: 初始化多部分上传
|
|
89
114
|
Logger.instance.fancyinfo_start("初始化上传...")
|
|
90
115
|
init_result = @jps_client.init_file_multipart(
|
|
91
116
|
s3_key: s3_key,
|
|
92
|
-
content_type:
|
|
117
|
+
content_type: content_type,
|
|
93
118
|
upload_config: @upload_config
|
|
94
119
|
)
|
|
95
120
|
|
|
@@ -98,12 +123,11 @@ module JPSClient
|
|
|
98
123
|
puts "[JPS_DEBUG] init_file_multipart 返回: #{init_result.inspect}"
|
|
99
124
|
end
|
|
100
125
|
|
|
101
|
-
|
|
126
|
+
upload_id = multipart_upload_id(init_result)
|
|
127
|
+
if init_result.nil? || upload_id.nil?
|
|
102
128
|
raise ExceptionError, init_multipart_error_message(init_result)
|
|
103
129
|
end
|
|
104
130
|
|
|
105
|
-
upload_id = init_result["data"]["upload_id"]
|
|
106
|
-
|
|
107
131
|
# 准备分片任务队列
|
|
108
132
|
@tasks_queue = Queue.new
|
|
109
133
|
@worker_threads = []
|
|
@@ -150,7 +174,7 @@ module JPSClient
|
|
|
150
174
|
|
|
151
175
|
if complete_result && (complete_result["code"] == 0 || complete_result["code"] == 200)
|
|
152
176
|
# 返回 URL 字符串以保持向后兼容
|
|
153
|
-
upload_result = complete_result
|
|
177
|
+
upload_result = uploaded_file_url(complete_result, s3_key)
|
|
154
178
|
Logger.instance.fancyinfo_success("文件#{@upload_binary_file} 上传成功! 😎😎😎")
|
|
155
179
|
else
|
|
156
180
|
upload_result = nil
|
|
@@ -320,16 +344,17 @@ module JPSClient
|
|
|
320
344
|
s3_key: s3_key,
|
|
321
345
|
upload_id: upload_id,
|
|
322
346
|
part_id: part_no,
|
|
347
|
+
content_type: @content_type.to_s,
|
|
323
348
|
upload_config: @upload_config
|
|
324
349
|
)
|
|
325
350
|
|
|
326
|
-
|
|
351
|
+
original_url = signed_upload_url(sign_result)
|
|
352
|
+
if original_url.nil?
|
|
327
353
|
# Logger.instance.info("分片 ##{part_no} 获取上传URL失败") # 避免打断进度条
|
|
328
354
|
handle_retry(upload_params_item, "获取上传URL失败")
|
|
329
355
|
return
|
|
330
356
|
end
|
|
331
357
|
|
|
332
|
-
original_url = sign_result["data"]["url"]
|
|
333
358
|
upload_url = original_url
|
|
334
359
|
|
|
335
360
|
# 如果启用 Cloudflare 加速,转换 URL
|
|
@@ -380,7 +405,7 @@ module JPSClient
|
|
|
380
405
|
method: :put,
|
|
381
406
|
body: put_data,
|
|
382
407
|
headers: {
|
|
383
|
-
'Content-Type' =>
|
|
408
|
+
'Content-Type' => @content_type || ContentType::DEFAULT,
|
|
384
409
|
'Content-Length' => read_length.to_s
|
|
385
410
|
},
|
|
386
411
|
timeout: chunk_timeout,
|
|
@@ -431,13 +456,34 @@ module JPSClient
|
|
|
431
456
|
end
|
|
432
457
|
end
|
|
433
458
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
459
|
+
def calculate_chunk_timeout(chunk_size)
|
|
460
|
+
# 使用配置的 timeout_seconds 作为单个分片的超时时间
|
|
461
|
+
# 默认600秒(10分钟)
|
|
462
|
+
@upload_config.timeout_seconds || 600
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def multipart_upload_id(init_result)
|
|
466
|
+
init_result&.dig("data", "upload_id") || init_result&.dig("data", "uploadId")
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def signed_upload_url(sign_result)
|
|
470
|
+
data = sign_result&.dig("data")
|
|
471
|
+
return nil unless data.is_a?(Hash)
|
|
472
|
+
|
|
473
|
+
data["url"] || data["signUrl"] || data["sign_url"] ||
|
|
474
|
+
data["apiGatewayUrl"] || data["api_gateway_url"]
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def uploaded_file_url(complete_result, s3_key)
|
|
478
|
+
data = complete_result&.dig("data")
|
|
479
|
+
return s3_key unless data.is_a?(Hash)
|
|
480
|
+
|
|
481
|
+
data["url"] || data["httpUrl"] || data["http_url"] ||
|
|
482
|
+
data["apiGatewayUrl"] || data["api_gateway_url"] ||
|
|
483
|
+
data["s3Url"] || data["s3_url"] || s3_key
|
|
484
|
+
end
|
|
439
485
|
|
|
440
|
-
|
|
486
|
+
def handle_retry(upload_params_item, error_reason)
|
|
441
487
|
part_no = upload_params_item["partNo"]
|
|
442
488
|
@progress_bar.delete_upload_index(upload_part:part_no)
|
|
443
489
|
|
|
@@ -105,6 +105,30 @@ module JPSClient
|
|
|
105
105
|
return 5
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
+
# 返回一份把 media_* 提升为主字段的副本
|
|
109
|
+
#
|
|
110
|
+
# 分片上传链路(init_multipart / sign_url / complete_multipart)统一读
|
|
111
|
+
# bucket_name / region / default_url,媒体大文件要落到 media 桶,就用这份视图,
|
|
112
|
+
# 而不是就地改写配置,避免影响同一进程里的安装包上传
|
|
113
|
+
#
|
|
114
|
+
# @return [UploadConfig] 新的配置实例,原实例不变
|
|
115
|
+
def to_media_config
|
|
116
|
+
self.class.new(
|
|
117
|
+
region: @media_region,
|
|
118
|
+
bucket_name: @media_bucket_name,
|
|
119
|
+
default_url: @media_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
|
+
timeout_seconds: @timeout_seconds,
|
|
126
|
+
media_region: @media_region,
|
|
127
|
+
media_bucket_name: @media_bucket_name,
|
|
128
|
+
media_default_url: @media_default_url
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
|
|
108
132
|
# 验证配置完整性
|
|
109
133
|
# 注意:移除了对 access_key_id 和 access_key_secret 的验证
|
|
110
134
|
# 因为客户端使用预签名 URL,不直接使用 AWS 凭证
|
|
@@ -4,6 +4,7 @@ require 'thread'
|
|
|
4
4
|
require 'jpsclient/base/exception'
|
|
5
5
|
require 'jpsclient/upload/upload_config'
|
|
6
6
|
require 'jpsclient/utils/logger'
|
|
7
|
+
require 'jpsclient/utils/content_type'
|
|
7
8
|
|
|
8
9
|
module JPSClient
|
|
9
10
|
# Media 文件上传客户端
|
|
@@ -90,38 +91,13 @@ module JPSClient
|
|
|
90
91
|
@upload_failed = false
|
|
91
92
|
@upload_results = []
|
|
92
93
|
|
|
93
|
-
#
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
@active_tasks = 0
|
|
98
|
-
|
|
99
|
-
# 获取并发和重试配置
|
|
100
|
-
concurrent_workers = [@upload_config.concurrent_workers, valid_files.size].min
|
|
101
|
-
retry_count = @upload_config.max_retry_times
|
|
102
|
-
|
|
103
|
-
puts "准备上传 #{valid_files.size} 个文件"
|
|
104
|
-
puts "并发上传线程数: #{concurrent_workers}"
|
|
105
|
-
puts "失败重试次数: #{retry_count}"
|
|
106
|
-
puts
|
|
94
|
+
# 按分片大小分流:装不满一个分片的文件一次 PUT 传完,超出的走分片上传
|
|
95
|
+
# 大文件不并发,是因为分片上传自身已经并发传分片,再套一层会让线程数和进度条都失控
|
|
96
|
+
chunk_size = @upload_config.chunk_size_bytes
|
|
97
|
+
small_files, large_files = valid_files.partition { |f| File.size(f) <= chunk_size }
|
|
107
98
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
task_item = {
|
|
111
|
-
"file_path" => file_path,
|
|
112
|
-
"retry_count" => retry_count
|
|
113
|
-
}
|
|
114
|
-
@tasks_queue.push(task_item)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# 开始并发上传
|
|
118
|
-
Logger.instance.fancyinfo_start("开始上传...")
|
|
119
|
-
|
|
120
|
-
begin
|
|
121
|
-
concurrent_upload(concurrency: concurrent_workers)
|
|
122
|
-
ensure
|
|
123
|
-
cleanup_worker_threads
|
|
124
|
-
end
|
|
99
|
+
upload_small_files(small_files) if small_files.any?
|
|
100
|
+
large_files.each { |file_path| upload_large_file(file_path) }
|
|
125
101
|
|
|
126
102
|
# 收集结果
|
|
127
103
|
@upload_results.each do |item|
|
|
@@ -159,6 +135,86 @@ module JPSClient
|
|
|
159
135
|
|
|
160
136
|
private
|
|
161
137
|
|
|
138
|
+
# 并发简单上传(单个分片装得下的文件)
|
|
139
|
+
#
|
|
140
|
+
# @param file_paths [Array<String>] 文件路径列表
|
|
141
|
+
def upload_small_files(file_paths)
|
|
142
|
+
@tasks_queue = Queue.new
|
|
143
|
+
@worker_threads = []
|
|
144
|
+
@expected_files = file_paths.size
|
|
145
|
+
@active_tasks = 0
|
|
146
|
+
|
|
147
|
+
concurrent_workers = [@upload_config.concurrent_workers, file_paths.size].min
|
|
148
|
+
retry_count = @upload_config.max_retry_times
|
|
149
|
+
|
|
150
|
+
puts "准备上传 #{file_paths.size} 个文件"
|
|
151
|
+
puts "并发上传线程数: #{concurrent_workers}"
|
|
152
|
+
puts "失败重试次数: #{retry_count}"
|
|
153
|
+
puts
|
|
154
|
+
|
|
155
|
+
file_paths.each do |file_path|
|
|
156
|
+
@tasks_queue.push("file_path" => file_path, "retry_count" => retry_count)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
Logger.instance.fancyinfo_start("开始上传...")
|
|
160
|
+
|
|
161
|
+
begin
|
|
162
|
+
concurrent_upload(concurrency: concurrent_workers)
|
|
163
|
+
ensure
|
|
164
|
+
cleanup_worker_threads
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# 分片上传单个大文件(超过 chunk_size_mb 的文件)
|
|
169
|
+
#
|
|
170
|
+
# 复用安装包的分片链路(init_multipart / sign_url / complete_multipart),
|
|
171
|
+
# 但注入 media 桶配置,保证媒体文件仍然落在 media 桶、走 media 静态域名
|
|
172
|
+
#
|
|
173
|
+
# @param file_path [String] 文件路径
|
|
174
|
+
def upload_large_file(file_path)
|
|
175
|
+
file_name = File.basename(file_path)
|
|
176
|
+
puts "分片上传大文件: #{file_name}(#{(File.size(file_path) / 1024.0 / 1024.0).round(1)}MB)"
|
|
177
|
+
|
|
178
|
+
client = UploadClient.new(@jps_client, upload_config: @upload_config.to_media_config)
|
|
179
|
+
|
|
180
|
+
url = begin
|
|
181
|
+
client.upload_file(binary_file: file_path)
|
|
182
|
+
rescue => e
|
|
183
|
+
Logger.instance.fancyinfo_error("#{file_name} 分片上传异常: #{e.message}")
|
|
184
|
+
nil
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
if url.to_s.empty?
|
|
188
|
+
@results_mutex.synchronize do
|
|
189
|
+
@upload_results << {
|
|
190
|
+
"file_path" => file_path,
|
|
191
|
+
"url" => nil,
|
|
192
|
+
"success" => false,
|
|
193
|
+
"error" => "分片上传失败"
|
|
194
|
+
}
|
|
195
|
+
end
|
|
196
|
+
puts " ❌ #{file_name}"
|
|
197
|
+
return
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# complete 可能回退成 s3Key,也可能返回带 X-Amz-Signature 的预签名地址(30 分钟过期)。
|
|
201
|
+
# 这两种都不能写进 JPS 提交记录,统一改用媒体静态域名 + s3Key
|
|
202
|
+
unless static_media_url?(url)
|
|
203
|
+
url = "#{static_base_url}/#{client.s3_key.to_s.sub(%r{\A/}, '')}"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
@results_mutex.synchronize do
|
|
207
|
+
@upload_results << {
|
|
208
|
+
"file_path" => file_path,
|
|
209
|
+
"url" => url,
|
|
210
|
+
"success" => true,
|
|
211
|
+
"error" => nil
|
|
212
|
+
}
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
puts " ✅ #{file_name}"
|
|
216
|
+
end
|
|
217
|
+
|
|
162
218
|
# 并发上传控制
|
|
163
219
|
def concurrent_upload(concurrency: 1)
|
|
164
220
|
@worker_threads = []
|
|
@@ -235,19 +291,25 @@ module JPSClient
|
|
|
235
291
|
extension = File.extname(file_path)
|
|
236
292
|
s3_key = "#{@upload_config.media_default_url}#{file_uuid}#{extension}"
|
|
237
293
|
|
|
294
|
+
# Content-Type 按扩展名推导,并跟着签名请求一起发给服务端:
|
|
295
|
+
# 服务端会把它纳入签名,下面 PUT 时必须发送同一个值
|
|
296
|
+
content_type = ContentType.for_file(file_path)
|
|
297
|
+
|
|
298
|
+
debug_print_upload_params(file_name, s3_key, content_type)
|
|
299
|
+
|
|
238
300
|
# 获取预签名 URL
|
|
239
301
|
sign_result = @jps_client.get_simple_sign_url(
|
|
240
302
|
s3_key: s3_key,
|
|
303
|
+
content_type: content_type,
|
|
241
304
|
upload_config: @upload_config
|
|
242
305
|
)
|
|
243
306
|
|
|
244
|
-
|
|
307
|
+
upload_url = signed_upload_url(sign_result)
|
|
308
|
+
unless upload_url
|
|
245
309
|
handle_retry(task_item, "获取预签名 URL 失败")
|
|
246
310
|
return
|
|
247
311
|
end
|
|
248
312
|
|
|
249
|
-
upload_url = sign_result["data"]["url"]
|
|
250
|
-
|
|
251
313
|
# 读取文件
|
|
252
314
|
begin
|
|
253
315
|
file_content = File.binread(file_path)
|
|
@@ -266,7 +328,7 @@ module JPSClient
|
|
|
266
328
|
method: :put,
|
|
267
329
|
body: file_content,
|
|
268
330
|
headers: {
|
|
269
|
-
'Content-Type' =>
|
|
331
|
+
'Content-Type' => content_type,
|
|
270
332
|
'Content-Length' => file_size.to_s
|
|
271
333
|
},
|
|
272
334
|
timeout: timeout,
|
|
@@ -276,8 +338,7 @@ module JPSClient
|
|
|
276
338
|
response = request.run
|
|
277
339
|
|
|
278
340
|
if response.success?
|
|
279
|
-
|
|
280
|
-
file_url = "https://#{@upload_config.media_bucket_name}.s3.#{@upload_config.media_region}.amazonaws.com/#{s3_key}"
|
|
341
|
+
file_url = uploaded_media_url(sign_result, s3_key)
|
|
281
342
|
|
|
282
343
|
# 记录成功结果
|
|
283
344
|
@results_mutex.synchronize do
|
|
@@ -300,6 +361,21 @@ module JPSClient
|
|
|
300
361
|
end
|
|
301
362
|
end
|
|
302
363
|
|
|
364
|
+
# 调试模式(JPS_DEBUG)下打印本次上传使用的 S3 参数
|
|
365
|
+
# 并发上传时逐行 puts 会互相穿插,这里一次性输出整块
|
|
366
|
+
def debug_print_upload_params(file_name, s3_key, content_type)
|
|
367
|
+
return unless JPSClient.debug?
|
|
368
|
+
|
|
369
|
+
puts [
|
|
370
|
+
"[JPS_DEBUG] media 上传参数: #{file_name}",
|
|
371
|
+
"[JPS_DEBUG] s3Key: #{s3_key}",
|
|
372
|
+
"[JPS_DEBUG] bucketName: #{@upload_config.media_bucket_name}",
|
|
373
|
+
"[JPS_DEBUG] region: #{@upload_config.media_region}",
|
|
374
|
+
"[JPS_DEBUG] uploadType: #{@upload_config.upload_type}",
|
|
375
|
+
"[JPS_DEBUG] contentType: #{content_type}"
|
|
376
|
+
].join("\n")
|
|
377
|
+
end
|
|
378
|
+
|
|
303
379
|
# 处理重试
|
|
304
380
|
def handle_retry(task_item, error_reason)
|
|
305
381
|
file_path = task_item["file_path"]
|
|
@@ -348,6 +424,46 @@ module JPSClient
|
|
|
348
424
|
[[calculated, 30].max, 300].min
|
|
349
425
|
end
|
|
350
426
|
|
|
427
|
+
# 是否是可以长期访问的地址:必须是 http 开头且不带 S3 预签名参数
|
|
428
|
+
def static_media_url?(url)
|
|
429
|
+
value = url.to_s
|
|
430
|
+
value.start_with?("http") && !value.include?("X-Amz-Signature")
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# 取预签名 PUT 地址:sign_url 返回的 url 才带 X-Amz-Signature,
|
|
434
|
+
# api_gateway_url 是上传完成后的静态访问地址,PUT 到它上面必然失败
|
|
435
|
+
def signed_upload_url(sign_result)
|
|
436
|
+
data = sign_result&.dig("data")
|
|
437
|
+
return nil unless data.is_a?(Hash)
|
|
438
|
+
|
|
439
|
+
data["url"] || data["signUrl"] || data["sign_url"]
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# 取上传完成后的访问地址,用于写回 JPS 提交记录
|
|
443
|
+
#
|
|
444
|
+
# 优先 api_gateway_url:sign_url 返回的静态域名地址就是给上传后更新数据用的。
|
|
445
|
+
# 这里绝不能回退到 data["url"],那是带 X-Amz-Signature 且 30 分钟过期的预签名地址,
|
|
446
|
+
# 写进 commit_log 后链接很快失效
|
|
447
|
+
def uploaded_media_url(sign_result, s3_key)
|
|
448
|
+
data = sign_result&.dig("data")
|
|
449
|
+
if data.is_a?(Hash)
|
|
450
|
+
url = data["apiGatewayUrl"] || data["api_gateway_url"] ||
|
|
451
|
+
data["fileUrl"] || data["file_url"] ||
|
|
452
|
+
data["staticUrl"] || data["static_url"] ||
|
|
453
|
+
data["httpUrl"] || data["http_url"]
|
|
454
|
+
return url unless url.to_s.empty?
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
"#{static_base_url}/#{s3_key}"
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def static_base_url
|
|
461
|
+
bucket_name = @upload_config.media_bucket_name.to_s
|
|
462
|
+
return "https://jps-static.devtestapp.com" if bucket_name == "jps-resource"
|
|
463
|
+
|
|
464
|
+
"https://pgy-static.devtestapp.com"
|
|
465
|
+
end
|
|
466
|
+
|
|
351
467
|
# 检查是否上传失败
|
|
352
468
|
def upload_failed?
|
|
353
469
|
@upload_failed_mutex.synchronize { @upload_failed }
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module JPSClient
|
|
2
|
+
# 上传文件的 Content-Type 推导
|
|
3
|
+
#
|
|
4
|
+
# S3 对象最终的 Content-Type 由两处决定:简单上传是 PUT 时发送的 header,
|
|
5
|
+
# 分片上传是 CreateMultipartUpload 的 contentType。两条链路统一走这里的映射,
|
|
6
|
+
# 避免所有对象都存成 application/octet-stream,导致浏览器无法内联预览图片/视频、
|
|
7
|
+
# Android 无法识别 apk 安装包。
|
|
8
|
+
module ContentType
|
|
9
|
+
# 无法识别扩展名时的兜底类型
|
|
10
|
+
DEFAULT = 'application/octet-stream'.freeze
|
|
11
|
+
|
|
12
|
+
# 扩展名 → Content-Type
|
|
13
|
+
# .ipa / .exe 保持 application/octet-stream:这是安装包分发的惯例值,
|
|
14
|
+
# 换成其他类型对客户端没有收益
|
|
15
|
+
EXTENSION_MAP = {
|
|
16
|
+
'.zip' => 'application/zip',
|
|
17
|
+
'.apk' => 'application/vnd.android.package-archive',
|
|
18
|
+
'.ipa' => DEFAULT,
|
|
19
|
+
'.exe' => DEFAULT,
|
|
20
|
+
'.png' => 'image/png',
|
|
21
|
+
'.jpg' => 'image/jpeg',
|
|
22
|
+
'.jpeg' => 'image/jpeg',
|
|
23
|
+
'.gif' => 'image/gif',
|
|
24
|
+
'.bmp' => 'image/bmp',
|
|
25
|
+
'.webp' => 'image/webp',
|
|
26
|
+
'.mp4' => 'video/mp4',
|
|
27
|
+
'.mov' => 'video/quicktime',
|
|
28
|
+
'.avi' => 'video/x-msvideo',
|
|
29
|
+
'.mkv' => 'video/x-matroska',
|
|
30
|
+
'.webm' => 'video/webm'
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
module_function
|
|
34
|
+
|
|
35
|
+
# 根据文件扩展名推导 Content-Type
|
|
36
|
+
#
|
|
37
|
+
# @param file_path [String] 文件路径
|
|
38
|
+
# @return [String] Content-Type,无法识别时返回 DEFAULT
|
|
39
|
+
def for_file(file_path)
|
|
40
|
+
EXTENSION_MAP.fetch(::File.extname(file_path.to_s).downcase, DEFAULT)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/jpsclient/version.rb
CHANGED
data/lib/jpsclient.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jpsclient
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.7.
|
|
4
|
+
version: 2.7.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Your Name
|
|
@@ -274,6 +274,7 @@ files:
|
|
|
274
274
|
- lib/jpsclient/upload/upload_media_client.rb
|
|
275
275
|
- lib/jpsclient/upload/upload_progress.rb
|
|
276
276
|
- lib/jpsclient/utils/aes.rb
|
|
277
|
+
- lib/jpsclient/utils/content_type.rb
|
|
277
278
|
- lib/jpsclient/utils/debug.rb
|
|
278
279
|
- lib/jpsclient/utils/logger.rb
|
|
279
280
|
- lib/jpsclient/version.rb
|
|
@@ -296,7 +297,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
296
297
|
- !ruby/object:Gem::Version
|
|
297
298
|
version: '0'
|
|
298
299
|
requirements: []
|
|
299
|
-
rubygems_version: 4.0.
|
|
300
|
+
rubygems_version: 4.0.11
|
|
300
301
|
specification_version: 4
|
|
301
302
|
summary: JPS Platform API Client with Full API Support
|
|
302
303
|
test_files: []
|