unipod 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +79 -0
- data/exe/unipod +13 -0
- data/lib/unipod/command/install.rb +384 -0
- data/lib/unipod/command/push.rb +384 -0
- data/lib/unipod/command/server.rb +115 -0
- data/lib/unipod/command.rb +40 -0
- data/lib/unipod/config.rb +393 -0
- data/lib/unipod/server/cache_manager.rb +1106 -0
- data/lib/unipod/server/download_handler.rb +359 -0
- data/lib/unipod/server/index.html +460 -0
- data/lib/unipod/server/package_builder_queue.rb +255 -0
- data/lib/unipod/server/server.rb +393 -0
- data/lib/unipod/server/server_handler.rb +526 -0
- data/lib/unipod/ui.rb +173 -0
- data/lib/unipod/version.rb +5 -0
- data/lib/unipod.rb +33 -0
- metadata +226 -0
@@ -0,0 +1,526 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'socket'
|
6
|
+
require 'digest'
|
7
|
+
require 'thread'
|
8
|
+
|
9
|
+
module UniPod
|
10
|
+
module Server
|
11
|
+
# 处理HTTP请求的处理程序
|
12
|
+
class ServerHandler
|
13
|
+
attr_reader :cache_manager, :verbose
|
14
|
+
|
15
|
+
# 使用DownloadHandler的共享锁
|
16
|
+
def self.package_build_locks
|
17
|
+
DownloadHandler.package_build_locks
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.locks_mutex
|
21
|
+
DownloadHandler.locks_mutex
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(cache_manager, options = {})
|
25
|
+
@cache_manager = cache_manager
|
26
|
+
@verbose = options[:verbose] || false
|
27
|
+
end
|
28
|
+
|
29
|
+
# 添加CORS头信息
|
30
|
+
def add_cors_headers(res)
|
31
|
+
res['Access-Control-Allow-Origin'] = '*'
|
32
|
+
res['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
33
|
+
res['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, Accept, X-Requested-With'
|
34
|
+
res['Access-Control-Max-Age'] = '86400' # 24小时
|
35
|
+
|
36
|
+
# 添加npm兼容的header
|
37
|
+
res['npm-notice'] = 'UniPod Registry for Unity Package Manager'
|
38
|
+
|
39
|
+
# Unity Package Manager 专用头
|
40
|
+
res['X-Unity-Package-Manager'] = 'true'
|
41
|
+
|
42
|
+
# 添加CORS头,确保Unity Package Manager可以正确识别
|
43
|
+
res['Access-Control-Expose-Headers'] = 'Content-Type, ETag, Cache-Control, Content-Length, Accept-Ranges, X-Unity-Package-Manager'
|
44
|
+
|
45
|
+
# 添加缓存控制头
|
46
|
+
res['Cache-Control'] = 'max-age=0, no-cache, no-store, must-revalidate'
|
47
|
+
res['Pragma'] = 'no-cache'
|
48
|
+
res['Expires'] = '0'
|
49
|
+
end
|
50
|
+
|
51
|
+
# 处理搜索请求 - Unity Package Manager API
|
52
|
+
def handle_search(req, res, req_id = 0)
|
53
|
+
query = req.query.nil? ? '' : (req.query['text'] || '')
|
54
|
+
from = req.query.nil? ? 0 : (req.query['from'] || '0').to_i
|
55
|
+
size = req.query.nil? ? 20 : (req.query['size'] || '20').to_i
|
56
|
+
|
57
|
+
log_verbose "[#{req_id}] 处理搜索请求: 查询=#{query}, from=#{from}, size=#{size}"
|
58
|
+
|
59
|
+
# 动态获取主机地址,优先使用客户端请求的主机地址
|
60
|
+
host_address = if req.header['host'] && !req.header['host'].empty?
|
61
|
+
req.header['host'].first
|
62
|
+
else
|
63
|
+
"localhost:7748"
|
64
|
+
end
|
65
|
+
log_verbose "[#{req_id}] 使用主机地址: #{host_address}"
|
66
|
+
|
67
|
+
# 初始化空的搜索结果
|
68
|
+
search_results = {'objects' => [], 'total' => 0, 'time' => Time.now.utc.strftime('%a, %d %b %Y %H:%M:%S GMT')}
|
69
|
+
|
70
|
+
# 尝试从缓存管理器获取包
|
71
|
+
if @cache_manager.respond_to?(:search_packages)
|
72
|
+
# 如果缓存管理器支持搜索功能,直接使用
|
73
|
+
log_verbose "[#{req_id}] 使用缓存管理器搜索包: #{query}"
|
74
|
+
# 修正参数数量, 确保参数顺序正确
|
75
|
+
packages_from_cache = @cache_manager.search_packages(query)
|
76
|
+
|
77
|
+
if packages_from_cache && packages_from_cache.is_a?(Hash) && packages_from_cache['objects']
|
78
|
+
search_results = packages_from_cache
|
79
|
+
end
|
80
|
+
elsif @cache_manager.respond_to?(:get_all_packages)
|
81
|
+
# 否则获取全部包并自行过滤
|
82
|
+
log_verbose "[#{req_id}] 从缓存管理器获取所有包"
|
83
|
+
all_packages = @cache_manager.get_all_packages
|
84
|
+
|
85
|
+
if all_packages && all_packages.is_a?(Hash) && all_packages['objects']
|
86
|
+
log_verbose "[#{req_id}] 成功从缓存管理器获取到 #{all_packages['objects'].size} 个包"
|
87
|
+
|
88
|
+
# 如果有查询,根据查询条件过滤包
|
89
|
+
filtered_packages = all_packages['objects']
|
90
|
+
if !query.empty?
|
91
|
+
filtered_packages = filtered_packages.select do |pkg_obj|
|
92
|
+
pkg = pkg_obj['package']
|
93
|
+
pkg['name'].to_s.downcase.include?(query.downcase) ||
|
94
|
+
(pkg['displayName'] && pkg['displayName'].to_s.downcase.include?(query.downcase)) ||
|
95
|
+
(pkg['description'] && pkg['description'].to_s.downcase.include?(query.downcase)) ||
|
96
|
+
(pkg['keywords'] && pkg['keywords'].any? { |k| k.to_s.downcase.include?(query.downcase) })
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# 更新搜索结果
|
101
|
+
search_results['objects'] = filtered_packages
|
102
|
+
search_results['total'] = filtered_packages.size
|
103
|
+
search_results['time'] = Time.now.utc.strftime('%a, %d %b %Y %H:%M:%S GMT')
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# 确保所有包对象都有正确的格式和字段
|
108
|
+
if search_results['objects'] && search_results['objects'].is_a?(Array)
|
109
|
+
search_results['objects'].each do |package_obj|
|
110
|
+
if package_obj.is_a?(Hash) && package_obj['package'] && package_obj['package'].is_a?(Hash)
|
111
|
+
pkg = package_obj['package']
|
112
|
+
|
113
|
+
# 确保包含所需字段
|
114
|
+
pkg['name'] ||= ''
|
115
|
+
pkg['description'] ||= ''
|
116
|
+
|
117
|
+
# 添加或更新dist-tags字段
|
118
|
+
version = pkg['version'] || '1.0.0'
|
119
|
+
pkg['dist-tags'] = {
|
120
|
+
'latest' => version
|
121
|
+
}
|
122
|
+
|
123
|
+
# 添加versions字段
|
124
|
+
pkg['versions'] = {
|
125
|
+
version => 'latest'
|
126
|
+
}
|
127
|
+
|
128
|
+
# 确保包含时间信息
|
129
|
+
pkg['time'] = {
|
130
|
+
'modified' => (Time.now - rand(100000)).utc.iso8601
|
131
|
+
} unless pkg['time']
|
132
|
+
|
133
|
+
# 确保包含readmeFilename
|
134
|
+
pkg['readmeFilename'] = 'README.md' unless pkg['readmeFilename']
|
135
|
+
|
136
|
+
# 确保包对象有flags和local字段
|
137
|
+
package_obj['flags'] = {} unless package_obj['flags']
|
138
|
+
package_obj['local'] = true unless package_obj.key?('local')
|
139
|
+
|
140
|
+
# 设置searchScore为客户端期望的值
|
141
|
+
package_obj['searchScore'] = 100000 if package_obj['searchScore']
|
142
|
+
|
143
|
+
# 确保tarball URL使用正确的主机地址
|
144
|
+
if pkg['dist'] && pkg['dist']['tarball']
|
145
|
+
pkg_name = pkg['name']
|
146
|
+
pkg_version = pkg['version'] || version
|
147
|
+
tarball_url = "http://#{host_address}/#{pkg_name}/-/#{pkg_name}-#{pkg_version}.tgz"
|
148
|
+
pkg['dist']['tarball'] = tarball_url
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# 处理分页
|
155
|
+
if search_results['objects'] && search_results['objects'].is_a?(Array)
|
156
|
+
total_packages = search_results['objects'].size
|
157
|
+
|
158
|
+
# 修复分页问题:Unity Package Manager需要特殊处理
|
159
|
+
if from >= total_packages
|
160
|
+
# 返回所有包而不是空数组 - Unity需要这个行为
|
161
|
+
paginated_packages = search_results['objects']
|
162
|
+
else
|
163
|
+
# 正常分页
|
164
|
+
paginated_packages = search_results['objects'][from, size] || []
|
165
|
+
end
|
166
|
+
|
167
|
+
# 更新搜索结果
|
168
|
+
search_results['objects'] = paginated_packages
|
169
|
+
search_results['total'] = total_packages
|
170
|
+
end
|
171
|
+
|
172
|
+
# 确保time字段格式正确(HTTP日期格式)
|
173
|
+
search_results['time'] = Time.now.utc.strftime('%a, %d %b %Y %H:%M:%S GMT')
|
174
|
+
|
175
|
+
log_verbose "[#{req_id}] 返回 #{search_results['objects'].size} 个搜索结果, 总计 #{search_results['total']} 个"
|
176
|
+
|
177
|
+
# 设置响应
|
178
|
+
res.status = 200
|
179
|
+
res['Content-Type'] = 'application/json'
|
180
|
+
json_response = JSON.generate(search_results)
|
181
|
+
res.body = json_response
|
182
|
+
puts_formatted_json(json_response, req_id, "搜索响应") if @verbose
|
183
|
+
end
|
184
|
+
|
185
|
+
# 处理包信息请求 - Unity Package Manager API
|
186
|
+
def handle_package_info(package_name, res, req_id = 0, req = nil)
|
187
|
+
begin
|
188
|
+
log_verbose "[#{req_id}] 处理包信息请求: #{package_name}"
|
189
|
+
|
190
|
+
# 动态获取主机地址,优先使用客户端请求的主机地址
|
191
|
+
host_address = if req && req.header['host'] && !req.header['host'].empty?
|
192
|
+
req.header['host'].first
|
193
|
+
else
|
194
|
+
"localhost:7748"
|
195
|
+
end
|
196
|
+
log_verbose "[#{req_id}] 使用主机地址: #{host_address}"
|
197
|
+
|
198
|
+
# 初始化变量,确保它们都有默认值
|
199
|
+
package_data = nil
|
200
|
+
versions = []
|
201
|
+
latest_version = nil
|
202
|
+
|
203
|
+
# 尝试获取包信息
|
204
|
+
if @cache_manager.respond_to?(:get_package_info)
|
205
|
+
log_verbose "[#{req_id}] 从缓存管理器获取包信息: #{package_name}"
|
206
|
+
package_data = @cache_manager.get_package_info(package_name)
|
207
|
+
|
208
|
+
if package_data && package_data['version']
|
209
|
+
log_verbose "[#{req_id}] 获取到包信息: #{package_name}, 版本: #{package_data['version']}"
|
210
|
+
latest_version = package_data['version']
|
211
|
+
versions << latest_version
|
212
|
+
else
|
213
|
+
log_verbose "[#{req_id}] 没有获取到包信息或版本号"
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# 如果没有获取到包信息,返回404
|
218
|
+
if versions.empty?
|
219
|
+
log_verbose "[#{req_id}] 未找到包: #{package_name}"
|
220
|
+
res.status = 404
|
221
|
+
res['Content-Type'] = 'application/json'
|
222
|
+
error_response = {
|
223
|
+
"error" => "Not Found",
|
224
|
+
"code" => "PACKAGE_NOT_FOUND",
|
225
|
+
"message" => "Package '#{package_name}' not found"
|
226
|
+
}
|
227
|
+
json_response = JSON.generate(error_response)
|
228
|
+
res.body = json_response
|
229
|
+
return
|
230
|
+
end
|
231
|
+
|
232
|
+
# 从主版本生成其他可能的版本
|
233
|
+
if versions.size == 1
|
234
|
+
version_parts = latest_version.split('.')
|
235
|
+
if version_parts.size >= 3
|
236
|
+
major, minor, patch = version_parts
|
237
|
+
|
238
|
+
# 添加可能的之前版本
|
239
|
+
patch_num = patch.to_i
|
240
|
+
if patch_num > 0
|
241
|
+
versions << "#{major}.#{minor}.#{patch_num - 1}"
|
242
|
+
end
|
243
|
+
|
244
|
+
if minor.to_i > 0
|
245
|
+
versions << "#{major}.#{minor.to_i - 1}.0"
|
246
|
+
end
|
247
|
+
|
248
|
+
# 确保版本唯一且按照版本号排序
|
249
|
+
versions = versions.uniq.sort_by { |v| Gem::Version.new(v) rescue "0" }
|
250
|
+
latest_version = versions.last
|
251
|
+
|
252
|
+
log_verbose "[#{req_id}] 生成的版本列表: #{versions.join(', ')}, 最新版本: #{latest_version}"
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
# 构建包信息
|
257
|
+
full_package_data = {
|
258
|
+
"name" => package_name,
|
259
|
+
"versions" => {},
|
260
|
+
"time" => {
|
261
|
+
"modified" => Time.now.utc.iso8601,
|
262
|
+
"created" => (Time.now - 86400).utc.iso8601
|
263
|
+
},
|
264
|
+
"users" => {},
|
265
|
+
"dist-tags" => {
|
266
|
+
"latest" => latest_version
|
267
|
+
},
|
268
|
+
"_rev" => "10-#{Digest::MD5.hexdigest(Time.now.to_s)[0,16]}",
|
269
|
+
"_id" => package_name,
|
270
|
+
"_attachments" => {},
|
271
|
+
"readme" => "# #{package_name}\n\n#{package_data && package_data['description'] ? package_data['description'] : '无描述'}"
|
272
|
+
}
|
273
|
+
|
274
|
+
# 为每个版本生成信息
|
275
|
+
versions.each_with_index do |version, idx|
|
276
|
+
# 创建版本的基本信息
|
277
|
+
version_info = if version == latest_version && package_data
|
278
|
+
# 使用获取到的包信息作为基础
|
279
|
+
package_data.clone
|
280
|
+
else
|
281
|
+
# 基于最新版本创建其他版本信息
|
282
|
+
{
|
283
|
+
'name' => package_name,
|
284
|
+
'version' => version,
|
285
|
+
'description' => package_data && package_data['description'] ? package_data['description'] : '无描述',
|
286
|
+
'keywords' => package_data && package_data['keywords'] ? package_data['keywords'] : [],
|
287
|
+
'author' => package_data && package_data['author'] ? package_data['author'] : {'name' => 'Unknown', 'email' => 'unknown@example.com'}
|
288
|
+
}
|
289
|
+
end
|
290
|
+
|
291
|
+
# 确保版本号正确
|
292
|
+
version_info['version'] = version
|
293
|
+
|
294
|
+
# 添加必要的字段
|
295
|
+
version_info['_id'] = "#{package_name}@#{version}"
|
296
|
+
version_info['readmeFilename'] = 'README.md' unless version_info['readmeFilename']
|
297
|
+
version_info['gitHead'] = Digest::SHA1.hexdigest("#{package_name}-#{version}") unless version_info['gitHead']
|
298
|
+
version_info['_nodeVersion'] = '23.9.0' unless version_info['_nodeVersion']
|
299
|
+
version_info['_npmVersion'] = '10.9.2' unless version_info['_npmVersion']
|
300
|
+
version_info['contributors'] = [] unless version_info['contributors']
|
301
|
+
|
302
|
+
# 添加或更新dist信息
|
303
|
+
version_info['dist'] = {} unless version_info['dist']
|
304
|
+
|
305
|
+
# 计算shasum
|
306
|
+
shasum = get_or_calculate_shasum(package_name, version, version_info, req_id, version == latest_version)
|
307
|
+
|
308
|
+
# 生成integrity值
|
309
|
+
integrity_hash = Digest::SHA512.hexdigest("#{package_name}-#{version}-#{shasum}")
|
310
|
+
integrity_chars = integrity_hash[0...40] + rand(10000).to_s.rjust(4, '0')
|
311
|
+
integrity_b64 = integrity_chars.tr('0123456789abcdef', 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/')
|
312
|
+
|
313
|
+
# 更新dist信息
|
314
|
+
version_info['dist']['integrity'] = "sha512-#{integrity_b64}"
|
315
|
+
version_info['dist']['shasum'] = shasum
|
316
|
+
version_info['dist']['tarball'] = "http://#{host_address}/#{package_name}/-/#{package_name}-#{version}.tgz"
|
317
|
+
|
318
|
+
# 将版本添加到versions对象
|
319
|
+
full_package_data['versions'][version] = version_info
|
320
|
+
|
321
|
+
# 添加时间信息
|
322
|
+
time_offset = idx * 600 # 每个版本间隔10分钟
|
323
|
+
full_package_data['time'][version] = (Time.now - (86400 - time_offset)).utc.iso8601
|
324
|
+
end
|
325
|
+
|
326
|
+
# 设置响应
|
327
|
+
res.status = 200
|
328
|
+
res['Content-Type'] = 'application/json'
|
329
|
+
json_response = JSON.generate(full_package_data)
|
330
|
+
res.body = json_response
|
331
|
+
log_verbose "[#{req_id}] 成功生成包信息响应: #{package_name}, 包含 #{versions.size} 个版本"
|
332
|
+
|
333
|
+
rescue => e
|
334
|
+
# 捕获所有异常
|
335
|
+
error_message = "处理包信息请求时出错: #{e.message}\n#{e.backtrace.join("\n")}"
|
336
|
+
log_verbose "[#{req_id}] 错误: #{error_message}"
|
337
|
+
|
338
|
+
res.status = 500
|
339
|
+
res['Content-Type'] = 'application/json'
|
340
|
+
error_response = {
|
341
|
+
"error" => "Internal Server Error",
|
342
|
+
"code" => "REQUEST_PROCESSING_ERROR",
|
343
|
+
"message" => e.message
|
344
|
+
}
|
345
|
+
json_response = JSON.generate(error_response)
|
346
|
+
res.body = json_response
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
# 处理根路由请求
|
351
|
+
def handle_root_request(res, req_id = 0)
|
352
|
+
# 尝试读取HTML模板
|
353
|
+
template_path = File.join(File.dirname(__FILE__), 'index.html')
|
354
|
+
|
355
|
+
# 设置响应状态和内容类型
|
356
|
+
res.status = 200
|
357
|
+
res['Content-Type'] = 'text/html'
|
358
|
+
|
359
|
+
if File.exist?(template_path)
|
360
|
+
# 读取index.html文件内容
|
361
|
+
html = File.read(template_path)
|
362
|
+
|
363
|
+
# 获取包列表
|
364
|
+
packages_data = []
|
365
|
+
|
366
|
+
# 尝试从缓存管理器获取包列表
|
367
|
+
if @cache_manager.respond_to?(:get_all_packages)
|
368
|
+
log_verbose "[#{req_id}] 从缓存管理器获取所有包用于首页展示"
|
369
|
+
all_packages = @cache_manager.get_all_packages
|
370
|
+
|
371
|
+
if all_packages && all_packages['objects'] && all_packages['objects'].is_a?(Array)
|
372
|
+
log_verbose "[#{req_id}] 成功从缓存管理器获取到 #{all_packages['objects'].size} 个包"
|
373
|
+
|
374
|
+
# 转换为前端需要的格式
|
375
|
+
packages_data = all_packages['objects'].map do |pkg_obj|
|
376
|
+
pkg = pkg_obj['package']
|
377
|
+
{
|
378
|
+
name: pkg['name'],
|
379
|
+
version: pkg['version'],
|
380
|
+
displayName: pkg['displayName'] || pkg['name'],
|
381
|
+
description: pkg['description'] || '',
|
382
|
+
unity: pkg['unity'] || 'unknown',
|
383
|
+
author: pkg['author'] || { name: 'Unknown', email: '' },
|
384
|
+
keywords: pkg['keywords'] || [],
|
385
|
+
category: pkg['category'] || 'Libraries'
|
386
|
+
}
|
387
|
+
end
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
# 如果没有从缓存获取到包,使用空数组
|
392
|
+
if packages_data.empty?
|
393
|
+
log_verbose "[#{req_id}] 未从缓存获取到包,返回空数组用于首页展示"
|
394
|
+
packages_data = []
|
395
|
+
end
|
396
|
+
|
397
|
+
# 转换为JSON并替换模板中的变量
|
398
|
+
packages_json = packages_data.to_json
|
399
|
+
html.gsub!('{{PACKAGES_JSON}}', packages_json)
|
400
|
+
|
401
|
+
# 设置响应体
|
402
|
+
res.body = html
|
403
|
+
log_verbose "[#{req_id}] 首页展示 #{packages_data.size} 个包"
|
404
|
+
else
|
405
|
+
# 如果找不到模板文件,返回空白页面
|
406
|
+
log_verbose "[#{req_id}] 未找到index.html模板文件,返回空白页面"
|
407
|
+
res.body = ''
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
# 处理请求错误
|
412
|
+
def handle_request_error(error, res, req_id = 0)
|
413
|
+
UI.error "[#{req_id}] 请求处理错误: #{error.message}"
|
414
|
+
UI.error error.backtrace.join("\n") if @verbose
|
415
|
+
|
416
|
+
# 返回500错误
|
417
|
+
res.status = 500
|
418
|
+
res['Content-Type'] = 'application/json'
|
419
|
+
error_json = JSON.generate({
|
420
|
+
error: "服务器内部错误",
|
421
|
+
message: error.message
|
422
|
+
})
|
423
|
+
res.body = error_json
|
424
|
+
puts_formatted_json(error_json, req_id, "错误响应") if @verbose
|
425
|
+
end
|
426
|
+
|
427
|
+
# 美化JSON并打印输出
|
428
|
+
def puts_formatted_json(json_string, req_id, desc = "JSON响应")
|
429
|
+
begin
|
430
|
+
# 解析JSON字符串为Ruby对象
|
431
|
+
json_obj = JSON.parse(json_string)
|
432
|
+
# 使用pretty_generate重新生成格式化的JSON
|
433
|
+
formatted_json = JSON.pretty_generate(json_obj)
|
434
|
+
# 打印格式化后的JSON
|
435
|
+
puts "[#{req_id}] #{desc}:"
|
436
|
+
puts formatted_json
|
437
|
+
rescue => e
|
438
|
+
puts "[#{req_id}] 无法格式化JSON: #{e.message}"
|
439
|
+
puts json_string
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
private
|
444
|
+
|
445
|
+
def log_verbose(message)
|
446
|
+
UI.puts message if @verbose
|
447
|
+
end
|
448
|
+
|
449
|
+
# 获取本地IP地址
|
450
|
+
def get_local_ip
|
451
|
+
begin
|
452
|
+
Socket.ip_address_list.detect{|intf| intf.ipv4_private?}&.ip_address || 'localhost'
|
453
|
+
rescue
|
454
|
+
'localhost'
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
# 获取包锁
|
459
|
+
def get_package_lock(package_key)
|
460
|
+
self.class.locks_mutex.synchronize do
|
461
|
+
self.class.package_build_locks[package_key] ||= Mutex.new
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
465
|
+
# 计算或生成给定包版本的shasum
|
466
|
+
def get_or_calculate_shasum(package_name, version, version_info, req_id, is_latest_version = false)
|
467
|
+
# 锁定此包版本,防止并发生成
|
468
|
+
package_lock = get_package_lock("#{package_name}-#{version}")
|
469
|
+
|
470
|
+
package_lock.synchronize do
|
471
|
+
tarball_filename = "#{package_name}-#{version}.tgz"
|
472
|
+
tarball_path = @cache_manager.get_package_tarball(package_name, tarball_filename)
|
473
|
+
|
474
|
+
# 如果tarball文件已存在,使用实际文件计算shasum
|
475
|
+
if tarball_path && File.exist?(tarball_path)
|
476
|
+
file_content = File.open(tarball_path, 'rb') { |f| f.read }
|
477
|
+
shasum = Digest::SHA1.hexdigest(file_content)
|
478
|
+
log_verbose "[#{req_id}] 版本 #{version} 使用实际文件计算的shasum: #{shasum}"
|
479
|
+
return shasum
|
480
|
+
end
|
481
|
+
|
482
|
+
# 只有最新版本才尝试构建tarball
|
483
|
+
if is_latest_version
|
484
|
+
# 如果有Git信息,尝试构建tarball并计算shasum
|
485
|
+
if version_info && version_info['git'] && version_info['git']['url']
|
486
|
+
git_url = version_info['git']['url']
|
487
|
+
git_tag = version_info['git']['tag'] || "v#{version}"
|
488
|
+
git_branch = version_info['git']['branch'] || 'main'
|
489
|
+
|
490
|
+
log_verbose "[#{req_id}] 为最新版本#{version}添加tarball构建任务: #{git_url}, tag: #{git_tag}"
|
491
|
+
|
492
|
+
# 获取包构建队列单例实例
|
493
|
+
builder_queue = PackageBuilderQueue.instance
|
494
|
+
|
495
|
+
# 设置任务优先级
|
496
|
+
# 包信息API触发的构建优先级为0(普通)
|
497
|
+
priority = 0 # 普通优先级
|
498
|
+
|
499
|
+
# 将任务添加到队列,不需要等待结果
|
500
|
+
builder_queue.enqueue(package_name, version, git_url, git_tag, git_branch, priority) do |success, result_path|
|
501
|
+
if success
|
502
|
+
log_verbose "[#{req_id}] 异步包构建成功: #{result_path}"
|
503
|
+
else
|
504
|
+
log_verbose "[#{req_id}] 异步包构建失败: #{package_name}@#{version}"
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
# 不需要等待构建完成,使用默认值
|
509
|
+
log_verbose "[#{req_id}] 任务已加入队列,使用默认shasum"
|
510
|
+
else
|
511
|
+
log_verbose "[#{req_id}] 包没有Git信息,无法构建: #{package_name}@#{version}"
|
512
|
+
end
|
513
|
+
else
|
514
|
+
# 非最新版本,记录日志
|
515
|
+
log_verbose "[#{req_id}] 版本 #{version} 不是最新版本,不加入构建队列"
|
516
|
+
end
|
517
|
+
|
518
|
+
# 如果无法生成文件或计算失败,使用默认值
|
519
|
+
shasum = Digest::SHA1.hexdigest("#{package_name}-#{version}")
|
520
|
+
log_verbose "[#{req_id}] #{is_latest_version ? '使用默认' : '不生成'}shasum: #{shasum}"
|
521
|
+
return shasum
|
522
|
+
end
|
523
|
+
end
|
524
|
+
end
|
525
|
+
end
|
526
|
+
end
|
data/lib/unipod/ui.rb
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rainbow'
|
4
|
+
require 'tty-spinner'
|
5
|
+
require 'tty-prompt'
|
6
|
+
require 'tty-table'
|
7
|
+
require 'tty-progressbar'
|
8
|
+
|
9
|
+
module UniPod
|
10
|
+
class UI
|
11
|
+
class << self
|
12
|
+
def puts(message)
|
13
|
+
Kernel.puts(message)
|
14
|
+
end
|
15
|
+
|
16
|
+
def info(message)
|
17
|
+
puts Rainbow("ℹ️ #{message}").blue
|
18
|
+
end
|
19
|
+
|
20
|
+
def success(message)
|
21
|
+
puts Rainbow("✅ #{message}").green
|
22
|
+
end
|
23
|
+
|
24
|
+
def warning(message)
|
25
|
+
puts Rainbow("⚠️ #{message}").yellow
|
26
|
+
end
|
27
|
+
|
28
|
+
def error(message)
|
29
|
+
puts Rainbow("❌ #{message}").red
|
30
|
+
end
|
31
|
+
|
32
|
+
def debug(message)
|
33
|
+
return unless debug_mode?
|
34
|
+
puts Rainbow("🔍 #{message}").magenta
|
35
|
+
end
|
36
|
+
|
37
|
+
def title(message)
|
38
|
+
width = terminal_width
|
39
|
+
puts
|
40
|
+
puts Rainbow("━" * width).blue
|
41
|
+
puts Rainbow(message.center(width)).blue.bright
|
42
|
+
puts Rainbow("━" * width).blue
|
43
|
+
puts
|
44
|
+
end
|
45
|
+
|
46
|
+
def spinner(message, &block)
|
47
|
+
spinner = TTY::Spinner.new("[:spinner] #{message}", format: :dots)
|
48
|
+
spinner.auto_spin
|
49
|
+
|
50
|
+
begin
|
51
|
+
result = block.call
|
52
|
+
spinner.success(Rainbow('(完成)').green)
|
53
|
+
result
|
54
|
+
rescue => e
|
55
|
+
spinner.error(Rainbow("(失败: #{e.message})").red)
|
56
|
+
raise e
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def progress(total, message = "处理中")
|
61
|
+
bar = TTY::ProgressBar.new("[:bar] #{message} :percent (:current/:total)",
|
62
|
+
total: total, width: terminal_width - 30)
|
63
|
+
|
64
|
+
if block_given?
|
65
|
+
(0...total).each do |i|
|
66
|
+
yield i
|
67
|
+
bar.advance
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
bar
|
72
|
+
end
|
73
|
+
|
74
|
+
def prompt
|
75
|
+
@prompt ||= TTY::Prompt.new
|
76
|
+
end
|
77
|
+
|
78
|
+
def select(message, choices)
|
79
|
+
prompt.select(message, choices)
|
80
|
+
end
|
81
|
+
|
82
|
+
def multi_select(message, choices)
|
83
|
+
prompt.multi_select(message, choices)
|
84
|
+
end
|
85
|
+
|
86
|
+
def ask(message, default: nil, required: false)
|
87
|
+
options = {}
|
88
|
+
options[:default] = default if default
|
89
|
+
options[:required] = required
|
90
|
+
|
91
|
+
prompt.ask(message, **options)
|
92
|
+
end
|
93
|
+
|
94
|
+
def yes?(message, default: true)
|
95
|
+
prompt.yes?(message, default: default)
|
96
|
+
end
|
97
|
+
|
98
|
+
def no?(message, default: true)
|
99
|
+
prompt.no?(message, default: default)
|
100
|
+
end
|
101
|
+
|
102
|
+
def table(headers, rows)
|
103
|
+
table = TTY::Table.new(headers, rows)
|
104
|
+
puts table.render(:unicode, padding: [0, 1])
|
105
|
+
end
|
106
|
+
|
107
|
+
def format_package(package)
|
108
|
+
name = package[:name] || package["name"]
|
109
|
+
version = package[:version] || package["version"]
|
110
|
+
description = package[:description] || package["description"]
|
111
|
+
|
112
|
+
"#{Rainbow(name).green.bright} #{Rainbow(version).yellow} - #{description}"
|
113
|
+
end
|
114
|
+
|
115
|
+
def format_package_list(packages)
|
116
|
+
packages.each do |package|
|
117
|
+
puts format_package(package)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def format_package_details(package)
|
122
|
+
rows = []
|
123
|
+
|
124
|
+
# 基本信息
|
125
|
+
rows << ["名称", Rainbow(package[:name] || package["name"]).green.bright]
|
126
|
+
rows << ["版本", Rainbow(package[:version] || package["version"]).yellow]
|
127
|
+
rows << ["描述", package[:description] || package["description"]]
|
128
|
+
|
129
|
+
# 作者信息
|
130
|
+
author = package[:author] || package["author"]
|
131
|
+
if author
|
132
|
+
author_str = author.is_a?(Hash) ? "#{author[:name]} <#{author[:email]}>" : author.to_s
|
133
|
+
rows << ["作者", author_str]
|
134
|
+
end
|
135
|
+
|
136
|
+
# 许可证
|
137
|
+
license = package[:license] || package["license"]
|
138
|
+
rows << ["许可证", license] if license
|
139
|
+
|
140
|
+
# 依赖项
|
141
|
+
dependencies = package[:dependencies] || package["dependencies"]
|
142
|
+
if dependencies && !dependencies.empty?
|
143
|
+
deps_str = dependencies.map { |k, v| "#{k} (#{v})" }.join(", ")
|
144
|
+
rows << ["依赖项", deps_str]
|
145
|
+
end
|
146
|
+
|
147
|
+
# Unity 特定信息
|
148
|
+
unity_version =
|
149
|
+
package.dig(:unity, :version) ||
|
150
|
+
package.dig("unity", "version") ||
|
151
|
+
package[:unity_version] ||
|
152
|
+
package["unity_version"]
|
153
|
+
|
154
|
+
rows << ["Unity 版本", unity_version] if unity_version
|
155
|
+
|
156
|
+
# 显示表格
|
157
|
+
table = TTY::Table.new([nil, nil], rows)
|
158
|
+
puts table.render(:unicode, padding: [0, 1], multiline: true)
|
159
|
+
end
|
160
|
+
|
161
|
+
private
|
162
|
+
|
163
|
+
def debug_mode?
|
164
|
+
ENV['UNIPOD_DEBUG'] == 'true'
|
165
|
+
end
|
166
|
+
|
167
|
+
def terminal_width
|
168
|
+
require 'io/console'
|
169
|
+
IO.console.winsize[1] rescue 80
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|