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,359 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'digest'
|
6
|
+
require 'thread'
|
7
|
+
|
8
|
+
module UniPod
|
9
|
+
module Server
|
10
|
+
# 处理包下载的处理程序
|
11
|
+
class DownloadHandler
|
12
|
+
attr_reader :cache_manager, :verbose
|
13
|
+
|
14
|
+
# 使用类变量存储包构建锁,便于在不同处理程序间共享
|
15
|
+
@@package_build_locks = {}
|
16
|
+
@@locks_mutex = Mutex.new
|
17
|
+
|
18
|
+
# 获取共享的包构建锁表
|
19
|
+
def self.package_build_locks
|
20
|
+
@@package_build_locks
|
21
|
+
end
|
22
|
+
|
23
|
+
# 获取共享的锁互斥量
|
24
|
+
def self.locks_mutex
|
25
|
+
@@locks_mutex
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(cache_manager, options = {})
|
29
|
+
@cache_manager = cache_manager
|
30
|
+
@verbose = options[:verbose] || false
|
31
|
+
end
|
32
|
+
|
33
|
+
# 添加CORS头信息
|
34
|
+
def add_cors_headers(res)
|
35
|
+
res['Access-Control-Allow-Origin'] = '*'
|
36
|
+
res['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
37
|
+
res['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, Accept, X-Requested-With'
|
38
|
+
res['Access-Control-Max-Age'] = '86400' # 24小时
|
39
|
+
|
40
|
+
# 添加npm兼容的header
|
41
|
+
res['npm-notice'] = 'UniPod Registry for Unity Package Manager'
|
42
|
+
|
43
|
+
# Unity Package Manager 专用头
|
44
|
+
res['X-Unity-Package-Manager'] = 'true'
|
45
|
+
|
46
|
+
# 添加CORS头,确保Unity Package Manager可以正确识别
|
47
|
+
res['Access-Control-Expose-Headers'] = 'Content-Type, ETag, Cache-Control, Content-Length, Accept-Ranges, X-Unity-Package-Manager'
|
48
|
+
|
49
|
+
# 添加缓存控制头
|
50
|
+
res['Cache-Control'] = 'max-age=0, no-cache, no-store, must-revalidate'
|
51
|
+
res['Pragma'] = 'no-cache'
|
52
|
+
res['Expires'] = '0'
|
53
|
+
end
|
54
|
+
|
55
|
+
# 处理包下载请求 - Unity Package Manager API
|
56
|
+
def handle_package_download(package_name, tarball_name, res, req_id = 0, req = nil)
|
57
|
+
log_verbose "[#{req_id}] 处理包下载请求: #{package_name} - #{tarball_name}"
|
58
|
+
|
59
|
+
# 添加CORS头,确保跨域请求正常工作
|
60
|
+
add_cors_headers(res)
|
61
|
+
|
62
|
+
# 从tarball名称中提取版本
|
63
|
+
version_match = tarball_name.match(/#{Regexp.escape(package_name)}-(.+)\.tgz/)
|
64
|
+
version = version_match ? version_match[1] : '1.0.0'
|
65
|
+
|
66
|
+
# 处理HEAD请求 - Unity Package Manager会先发送HEAD请求检查包是否存在
|
67
|
+
if req && req.request_method == 'HEAD'
|
68
|
+
handle_head_request(package_name, tarball_name, version, res, req_id)
|
69
|
+
return
|
70
|
+
end
|
71
|
+
|
72
|
+
# 处理GET请求 - 下载tarball文件
|
73
|
+
handle_get_request(package_name, tarball_name, res, req_id)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# 处理HEAD请求
|
79
|
+
def handle_head_request(package_name, tarball_name, version, res, req_id)
|
80
|
+
log_verbose "[#{req_id}] 处理HEAD请求: #{package_name} - #{tarball_name} (版本: #{version})"
|
81
|
+
|
82
|
+
# 检查tarball是否存在于缓存中
|
83
|
+
tarball_path = @cache_manager.get_package_tarball(package_name, tarball_name)
|
84
|
+
|
85
|
+
if tarball_path && File.exist?(tarball_path)
|
86
|
+
serve_tarball_head(tarball_path, tarball_name, res, req_id)
|
87
|
+
else
|
88
|
+
# 找不到tarball,尝试动态生成
|
89
|
+
log_verbose "[#{req_id}] HEAD请求: 未找到包 #{package_name} 的tarball: #{tarball_name},尝试生成"
|
90
|
+
|
91
|
+
# 获取当前包的锁或创建一个新锁
|
92
|
+
package_lock = get_package_lock("#{package_name}-#{version}")
|
93
|
+
|
94
|
+
# 使用锁确保只有一个线程在生成tarball
|
95
|
+
package_lock.synchronize do
|
96
|
+
# 再次检查文件是否存在(可能在我们获取锁的同时被其他线程创建)
|
97
|
+
tarball_path = @cache_manager.get_package_tarball(package_name, tarball_name)
|
98
|
+
if tarball_path && File.exist?(tarball_path)
|
99
|
+
serve_tarball_head(tarball_path, tarball_name, res, req_id)
|
100
|
+
return
|
101
|
+
end
|
102
|
+
|
103
|
+
# 尝试构建tarball
|
104
|
+
if try_build_package_tarball(package_name, version, tarball_name, req_id)
|
105
|
+
# 再次检查文件是否生成
|
106
|
+
tarball_path = @cache_manager.get_package_tarball(package_name, tarball_name)
|
107
|
+
if tarball_path && File.exist?(tarball_path)
|
108
|
+
serve_tarball_head(tarball_path, tarball_name, res, req_id)
|
109
|
+
return
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# 如果仍然无法生成tarball,返回404
|
115
|
+
log_verbose "[#{req_id}] HEAD请求: 无法找到或生成包 #{package_name} 的tarball: #{tarball_name}"
|
116
|
+
res.status = 404
|
117
|
+
res['Content-Type'] = 'application/json'
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# 处理HEAD请求的tarball文件头信息
|
122
|
+
def serve_tarball_head(tarball_path, tarball_name, res, req_id)
|
123
|
+
file_size = File.size(tarball_path)
|
124
|
+
|
125
|
+
# 计算shasum以确保与包信息API中的值匹配
|
126
|
+
file_content = File.open(tarball_path, 'rb') { |f| f.read }
|
127
|
+
shasum = Digest::SHA1.hexdigest(file_content)
|
128
|
+
log_verbose "[#{req_id}] HEAD请求: 文件shasum: #{shasum}"
|
129
|
+
|
130
|
+
res.status = 200
|
131
|
+
res['Content-Type'] = 'application/octet-stream'
|
132
|
+
res['Content-Disposition'] = "attachment; filename=#{tarball_name}"
|
133
|
+
res['Content-Length'] = file_size.to_s
|
134
|
+
|
135
|
+
# 添加ETag头,这对Unity Package Manager也很重要
|
136
|
+
res['ETag'] = %Q{"#{shasum}"}
|
137
|
+
end
|
138
|
+
|
139
|
+
# 处理GET请求
|
140
|
+
def handle_get_request(package_name, tarball_name, res, req_id)
|
141
|
+
# 尝试从缓存中查找
|
142
|
+
tarball_path = @cache_manager.get_package_tarball(package_name, tarball_name)
|
143
|
+
|
144
|
+
if tarball_path && File.exist?(tarball_path)
|
145
|
+
# 文件已存在,直接返回
|
146
|
+
return serve_existing_tarball(tarball_path, tarball_name, res, req_id)
|
147
|
+
else
|
148
|
+
# 尝试从包信息动态生成tarball
|
149
|
+
log_verbose "[#{req_id}] 未找到tarball,尝试动态生成: #{package_name} - #{tarball_name}"
|
150
|
+
|
151
|
+
# 从tarball名称中提取版本
|
152
|
+
version_match = tarball_name.match(/#{Regexp.escape(package_name.gsub('/', '-'))}-(.+)\.tgz/)
|
153
|
+
version = version_match ? version_match[1] : nil
|
154
|
+
|
155
|
+
if version
|
156
|
+
# 获取当前包的锁或创建一个新锁
|
157
|
+
package_lock = get_package_lock("#{package_name}-#{version}")
|
158
|
+
|
159
|
+
# 使用锁确保只有一个线程在生成tarball
|
160
|
+
package_lock.synchronize do
|
161
|
+
# 再次检查文件是否存在(可能在我们获取锁的同时被其他线程创建)
|
162
|
+
tarball_path = @cache_manager.get_package_tarball(package_name, tarball_name)
|
163
|
+
if tarball_path && File.exist?(tarball_path)
|
164
|
+
return serve_existing_tarball(tarball_path, tarball_name, res, req_id)
|
165
|
+
end
|
166
|
+
|
167
|
+
# 尝试构建tarball文件
|
168
|
+
if try_build_package_tarball(package_name, version, tarball_name, req_id)
|
169
|
+
# 检查是否生成了tarball
|
170
|
+
tarball_path = @cache_manager.get_package_tarball(package_name, tarball_name)
|
171
|
+
if tarball_path && File.exist?(tarball_path)
|
172
|
+
return serve_existing_tarball(tarball_path, tarball_name, res, req_id)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# 如果动态生成失败,返回404
|
179
|
+
log_verbose "[#{req_id}] 动态生成tarball失败,返回404"
|
180
|
+
handle_file_not_found(package_name, tarball_name, res, req_id)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# 返回已存在的tarball文件
|
185
|
+
def serve_existing_tarball(tarball_path, tarball_name, res, req_id)
|
186
|
+
log_verbose "[#{req_id}] 找到tarball: #{tarball_path}, 大小: #{File.size(tarball_path)}"
|
187
|
+
|
188
|
+
begin
|
189
|
+
file_size = File.size(tarball_path)
|
190
|
+
file_content = File.open(tarball_path, 'rb') { |f| f.read }
|
191
|
+
|
192
|
+
# 计算shasum以确保与包信息API中的值匹配
|
193
|
+
shasum = Digest::SHA1.hexdigest(file_content)
|
194
|
+
log_verbose "[#{req_id}] 文件shasum: #{shasum}"
|
195
|
+
|
196
|
+
res.status = 200
|
197
|
+
res['Content-Type'] = 'application/octet-stream'
|
198
|
+
res['Content-Disposition'] = "attachment; filename=#{tarball_name}"
|
199
|
+
res['Content-Length'] = file_size.to_s
|
200
|
+
|
201
|
+
# 设置ETag和其他下载相关头
|
202
|
+
res['ETag'] = %Q{"#{shasum}"}
|
203
|
+
res['Accept-Ranges'] = 'bytes'
|
204
|
+
|
205
|
+
# 使用字符串形式返回文件内容,避免流式传输的问题
|
206
|
+
res.body = file_content
|
207
|
+
|
208
|
+
log_verbose "[#{req_id}] 成功返回tarball: #{tarball_path}, 大小: #{file_size}"
|
209
|
+
true
|
210
|
+
rescue => e
|
211
|
+
handle_file_read_error(e, res, req_id)
|
212
|
+
false
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# 尝试构建包的tarball文件(不返回响应,仅用于生成文件)
|
217
|
+
def try_build_package_tarball(package_name, version, tarball_name, req_id)
|
218
|
+
# 仅获取这个版本的包信息,避免加载所有版本
|
219
|
+
package_info = nil
|
220
|
+
latest_version = nil
|
221
|
+
is_latest_version = false
|
222
|
+
|
223
|
+
# 尝试从索引中定位特定版本的包信息
|
224
|
+
if @cache_manager.respond_to?(:get_package_versions)
|
225
|
+
package_versions = @cache_manager.get_package_versions(package_name)
|
226
|
+
if package_versions && package_versions['versions'] && package_versions['versions'][version]
|
227
|
+
package_info = package_versions['versions'][version]
|
228
|
+
# 检查是否为最新版本
|
229
|
+
if package_versions['dist-tags'] && package_versions['dist-tags']['latest']
|
230
|
+
latest_version = package_versions['dist-tags']['latest']
|
231
|
+
is_latest_version = (version == latest_version)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# 如果找不到特定版本,使用通用包信息
|
237
|
+
if !package_info && @cache_manager.respond_to?(:get_package_info)
|
238
|
+
package_info = @cache_manager.get_package_info(package_name)
|
239
|
+
if package_info && package_info['version']
|
240
|
+
latest_version = package_info['version']
|
241
|
+
is_latest_version = (version == latest_version)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# 从Git信息中获取克隆所需的信息
|
246
|
+
if package_info && package_info['git'] && package_info['git']['url']
|
247
|
+
git_url = package_info['git']['url']
|
248
|
+
|
249
|
+
# 确定Git标签或分支
|
250
|
+
git_tag = nil
|
251
|
+
git_branch = nil
|
252
|
+
|
253
|
+
# 首先检查是否有特定版本的标签
|
254
|
+
if package_info['versions'] && package_info['versions'][version] &&
|
255
|
+
package_info['versions'][version]['git'] && package_info['versions'][version]['git']['tag']
|
256
|
+
git_tag = package_info['versions'][version]['git']['tag']
|
257
|
+
elsif package_info['git']['tag']
|
258
|
+
git_tag = package_info['git']['tag']
|
259
|
+
else
|
260
|
+
# 否则使用版本号作为标签(常规约定)
|
261
|
+
git_tag = "v#{version}"
|
262
|
+
end
|
263
|
+
|
264
|
+
# 作为后备,使用分支
|
265
|
+
git_branch = package_info['git']['branch'] || 'main'
|
266
|
+
|
267
|
+
log_verbose "[#{req_id}] 将包构建任务添加到队列: #{package_name}@#{version} (#{git_url})"
|
268
|
+
|
269
|
+
# 获取包构建队列单例实例
|
270
|
+
builder_queue = PackageBuilderQueue.instance
|
271
|
+
|
272
|
+
# 设置任务优先级
|
273
|
+
# 下载请求的任务优先级为1(较高),由包信息API触发的构建优先级为0(普通)
|
274
|
+
priority = 1 # 下载请求优先级高
|
275
|
+
|
276
|
+
# 将任务添加到队列
|
277
|
+
builder_queue.enqueue(package_name, version, git_url, git_tag, git_branch, priority) do |success, tarball_path|
|
278
|
+
if success
|
279
|
+
log_verbose "[#{req_id}] 异步包构建成功: #{tarball_path}"
|
280
|
+
else
|
281
|
+
log_verbose "[#{req_id}] 异步包构建失败: #{package_name}@#{version}"
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
# 检查任务是否已经完成(如果已有文件)
|
286
|
+
tarball_path = @cache_manager.get_package_tarball(package_name, tarball_name)
|
287
|
+
if tarball_path && File.exist?(tarball_path)
|
288
|
+
log_verbose "[#{req_id}] 发现tarball文件已构建: #{tarball_path}"
|
289
|
+
return true
|
290
|
+
end
|
291
|
+
|
292
|
+
# 返回 false,因为文件尚未构建完成
|
293
|
+
# 文件将在后台异步构建,下次请求时可能已经可用
|
294
|
+
log_verbose "[#{req_id}] 任务已加入队列,但尚未构建完成: #{package_name}@#{version}"
|
295
|
+
return false
|
296
|
+
else
|
297
|
+
log_verbose "[#{req_id}] 包信息中没有Git URL,无法构建: #{package_name}@#{version}"
|
298
|
+
end
|
299
|
+
|
300
|
+
false # 生成失败
|
301
|
+
end
|
302
|
+
|
303
|
+
# 获取包锁
|
304
|
+
def get_package_lock(package_key)
|
305
|
+
self.class.locks_mutex.synchronize do
|
306
|
+
self.class.package_build_locks[package_key] ||= Mutex.new
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
# 处理文件读取错误
|
311
|
+
def handle_file_read_error(error, res, req_id)
|
312
|
+
UI.error "[#{req_id}] 读取tarball文件时出错: #{error.message}"
|
313
|
+
res.status = 500
|
314
|
+
res['Content-Type'] = 'application/json'
|
315
|
+
res.body = JSON.generate({
|
316
|
+
error: "Internal Server Error",
|
317
|
+
code: "FILE_READ_ERROR",
|
318
|
+
message: "读取文件时出错: #{error.message}"
|
319
|
+
})
|
320
|
+
end
|
321
|
+
|
322
|
+
# 处理文件未找到
|
323
|
+
def handle_file_not_found(package_name, tarball_name, res, req_id)
|
324
|
+
log_verbose "[#{req_id}] 未找到包 #{package_name} 的tarball: #{tarball_name}"
|
325
|
+
res.status = 404
|
326
|
+
res['Content-Type'] = 'application/json'
|
327
|
+
json_response = JSON.generate({
|
328
|
+
error: "Not Found",
|
329
|
+
code: "TARBALL_NOT_FOUND",
|
330
|
+
package: package_name,
|
331
|
+
tarball: tarball_name,
|
332
|
+
message: "无法找到请求的包文件"
|
333
|
+
})
|
334
|
+
res.body = json_response
|
335
|
+
puts_formatted_json(json_response, req_id, "包文件未找到响应") if @verbose
|
336
|
+
end
|
337
|
+
|
338
|
+
# 美化JSON并打印输出
|
339
|
+
def puts_formatted_json(json_string, req_id, desc = "JSON响应")
|
340
|
+
begin
|
341
|
+
# 解析JSON字符串为Ruby对象
|
342
|
+
json_obj = JSON.parse(json_string)
|
343
|
+
# 使用pretty_generate重新生成格式化的JSON
|
344
|
+
formatted_json = JSON.pretty_generate(json_obj)
|
345
|
+
# 打印格式化后的JSON
|
346
|
+
puts "[#{req_id}] #{desc}:"
|
347
|
+
puts formatted_json
|
348
|
+
rescue => e
|
349
|
+
puts "[#{req_id}] 无法格式化JSON: #{e.message}"
|
350
|
+
puts json_string
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
def log_verbose(message)
|
355
|
+
UI.puts message if @verbose
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|