network-infra-utility 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 960f1593215711ad0837083bf676f357760fab6d2cf22947ce482328d1be0cb8
4
- data.tar.gz: 051314c8fe9620ee9a0540e05559e92358002ef6da53dd848ce0d7a62cae1258
3
+ metadata.gz: e8a2b1b8402f7fdb0ba39e7a6d01b384d624e6906530a27c575ae6371bedabfd
4
+ data.tar.gz: 7f0cb17756a53dcc31386fa5aebc786ba3640ca0cd86443c539237c41239d0b8
5
5
  SHA512:
6
- metadata.gz: d7a36a0695efcc11cc60dae6529502ceff7cad727ec4315e45e8966ba28b127baafe76ddc349f49bb859c03b0c33f7ec84a95580905e89b5b470d223bdaff68e
7
- data.tar.gz: b1af08569c4a1a722d09e27e4658c7e7376217d50479cc0bcd3f15c7fe7bde35e9be9be2b30301c8d63dd4d58ab30065bb1b9cc8d825214360e78a1183aa3b74
6
+ metadata.gz: 75901331bb496d6bbd54935256bf01990ddd2221b97415a9210a1133ba03516abbb6ebc4d4550197e17ebeee9593dc0920f90b146fd0ae7f546836bd9a92f4e0
7
+ data.tar.gz: 66e83826b05f0029b781c93d03daf2f7faaf01058d1a404f34bf25a1bf2a68064c43ac95912148e1a9793d7ff05fd3fe80def8265d18f2aa71ceb69f9bc602ce
data/README.md CHANGED
@@ -1,13 +1,3 @@
1
- ---
2
- AIGC:
3
- ContentProducer: '001191110102MAD55U9H0F10002'
4
- ContentPropagator: '001191110102MAD55U9H0F10002'
5
- Label: '1'
6
- ProduceID: 'c63508f8-64ef-4fa8-b091-e1efa3d7ce89'
7
- PropagateID: 'c63508f8-64ef-4fa8-b091-e1efa3d7ce89'
8
- ReservedCode1: '145627d0-3e13-4b6f-ac83-23d7f58c195c'
9
- ReservedCode2: '145627d0-3e13-4b6f-ac83-23d7f58c195c'
10
- ---
11
1
 
12
2
  # Network Infrastructure Utility
13
3
 
@@ -53,5 +43,3 @@ bundle exec rake # 两者都跑(默认)
53
43
  ## License
54
44
 
55
45
  采用 [GNU Affero General Public License v3.0 或更高版本](https://www.gnu.org/licenses/agpl-3.0.html)(AGPL-3.0-or-later),详见 [LICENSE.txt](LICENSE.txt)。
56
-
57
- > AI生成
data/bin/geo-api ADDED
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ # coding: utf-8
4
+ #
5
+ # geo-api — network-infra-utility 的 IP 归属信息查询命令行服务
6
+ #
7
+ # 把 service/geodb 的 GeoAPI 封装为可直接调用的命令行服务,默认监听
8
+ # 0.0.0.0:9292,供本地或同网段主机通过 HTTP 查询 IP 归属信息
9
+ # (asn / city / country 三个接口,详见 service/geodb/GeoAPI.md)。
10
+ #
11
+ # 用法:
12
+ # geo-api # 默认 ./geodb/ + 0.0.0.0:9292
13
+ # geo-api -d /data/ipdb # 指定数据文件目录
14
+ # geo-api -p 8080 # 指定端口
15
+ # geo-api -b 127.0.0.1 -p 9292 -d ./ipdb
16
+ # geo-api --server puma # 指定 Rack 服务器
17
+ # GEODB_DATA_DIR=/data/ipdb geo-api # 通过环境变量指定数据目录
18
+ #
19
+ # 选项:
20
+ # -d, --data-dir DIR IP 数据文件目录,默认 ./geodb/
21
+ # -b, --host HOST 监听地址,默认 0.0.0.0
22
+ # -p, --port PORT 监听端口,默认 9292
23
+ # -s, --server NAME Rack 服务器,默认 puma
24
+ # -h, --help 显示帮助
25
+ # -v, --version 显示版本
26
+ #
27
+ # 数据目录说明:
28
+ # 目录下应至少存在 geoapi 期望的 JSON 文件之一 (asn.json / city-IPv4.json /
29
+ # country-IPv4.json / country-IPv6.json / city-IPv6.json / geo-city.json /
30
+ # geo-country.json)。目录不存在或没有任何 *.json 文件将直接报错退出。
31
+ # 查询时若具体文件缺失,对应接口容错返回 404 无结果,不影响其他接口。
32
+
33
+ require "optparse"
34
+ require "rackup"
35
+
36
+ # 找到 gem 自身根目录 (本文件在 <root>/bin/geo-api)
37
+ ROOT = File.expand_path("..", __dir__)
38
+ $LOAD_PATH.unshift File.join(ROOT, "service", "geodb")
39
+
40
+ # 命令行默认值
41
+ options = {
42
+ data_dir: nil,
43
+ host: "0.0.0.0",
44
+ port: 9292,
45
+ server: "puma"
46
+ }
47
+
48
+ parser = OptionParser.new do |opts|
49
+ opts.banner = <<~BAN
50
+ geo-api — IP 归属信息查询服务 (network-infra-utility)
51
+
52
+ 用法:
53
+ geo-api [options]
54
+ GEODB_DATA_DIR=/data/ipdb geo-api
55
+
56
+ 选项:
57
+ BAN
58
+ opts.on("-d", "--data-dir DIR", "IP 数据文件目录,默认 ./geodb/") { |v| options[:data_dir] = v }
59
+ opts.on("-b", "--host HOST", "监听地址,默认 0.0.0.0") { |v| options[:host] = v }
60
+ opts.on("-p", "--port PORT", Integer, "监听端口,默认 9292") { |v| options[:port] = v }
61
+ opts.on("-s", "--server NAME", "Rack 服务器,默认 puma") { |v| options[:server] = v }
62
+ opts.on("-v", "--version", "显示版本") do
63
+ require_relative "../version"
64
+ puts "geo-api #{NetworkInfraUtility::VERSION}"
65
+ exit 0
66
+ end
67
+ opts.on("-h", "--help", "显示帮助") do
68
+ puts opts
69
+ exit 0
70
+ end
71
+ end
72
+ parser.parse!
73
+
74
+ # 数据目录: 命令行 > 环境变量 > 默认 ./geodb
75
+ data_dir = options[:data_dir] || ENV["GEODB_DATA_DIR"] || "geodb"
76
+ data_dir = File.expand_path(data_dir)
77
+ ENV["GEODB_DATA_DIR"] = data_dir
78
+
79
+ # ---- 启动前校验数据目录 ---------------------------------------------------
80
+ # 目录不存在直接报错退出;目录存在但没有任何 *.json 也视为错误。
81
+ unless File.directory?(data_dir)
82
+ warn "✗ 数据目录不存在: #{data_dir}"
83
+ warn " 请用 -d/--data-dir 指定正确的数据文件目录,或在该位置创建 geodb/ 目录。"
84
+ warn " 期望目录结构: #{data_dir}/ 下含 asn.json / city-IPv4.json /"
85
+ warn " country-IPv4.json / geo-city.json 等 JSON 数据文件。"
86
+ exit 1
87
+ end
88
+
89
+ json_files = Dir.glob(File.join(data_dir, "*.json"))
90
+ if json_files.empty?
91
+ warn "✗ 数据目录 #{data_dir} 下没有任何 JSON 数据文件。"
92
+ warn " 请先将 GeoLite2 CSV 数据通过 service/geodb/geodb.rb 生成为 JSON,"
93
+ warn " 或用 -d/--data-dir 指向已生成 JSON 的目录。"
94
+ exit 1
95
+ end
96
+
97
+ require_relative "../version"
98
+ puts "╭─ geo-api #{NetworkInfraUtility::VERSION} ───────────────────────────────"
99
+ puts "│ 数据目录: #{data_dir} "
100
+ puts "│ #{json_files.length}个数据文件:#{json_files.map { |f| File.basename(f) }.join(' ')}"
101
+ puts "│ 监听: http://#{options[:host]}:#{options[:port]}"
102
+ puts "│ 服务: #{options[:server]}"
103
+ puts "╰─ Ctrl+C 退出 ─────────────────────────────────────"
104
+ $stdout.flush
105
+
106
+ # ---- 启动服务 -------------------------------------------------------------
107
+ require "api" # 即 service/geodb/api.rb
108
+
109
+ Rackup::Server.start(
110
+ app: GeoAPI.app,
111
+ server: options[:server],
112
+ Host: options[:host],
113
+ Port: options[:port]
114
+ )
data/bin/geo-get ADDED
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ # coding: utf-8
4
+ #
5
+ # geo-get — network-infra-utility 的 IP 归属信息命令行查询工具
6
+ #
7
+ # 向运行中的 geo-api 服务发起查询,一次输入 IP,同时拉取 country / city /
8
+ # asn 三类归属信息并汇总输出。默认展示文字,加 -j 则打印 JSON。
9
+ #
10
+ # 用法:
11
+ # geo-get 1.181.240.251 # 默认三接口齐查, 文字格式
12
+ # geo-get 8.8.8.8 -j # JSON 格式输出
13
+ # geo-get 8.8.8.8 --asn # 仅查 asn, 文字
14
+ # geo-get 1.1.1.1 --country --json # 仅查 country, JSON
15
+ #
16
+ # 选项:
17
+ # -t, --text 文字格式输出 (默认)
18
+ # -j, --json JSON 格式输出
19
+ # --country 仅查询国家接口
20
+ # --city 仅查询城市接口
21
+ # --asn 仅查询 ASN 接口
22
+ # (不指定 --country/--city/--asn 时三接口齐查)
23
+ # -h, --help 显示帮助
24
+ # -v, --version 显示版本
25
+ #
26
+ # 默认连 http://127.0.0.1:9292 (geo-api 默认监听)。
27
+ # 服务未运行时给出明确提示与启动命令。
28
+
29
+ require "optparse"
30
+ require "net/http"
31
+ require "json"
32
+ require "uri"
33
+
34
+ HOST = "127.0.0.1"
35
+ PORT = 9292
36
+ BASE = "http://#{HOST}:#{PORT}"
37
+
38
+ # ---- 命令行解析 -------------------------------------------------------------
39
+ options = { format: :text, only: nil }
40
+ parser = OptionParser.new do |opts|
41
+ opts.banner = <<~BAN
42
+ geo-get — IP 归属信息查询工具 (network-infra-utility)
43
+
44
+ 用法:
45
+ geo-get <IP> [options]
46
+ geo-get # 查默认 IP 1.181.240.251, 三接口齐查, 文字
47
+ geo-get 8.8.8.8 -j # JSON 格式
48
+ geo-get 8.8.8.8 --asn # 仅查 asn
49
+
50
+ 选项:
51
+ BAN
52
+ opts.on("-t", "--text", "文字格式输出 (默认)") { options[:format] = :text }
53
+ opts.on("-j", "--json", "JSON 格式输出") { options[:format] = :json }
54
+ opts.on("--country", "仅查询国家接口") { options[:only] = :country }
55
+ opts.on("--city", "仅查询城市接口") { options[:only] = :city }
56
+ opts.on("--asn", "仅查询 ASN 接口") { options[:only] = :asn }
57
+ opts.on("-v", "--version", "显示版本") do
58
+ require_relative "../version"
59
+ puts "geo-get #{NetworkInfraUtility::VERSION}"
60
+ exit 0
61
+ end
62
+ opts.on("-h", "--help", "显示帮助") do
63
+ puts opts
64
+ exit 0
65
+ end
66
+ end
67
+ parser.parse!
68
+
69
+ # 第一个非 -- 参数当 IP; 不给就用默认演示 IP
70
+ ip = ARGV.find { |a| !a.start_with?("-") } || "1.181.240.251"
71
+
72
+ only = options[:only]
73
+ do_country = only.nil? || only == :country
74
+ do_city = only.nil? || only == :city
75
+ do_asn = only.nil? || only == :asn
76
+
77
+ # ---- HTTP 取数 --------------------------------------------------------------
78
+ # 发起一次 GET, 返回 {code:, body:}
79
+ def fetch(base, path, params)
80
+ uri = URI("#{base}#{path}")
81
+ uri.query = URI.encode_www_form(params) unless params.empty?
82
+ res = Net::HTTP.get_response(uri)
83
+ { code: res.code.to_i, body: (JSON.parse(res.body) rescue res.body) }
84
+ rescue Errno::ECONNREFUSED
85
+ abort "✗ 无法连接 #{base} —— geo-api 服务未启动。\n 启动命令: geo-api -d E:/workspace/momentum/twinklite/data/geodb"
86
+ rescue => e
87
+ abort "✗ 请求失败: #{e.class}: #{e.message}"
88
+ end
89
+
90
+ # ---- 按接口拉取 + 收集 ------------------------------------------------------
91
+ results = {}
92
+ results[:country] = fetch(BASE, "/geo/country", addr: ip) if do_country
93
+ results[:city] = fetch(BASE, "/geo/city", addr: ip) if do_city
94
+ results[:asn] = fetch(BASE, "/geo/asn", addr: ip) if do_asn
95
+
96
+ # ---- 输出: --json / --text 两路分发 -----------------------------------------
97
+ case options[:format]
98
+ when :json
99
+ # JSON 格式: 把每个接口的响应原样放进一个对象打印。
100
+ # 命中的接口放解析后的 body; 404/无结果放 null, 不逐条刷错误。
101
+ payload = {}
102
+ results.each do |key, r|
103
+ payload[key] = (r[:code] == 200 ? r[:body] : nil)
104
+ end
105
+ puts JSON.pretty_generate(payload)
106
+ when :text
107
+ puts "查询 IP: #{ip} 范围: #{only ? "仅 #{only}" : 'country / city / asn 三接口齐查'} 服务: #{BASE}"
108
+ puts "-" * 60
109
+
110
+ hits = []
111
+ if do_country && (r = results[:country]) && r[:code] == 200
112
+ d = r[:body]
113
+ g = d["geoname"]; rc = d["registered_country"]
114
+ country = (g && g["country_name"]) || (rc && rc["country_name"]) || "未知"
115
+ hits << "[国家] #{country} (#{(g && g["country_iso_code"]) || (rc && rc["country_iso_code"])}) 网段 #{d["network"]}"
116
+ end
117
+ if do_city && (r = results[:city]) && r[:code] == 200
118
+ d = r[:body]
119
+ g = d["geoname"]
120
+ city = (g && (g["city_namezh"] || g["city_name"])) || "未知"
121
+ coord = g && g["latitude"] && g["longitude"] ? "#{g["latitude"]}, #{g["longitude"]}" : "无"
122
+ hits << "[城市] #{city} 坐标 #{coord} 网段 #{d["network"]}"
123
+ end
124
+ if do_asn && (r = results[:asn]) && r[:code] == 200
125
+ d = r[:body]
126
+ hits << "[ASN] AS#{d["autonomous_system_number"]} #{d["autonomous_system_organization"]} 网段 #{d["network"]}"
127
+ end
128
+
129
+ if hits.empty?
130
+ puts "#{ip} 未查到归属信息 (该 IP 不在已知数据范围内, 如回环/保留地址)"
131
+ else
132
+ puts "#{ip} 的归属信息:"
133
+ hits.each { |h| puts " #{h}" }
134
+ end
135
+ end
data/bin/geo-load ADDED
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ # coding: utf-8
4
+ #
5
+ # geo-load — network-infra-utility 的 GeoLite2 CSV → JSON 转换命令行工具
6
+ #
7
+ # 把 GeoLite2 解压后的 CSV 目录转换为 geo-api 可用的 JSON 数据文件。
8
+ # 一次调用依次执行 load_asn / load_city / load_country,生成 7 个 JSON:
9
+ # asn.json / city-IPv4.json / city-IPv6.json
10
+ # country-IPv4.json / country-IPv6.json / geo-city.json / geo-country.json
11
+ #
12
+ # 用法:
13
+ # geo-load # 当前目录找 CSV, 输出到 ./geodb/
14
+ # geo-load /data/GeoLite2-CSV # 指定 CSV 目录
15
+ # geo-load /data/GeoLite2-CSV /var/lib/geodb # 指定输入+输出
16
+ #
17
+ # 参数规则:
18
+ # geo-load [RAW_DIR] [DOC_DIR]
19
+ #
20
+ # RAW_DIR (第一个参数, 可省):
21
+ # 不写 → 在当前工作目录下查找 GeoLite2 CSV 目录, 找不到报错
22
+ # 写了 → 用指定路径; 路径不存在报错
23
+ #
24
+ # DOC_DIR (第二个参数, 可省):
25
+ # 不写 → 默认在当前工作目录下生成 geodb/ 子目录
26
+ # 写了 → 用指定路径; 路径不存在则自动创建, 创建不了报错
27
+ #
28
+ # 选项:
29
+ # --asn-only 仅转换 ASN 数据
30
+ # --city-only 仅转换 City 数据
31
+ # --country-only 仅转换 Country 数据
32
+ # (不指定时三类全转)
33
+ # -h, --help 显示帮助
34
+ # -v, --version 显示版本
35
+
36
+ require "optparse"
37
+
38
+ # ---- 命令行解析 -------------------------------------------------------------
39
+ options = { only: nil }
40
+ parser = OptionParser.new do |opts|
41
+ opts.banner = <<~BAN
42
+ geo-load — GeoLite2 CSV → JSON 转换工具 (network-infra-utility)
43
+
44
+ 用法:
45
+ geo-load [RAW_DIR] [DOC_DIR]
46
+ geo-load # 当前目录找 CSV, 输出到 ./geodb/
47
+ geo-load /data/GeoLite2-CSV # 指定 CSV 目录
48
+ geo-load /data/GeoLite2-CSV /opt/geodb
49
+
50
+ 参数:
51
+ RAW_DIR GeoLite2 CSV 目录 (可省, 省则在当前目录查找)
52
+ DOC_DIR JSON 输出目录 (可省, 省则输出到 ./geodb/)
53
+
54
+ 选项:
55
+ BAN
56
+ opts.on("--asn-only", "仅转换 ASN 数据") { options[:only] = :asn }
57
+ opts.on("--city-only", "仅转换 City 数据") { options[:only] = :city }
58
+ opts.on("--country-only", "仅转换 Country 数据") { options[:only] = :country }
59
+ opts.on("-v", "--version", "显示版本") do
60
+ require_relative "../version"
61
+ puts "geo-load #{NetworkInfraUtility::VERSION}"
62
+ exit 0
63
+ end
64
+ opts.on("-h", "--help", "显示帮助") do
65
+ puts opts
66
+ exit 0
67
+ end
68
+ end
69
+ parser.parse!
70
+
71
+ # 取位置参数: 第一个非 -- 参数是 RAW_DIR, 第二个是 DOC_DIR
72
+ positional = ARGV.reject { |a| a.start_with?("-") }
73
+ raw_arg = positional[0]
74
+ doc_arg = positional[1]
75
+
76
+ # ---- 确定 RAW_DIR (输入目录) ------------------------------------------------
77
+ # GeoLite2 解压后的 CSV 目录特征: 含 GeoLite2-ASN-CSV_* / GeoLite2-City-CSV_* /
78
+ # GeoLite2-Country-CSV_* 子目录, 或本身就是这些子目录之一。
79
+ GEO_PATTERN = /GeoLite2-(ASN|City|Country)-CSV/i
80
+
81
+ if raw_arg
82
+ # 用户显式指定了 RAW_DIR
83
+ raw_dir = File.expand_path(raw_arg)
84
+ unless File.directory?(raw_dir)
85
+ abort "✗ 输入目录不存在: #{raw_dir}\n 请指定有效的 GeoLite2 CSV 解压目录。"
86
+ end
87
+ else
88
+ # 没指定 → 在当前工作目录下查找
89
+ cwd = Dir.pwd
90
+ # 先看当前目录本身是否符合
91
+ candidates = []
92
+ if cwd =~ GEO_PATTERN || Dir.glob(File.join(cwd, "GeoLite2-*-CSV_*")).any?
93
+ candidates << cwd
94
+ end
95
+ # 再看子目录
96
+ Dir.glob(File.join(cwd, "*")).each do |p|
97
+ candidates << p if File.directory?(p) && (p =~ GEO_PATTERN || Dir.glob(File.join(p, "GeoLite2-*-CSV_*")).any?)
98
+ end
99
+ # 也看下一层 (常见: cwd/GeoLite2-CSV_xxx/GeoLite2-ASN-CSV_xxx/)
100
+ Dir.glob(File.join(cwd, "*", "*")).each do |p|
101
+ candidates << p if File.directory?(p) && p =~ GEO_PATTERN
102
+ end
103
+
104
+ if candidates.empty?
105
+ abort <<~MSG
106
+ ✗ 在当前目录下未找到 GeoLite2 CSV 数据:
107
+ #{cwd}
108
+ 请将 GeoLite2 CSV 解压到当前目录, 或显式指定路径:
109
+ geo-load /path/to/GeoLite2-CSV [DOC_DIR]
110
+ 期望的目录结构:
111
+ GeoLite2-ASN-CSV_*/ GeoLite2-City-CSV_*/ GeoLite2-Country-CSV_*/
112
+ MSG
113
+ end
114
+
115
+ # 去重, 取包含最多 CSV 类型的目录 (优先选父目录)
116
+ raw_dir = candidates.uniq.max_by { |d| Dir.glob(File.join(d, "**", "*.csv")).length }
117
+ end
118
+
119
+ # ---- 确定 DOC_DIR (输出目录) ------------------------------------------------
120
+ if doc_arg
121
+ doc_dir = File.expand_path(doc_arg)
122
+ # 指定了路径: 不存在则创建, 创建不了报错
123
+ unless File.directory?(doc_dir)
124
+ begin
125
+ require "fileutils"
126
+ FileUtils.mkdir_p(doc_dir)
127
+ rescue SystemCallError => e
128
+ abort "✗ 无法创建输出目录 #{doc_dir}: #{e.message}"
129
+ end
130
+ end
131
+ else
132
+ # 没指定 → 默认在当前工作目录下生成 geodb/
133
+ doc_dir = File.expand_path("geodb", Dir.pwd)
134
+ begin
135
+ require "fileutils"
136
+ FileUtils.mkdir_p(doc_dir)
137
+ rescue SystemCallError => e
138
+ abort "✗ 无法创建输出目录 #{doc_dir}: #{e.message}"
139
+ end
140
+ end
141
+
142
+ # ---- 加载 GeoDB 模块 --------------------------------------------------------
143
+ # geodb.rb 依赖 cc + network + CC.use, 需要把 service/geodb 加入加载路径
144
+ ROOT = File.expand_path("..", __dir__)
145
+ $LOAD_PATH.unshift File.join(ROOT, "service", "geodb")
146
+
147
+ require "cc"
148
+ require "network"
149
+ CC.use "file", "enum", "shell-tools"
150
+ require "geodb"
151
+
152
+ # ---- 执行转换 ---------------------------------------------------------------
153
+ do_asn = options[:only].nil? || options[:only] == :asn
154
+ do_city = options[:only].nil? || options[:only] == :city
155
+ do_country = options[:only].nil? || options[:only] == :country
156
+
157
+ puts "╭─ geo-load #{NetworkInfraUtility::VERSION} ───────────────────────────────"
158
+ puts "│ 输入目录: #{raw_dir}"
159
+ puts "│ 输出目录: #{doc_dir}"
160
+ puts "│ 转换范围: #{options[:only] ? "仅 #{options[:only]}" : 'asn / city / country 全量'}"
161
+ puts "╰─ 开始转换 ─────────────────────────────────────"
162
+ $stdout.flush
163
+
164
+ # GeoDB 的三个方法用 Dir[glob] 匹配 CSV, 路径模式相对于调用目录
165
+ # 把 raw_dir + glob 拼成绝对路径模式
166
+ asn_pattern = File.join(raw_dir, "**", "GeoLite2-ASN-CSV_*", "*.csv")
167
+ city_pattern = File.join(raw_dir, "**", "GeoLite2-City-CSV_*", "*.csv")
168
+ country_pattern = File.join(raw_dir, "**", "GeoLite2-Country-CSV_*", "*.csv")
169
+
170
+ # out_path 需要以 / 结尾 (geodb.rb 里拼 "#{out_path}asn.json")
171
+ out_path = doc_dir.end_with?("/", "\\") ? doc_dir : doc_dir + "/"
172
+
173
+ if do_asn
174
+ puts "--- ASN ---"
175
+ GeoDB.load_asn(asn_pattern, out_path)
176
+ end
177
+ if do_city
178
+ puts "--- City ---"
179
+ GeoDB.load_city(city_pattern, out_path)
180
+ end
181
+ if do_country
182
+ puts "--- Country ---"
183
+ GeoDB.load_country(country_pattern, out_path)
184
+ end
185
+
186
+ puts "╭─ 完成 ───────────────────────────────────────"
187
+ puts "│ JSON 文件已生成到: #{doc_dir}"
188
+ puts "│ 文件列表:"
189
+ Dir.glob(File.join(doc_dir, "*.json")).each { |f| puts "│ #{File.basename(f)}" }
190
+ puts "╰─ 可用 geo-api -d #{doc_dir} 启动查询服务 ────"
@@ -21,7 +21,14 @@ Gem::Specification.new do |spec|
21
21
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
22
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features|example)/}) }
23
23
  end
24
- spec.bindir = "exe"
25
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
24
+ # bin/ 既放对外命令 (geo-api) 也放开发脚本 (console/setup),
25
+ # executables 显式声明,避免把开发脚本当作系统命令安装到用户 PATH。
26
+ spec.bindir = "bin"
27
+ spec.executables = %w[geo-api geo-get geo-load]
26
28
  spec.require_paths = ["document", "service", "support", "tool", "."]
29
+
30
+ # geo-api 命令行服务依赖的运行时 gem
31
+ spec.add_runtime_dependency "roda", "~> 3.0"
32
+ spec.add_runtime_dependency "rackup", "~> 2.0"
33
+ spec.add_runtime_dependency "puma", "~> 6.0"
27
34
  end
data/network.rb CHANGED
@@ -12,7 +12,7 @@ require_relative "version"
12
12
  # 加载顺序按依赖方向上游在前,避免循环依赖。
13
13
  module NetworkInfraUtility
14
14
  # 支撑层:无外部依赖的基础工具,最先加载
15
- # require_relative "support/xxx"
15
+ require_relative "support/basic/ip"
16
16
 
17
17
  # 工具层:依赖 support
18
18
  # require_relative "tool/xxx"