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.
@@ -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