@templmf/temp-solf-lmf 0.0.162 → 0.0.164
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.
- package/aws/README.md +70 -0
- package/aws/S3BucketExplorer.java +371 -0
- package/aws/pom.xml +60 -0
- package/package.json +1 -1
- package/server/env.example +0 -13
- package/server/package-lock.json +0 -861
- package/server/package.json +0 -17
- package/server/public/index.html +0 -683
- package/server/src/index.js +0 -269
- package/server/src/opencodeClient.js +0 -40
- package/server/src/ownership.js +0 -60
package/aws/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# S3 Bucket Explorer
|
|
2
|
+
|
|
3
|
+
基于 `aws-java-sdk-s3` (1.12.194) 编写的小工具,用于探查/解析 S3 bucket 中的对象数据。
|
|
4
|
+
|
|
5
|
+
## 构建
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
mvn clean package
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
构建完成后会在 `target/` 目录生成一个包含全部依赖的可执行 jar:
|
|
12
|
+
`target/s3-bucket-parser-1.0.0.jar`
|
|
13
|
+
|
|
14
|
+
## 使用
|
|
15
|
+
|
|
16
|
+
### 1. 列出 bucket 下的对象
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
java -jar target/s3-bucket-parser-1.0.0.jar list \
|
|
20
|
+
--bucket my-bucket \
|
|
21
|
+
--prefix logs/2026/ \
|
|
22
|
+
--region ap-northeast-1
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 2. 读取单个对象内容(自动识别并格式化 JSON)
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
java -jar target/s3-bucket-parser-1.0.0.jar read \
|
|
29
|
+
--bucket my-bucket \
|
|
30
|
+
--key logs/2026/09/10/data.json \
|
|
31
|
+
--region ap-northeast-1
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 3. 体检/诊断对象(排查为什么读出来是二进制)
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
java -jar target/s3-bucket-parser-1.0.0.jar inspect \
|
|
38
|
+
--bucket my-bucket \
|
|
39
|
+
--key logs/2026/09/10/data.bin \
|
|
40
|
+
--region ap-northeast-1
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
会打印:Content-Type / Content-Encoding / 是否有服务端加密(SSE)/ 自定义元数据,
|
|
44
|
+
以及内容开头 64 字节的十六进制 + ASCII 对照视图,并根据文件头 magic number
|
|
45
|
+
自动猜测格式(gzip / zip / parquet / png / jpeg / pdf / 明文 JSON / 明文文本 /
|
|
46
|
+
加密或自定义二进制格式)。
|
|
47
|
+
|
|
48
|
+
### 4. 批量下载对象到本地
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
java -jar target/s3-bucket-parser-1.0.0.jar download \
|
|
52
|
+
--bucket my-bucket \
|
|
53
|
+
--prefix logs/2026/ \
|
|
54
|
+
--out ./downloaded \
|
|
55
|
+
--region ap-northeast-1
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 认证方式(三选一)
|
|
59
|
+
|
|
60
|
+
1. **默认凭证链**(推荐):不传任何认证参数,SDK 会依次尝试环境变量
|
|
61
|
+
(`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`)、`~/.aws/credentials`、
|
|
62
|
+
EC2/ECS/Lambda 角色等。
|
|
63
|
+
2. **指定 profile**:`--profile myprofile`,对应 `~/.aws/credentials` 中的配置段。
|
|
64
|
+
3. **直接传 AK/SK**(仅建议本地临时测试):`--access-key xxx --secret-key yyy`。
|
|
65
|
+
|
|
66
|
+
## 说明
|
|
67
|
+
|
|
68
|
+
- 已针对 `ListObjectsV2` 做了分页处理,可以安全用于对象数量很大的 bucket。
|
|
69
|
+
- `read` 命令会尝试把内容解析成 JSON 并美化打印;如果不是合法 JSON,则原样打印文本内容。
|
|
70
|
+
- 如果对象是二进制文件(如图片、压缩包),建议直接用 `download` 命令而不是 `read`。
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
package com.example.s3tool;
|
|
2
|
+
|
|
3
|
+
import com.amazonaws.auth.AWSStaticCredentialsProvider;
|
|
4
|
+
import com.amazonaws.auth.BasicAWSCredentials;
|
|
5
|
+
import com.amazonaws.auth.profile.ProfileCredentialsProvider;
|
|
6
|
+
import com.amazonaws.services.s3.AmazonS3;
|
|
7
|
+
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
|
|
8
|
+
import com.amazonaws.services.s3.model.ListObjectsV2Request;
|
|
9
|
+
import com.amazonaws.services.s3.model.ListObjectsV2Result;
|
|
10
|
+
import com.amazonaws.services.s3.model.ObjectMetadata;
|
|
11
|
+
import com.amazonaws.services.s3.model.S3Object;
|
|
12
|
+
import com.amazonaws.services.s3.model.S3ObjectSummary;
|
|
13
|
+
import com.fasterxml.jackson.databind.JsonNode;
|
|
14
|
+
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
15
|
+
|
|
16
|
+
import java.io.BufferedReader;
|
|
17
|
+
import java.io.IOException;
|
|
18
|
+
import java.io.InputStream;
|
|
19
|
+
import java.io.InputStreamReader;
|
|
20
|
+
import java.nio.charset.StandardCharsets;
|
|
21
|
+
import java.nio.file.Files;
|
|
22
|
+
import java.nio.file.Path;
|
|
23
|
+
import java.nio.file.Paths;
|
|
24
|
+
import java.time.format.DateTimeFormatter;
|
|
25
|
+
import java.util.ArrayList;
|
|
26
|
+
import java.util.List;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* S3 Bucket 数据探查/解析小工具。
|
|
30
|
+
*
|
|
31
|
+
* 功能:
|
|
32
|
+
* 1. 列出 bucket(可按前缀过滤)下的所有对象,打印 key / 大小 / 最后修改时间
|
|
33
|
+
* 2. 读取指定对象内容并打印(自动识别 JSON 并格式化)
|
|
34
|
+
* 3. 批量下载 bucket(或某前缀)下的所有对象到本地目录
|
|
35
|
+
*
|
|
36
|
+
* 用法:
|
|
37
|
+
* list 列出对象
|
|
38
|
+
* java -jar s3-bucket-parser.jar list --bucket my-bucket [--prefix logs/] [--region ap-northeast-1] [--profile myprofile]
|
|
39
|
+
*
|
|
40
|
+
* read 读取单个对象内容并打印
|
|
41
|
+
* java -jar s3-bucket-parser.jar read --bucket my-bucket --key path/to/file.json [--region ap-northeast-1]
|
|
42
|
+
*
|
|
43
|
+
* download 批量下载
|
|
44
|
+
* java -jar s3-bucket-parser.jar download --bucket my-bucket [--prefix logs/] --out ./downloaded [--region ap-northeast-1]
|
|
45
|
+
*
|
|
46
|
+
* 认证方式(任选其一):
|
|
47
|
+
* - 不传 --profile / --access-key 时,使用默认凭证链(环境变量 / ~/.aws/credentials / EC2、ECS 角色等)
|
|
48
|
+
* - 传 --profile 指定 ~/.aws/credentials 中的某个 profile
|
|
49
|
+
* - 传 --access-key 和 --secret-key 直接指定(不推荐用于生产环境,仅方便本地测试)
|
|
50
|
+
*/
|
|
51
|
+
public class S3BucketExplorer {
|
|
52
|
+
|
|
53
|
+
public static void main(String[] args) {
|
|
54
|
+
if (args.length == 0) {
|
|
55
|
+
printUsageAndExit();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
String command = args[0];
|
|
59
|
+
Args parsed = Args.parse(args, 1);
|
|
60
|
+
|
|
61
|
+
String bucket = parsed.get("bucket");
|
|
62
|
+
if (bucket == null) {
|
|
63
|
+
System.err.println("错误:必须通过 --bucket 指定存储桶名称");
|
|
64
|
+
printUsageAndExit();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
String region = parsed.getOrDefault("region", "ap-northeast-1");
|
|
68
|
+
AmazonS3 s3 = buildClient(parsed, region);
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
switch (command) {
|
|
72
|
+
case "list":
|
|
73
|
+
listObjects(s3, bucket, parsed.get("prefix"));
|
|
74
|
+
break;
|
|
75
|
+
case "read":
|
|
76
|
+
String key = parsed.get("key");
|
|
77
|
+
if (key == null) {
|
|
78
|
+
System.err.println("错误:read 命令必须通过 --key 指定对象 key");
|
|
79
|
+
printUsageAndExit();
|
|
80
|
+
}
|
|
81
|
+
readObject(s3, bucket, key);
|
|
82
|
+
break;
|
|
83
|
+
case "inspect":
|
|
84
|
+
String inspectKey = parsed.get("key");
|
|
85
|
+
if (inspectKey == null) {
|
|
86
|
+
System.err.println("错误:inspect 命令必须通过 --key 指定对象 key");
|
|
87
|
+
printUsageAndExit();
|
|
88
|
+
}
|
|
89
|
+
inspectObject(s3, bucket, inspectKey);
|
|
90
|
+
break;
|
|
91
|
+
case "download":
|
|
92
|
+
String outDir = parsed.getOrDefault("out", "./downloaded");
|
|
93
|
+
downloadAll(s3, bucket, parsed.get("prefix"), outDir);
|
|
94
|
+
break;
|
|
95
|
+
default:
|
|
96
|
+
System.err.println("未知命令: " + command);
|
|
97
|
+
printUsageAndExit();
|
|
98
|
+
}
|
|
99
|
+
} finally {
|
|
100
|
+
s3.shutdown();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 构建 S3 客户端,按优先级选择凭证方式 */
|
|
105
|
+
private static AmazonS3 buildClient(Args parsed, String region) {
|
|
106
|
+
AmazonS3ClientBuilder builder = AmazonS3ClientBuilder.standard().withRegion(region);
|
|
107
|
+
|
|
108
|
+
String accessKey = parsed.get("access-key");
|
|
109
|
+
String secretKey = parsed.get("secret-key");
|
|
110
|
+
String profile = parsed.get("profile");
|
|
111
|
+
|
|
112
|
+
if (accessKey != null && secretKey != null) {
|
|
113
|
+
builder.withCredentials(new AWSStaticCredentialsProvider(
|
|
114
|
+
new BasicAWSCredentials(accessKey, secretKey)));
|
|
115
|
+
} else if (profile != null) {
|
|
116
|
+
builder.withCredentials(new ProfileCredentialsProvider(profile));
|
|
117
|
+
}
|
|
118
|
+
// 都不传则走默认凭证链(DefaultAWSCredentialsProviderChain)
|
|
119
|
+
|
|
120
|
+
return builder.build();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 列出 bucket 下的对象,自动处理分页 */
|
|
124
|
+
private static void listObjects(AmazonS3 s3, String bucket, String prefix) {
|
|
125
|
+
System.out.printf("正在列出 bucket=%s prefix=%s 下的对象...%n%n", bucket, prefix == null ? "(全部)" : prefix);
|
|
126
|
+
|
|
127
|
+
DateTimeFormatter fmt = DateTimeFormatter.ISO_INSTANT;
|
|
128
|
+
long totalCount = 0;
|
|
129
|
+
long totalSize = 0;
|
|
130
|
+
|
|
131
|
+
ListObjectsV2Request request = new ListObjectsV2Request()
|
|
132
|
+
.withBucketName(bucket)
|
|
133
|
+
.withPrefix(prefix)
|
|
134
|
+
.withMaxKeys(1000);
|
|
135
|
+
|
|
136
|
+
ListObjectsV2Result result;
|
|
137
|
+
System.out.println(padRight("KEY", 60) + padRight("SIZE(bytes)", 15) + "LAST MODIFIED");
|
|
138
|
+
System.out.println("-".repeat(100));
|
|
139
|
+
|
|
140
|
+
do {
|
|
141
|
+
result = s3.listObjectsV2(request);
|
|
142
|
+
for (S3ObjectSummary summary : result.getObjectSummaries()) {
|
|
143
|
+
System.out.println(
|
|
144
|
+
padRight(summary.getKey(), 60)
|
|
145
|
+
+ padRight(String.valueOf(summary.getSize()), 15)
|
|
146
|
+
+ fmt.format(summary.getLastModified().toInstant())
|
|
147
|
+
);
|
|
148
|
+
totalCount++;
|
|
149
|
+
totalSize += summary.getSize();
|
|
150
|
+
}
|
|
151
|
+
request.setContinuationToken(result.getNextContinuationToken());
|
|
152
|
+
} while (result.isTruncated());
|
|
153
|
+
|
|
154
|
+
System.out.println("-".repeat(100));
|
|
155
|
+
System.out.printf("共 %d 个对象,总大小 %.2f MB%n", totalCount, totalSize / 1024.0 / 1024.0);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 读取单个对象内容并打印,若能解析为 JSON 则格式化输出 */
|
|
159
|
+
private static void readObject(AmazonS3 s3, String bucket, String key) {
|
|
160
|
+
try (S3Object obj = s3.getObject(bucket, key)) {
|
|
161
|
+
ObjectMetadata meta = obj.getObjectMetadata();
|
|
162
|
+
System.out.printf("对象: s3://%s/%s%n", bucket, key);
|
|
163
|
+
System.out.printf("大小: %d bytes | Content-Type: %s%n%n", meta.getContentLength(), meta.getContentType());
|
|
164
|
+
|
|
165
|
+
String content = readAll(obj.getObjectContent());
|
|
166
|
+
|
|
167
|
+
// 尝试按 JSON 格式化,失败则原样打印为文本
|
|
168
|
+
try {
|
|
169
|
+
ObjectMapper mapper = new ObjectMapper();
|
|
170
|
+
JsonNode node = mapper.readTree(content);
|
|
171
|
+
System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(node));
|
|
172
|
+
} catch (IOException notJson) {
|
|
173
|
+
System.out.println(content);
|
|
174
|
+
}
|
|
175
|
+
} catch (IOException e) {
|
|
176
|
+
System.err.println("读取对象失败: " + e.getMessage());
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* “体检”一个对象:打印元数据(Content-Type / Content-Encoding / 自定义 x-amz-meta-*)
|
|
182
|
+
* 以及内容开头若干字节的十六进制 + ASCII 视图,并尝试根据 magic number 猜测真实格式。
|
|
183
|
+
* 用于排查“为什么读出来是二进制”这类问题,不需要猜测数据经过了什么处理。
|
|
184
|
+
*/
|
|
185
|
+
private static void inspectObject(AmazonS3 s3, String bucket, String key) {
|
|
186
|
+
try (S3Object obj = s3.getObject(bucket, key)) {
|
|
187
|
+
ObjectMetadata meta = obj.getObjectMetadata();
|
|
188
|
+
|
|
189
|
+
System.out.printf("对象: s3://%s/%s%n", bucket, key);
|
|
190
|
+
System.out.println("-".repeat(60));
|
|
191
|
+
System.out.println("== 元数据 ==");
|
|
192
|
+
System.out.println("Content-Length : " + meta.getContentLength());
|
|
193
|
+
System.out.println("Content-Type : " + meta.getContentType());
|
|
194
|
+
System.out.println("Content-Encoding : " + meta.getContentEncoding()); // 若为 gzip,这里通常会显示 "gzip"
|
|
195
|
+
System.out.println("ETag : " + meta.getETag());
|
|
196
|
+
System.out.println("SSE 算法 : " + meta.getSSEAlgorithm()); // 非 null 说明用了 SSE-S3/SSE-KMS 服务端加密
|
|
197
|
+
|
|
198
|
+
if (!meta.getUserMetadata().isEmpty()) {
|
|
199
|
+
System.out.println("自定义元数据(x-amz-meta-*):");
|
|
200
|
+
meta.getUserMetadata().forEach((k, v) -> System.out.println(" " + k + " = " + v));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
System.out.println();
|
|
204
|
+
System.out.println("== 内容开头 64 字节 (hex / ascii) ==");
|
|
205
|
+
byte[] head = new byte[64];
|
|
206
|
+
int read;
|
|
207
|
+
try (InputStream in = obj.getObjectContent()) {
|
|
208
|
+
read = in.readNBytes(head, 0, head.length);
|
|
209
|
+
}
|
|
210
|
+
printHexDump(head, read);
|
|
211
|
+
|
|
212
|
+
System.out.println();
|
|
213
|
+
System.out.println("== 格式猜测 ==");
|
|
214
|
+
System.out.println(guessFormat(head, read));
|
|
215
|
+
|
|
216
|
+
} catch (IOException e) {
|
|
217
|
+
System.err.println("读取对象失败: " + e.getMessage());
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** 打印十六进制 + ASCII 对照视图,类似 `xxd` */
|
|
222
|
+
private static void printHexDump(byte[] data, int len) {
|
|
223
|
+
for (int offset = 0; offset < len; offset += 16) {
|
|
224
|
+
StringBuilder hex = new StringBuilder();
|
|
225
|
+
StringBuilder ascii = new StringBuilder();
|
|
226
|
+
int lineEnd = Math.min(offset + 16, len);
|
|
227
|
+
for (int i = offset; i < lineEnd; i++) {
|
|
228
|
+
hex.append(String.format("%02x ", data[i]));
|
|
229
|
+
char c = (char) (data[i] & 0xFF);
|
|
230
|
+
ascii.append(c >= 32 && c < 127 ? c : '.');
|
|
231
|
+
}
|
|
232
|
+
System.out.printf("%04x %-48s %s%n", offset, hex, ascii);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** 根据文件头 magic number 猜测常见格式;识别不了的会提示如何进一步排查 */
|
|
237
|
+
private static String guessFormat(byte[] head, int len) {
|
|
238
|
+
if (len >= 2 && (head[0] & 0xFF) == 0x1f && (head[1] & 0xFF) == 0x8b) {
|
|
239
|
+
return "GZIP 压缩数据(很可能是写入前做了 gzip 压缩,需要先解压才能看到明文)";
|
|
240
|
+
}
|
|
241
|
+
if (len >= 4 && head[0] == 'P' && head[1] == 'K' && head[2] == 0x03 && head[3] == 0x04) {
|
|
242
|
+
return "ZIP 压缩包(可能是 zip,或基于 zip 的容器格式如 xlsx/docx/jar)";
|
|
243
|
+
}
|
|
244
|
+
if (len >= 4 && (head[0] & 0xFF) == 0x50 && (head[1] & 0xFF) == 0x41 && (head[2] & 0xFF) == 0x52 && (head[3] & 0xFF) == 0x31) {
|
|
245
|
+
return "Parquet 文件(文件头是 'PAR1')";
|
|
246
|
+
}
|
|
247
|
+
if (len >= 8 && (head[0] & 0xFF) == 0x89 && head[1] == 'P' && head[2] == 'N' && head[3] == 'G') {
|
|
248
|
+
return "PNG 图片";
|
|
249
|
+
}
|
|
250
|
+
if (len >= 3 && (head[0] & 0xFF) == 0xFF && (head[1] & 0xFF) == 0xD8 && (head[2] & 0xFF) == 0xFF) {
|
|
251
|
+
return "JPEG 图片";
|
|
252
|
+
}
|
|
253
|
+
if (len >= 4 && head[0] == '%' && head[1] == 'P' && head[2] == 'D' && head[3] == 'F') {
|
|
254
|
+
return "PDF 文件";
|
|
255
|
+
}
|
|
256
|
+
if (len >= 1 && (head[0] == '{' || head[0] == '[')) {
|
|
257
|
+
return "很可能是 JSON 明文(以 { 或 [ 开头)";
|
|
258
|
+
}
|
|
259
|
+
boolean looksLikeText = true;
|
|
260
|
+
for (int i = 0; i < len; i++) {
|
|
261
|
+
int b = head[i] & 0xFF;
|
|
262
|
+
if (b == 9 || b == 10 || b == 13) continue; // tab/换行/回车
|
|
263
|
+
if (b < 32 || b > 126) {
|
|
264
|
+
looksLikeText = false;
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (looksLikeText && len > 0) {
|
|
269
|
+
return "开头字节都在可打印 ASCII 范围内,很可能就是明文文本(如果整体看起来仍是乱码,也可能是明文但非 UTF-8 编码,比如 GBK)";
|
|
270
|
+
}
|
|
271
|
+
return "未识别出已知的文件头格式。字节分布随机、没有规律的话,通常意味着:\n"
|
|
272
|
+
+ " 1) 经过了加密(客户端加密或 SSE-C/SSE-KMS 服务端加密返回的是密文,需要用对应密钥解密),或\n"
|
|
273
|
+
+ " 2) 是某种自定义/私有的二进制序列化格式(如自定义 Protobuf/Thrift schema),需要对应的解析代码才能还原成明文。\n"
|
|
274
|
+
+ "建议:对照写入这份数据的那一端代码(生产者/上传方),看看是否调用了加密或自定义序列化逻辑。";
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** 批量下载 bucket(或某前缀)下所有对象到本地目录,保持原有目录结构 */
|
|
278
|
+
private static void downloadAll(AmazonS3 s3, String bucket, String prefix, String outDir) {
|
|
279
|
+
List<String> keys = new ArrayList<>();
|
|
280
|
+
|
|
281
|
+
ListObjectsV2Request request = new ListObjectsV2Request()
|
|
282
|
+
.withBucketName(bucket)
|
|
283
|
+
.withPrefix(prefix)
|
|
284
|
+
.withMaxKeys(1000);
|
|
285
|
+
ListObjectsV2Result result;
|
|
286
|
+
do {
|
|
287
|
+
result = s3.listObjectsV2(request);
|
|
288
|
+
for (S3ObjectSummary summary : result.getObjectSummaries()) {
|
|
289
|
+
keys.add(summary.getKey());
|
|
290
|
+
}
|
|
291
|
+
request.setContinuationToken(result.getNextContinuationToken());
|
|
292
|
+
} while (result.isTruncated());
|
|
293
|
+
|
|
294
|
+
System.out.printf("共找到 %d 个对象,开始下载到 %s ...%n", keys.size(), outDir);
|
|
295
|
+
|
|
296
|
+
int done = 0;
|
|
297
|
+
for (String key : keys) {
|
|
298
|
+
try (S3Object obj = s3.getObject(bucket, key)) {
|
|
299
|
+
Path target = Paths.get(outDir, key);
|
|
300
|
+
Files.createDirectories(target.getParent() == null ? Paths.get(outDir) : target.getParent());
|
|
301
|
+
try (InputStream in = obj.getObjectContent()) {
|
|
302
|
+
Files.copy(in, target, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
|
303
|
+
}
|
|
304
|
+
done++;
|
|
305
|
+
if (done % 50 == 0 || done == keys.size()) {
|
|
306
|
+
System.out.printf("已下载 %d/%d%n", done, keys.size());
|
|
307
|
+
}
|
|
308
|
+
} catch (IOException e) {
|
|
309
|
+
System.err.printf("下载失败 [%s]: %s%n", key, e.getMessage());
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
System.out.println("下载完成。");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private static String readAll(InputStream in) throws IOException {
|
|
316
|
+
StringBuilder sb = new StringBuilder();
|
|
317
|
+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
|
|
318
|
+
String line;
|
|
319
|
+
while ((line = reader.readLine()) != null) {
|
|
320
|
+
sb.append(line).append('\n');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return sb.toString();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private static String padRight(String s, int len) {
|
|
327
|
+
if (s.length() >= len) {
|
|
328
|
+
return s.substring(0, len - 1) + " ";
|
|
329
|
+
}
|
|
330
|
+
StringBuilder sb = new StringBuilder(s);
|
|
331
|
+
while (sb.length() < len) sb.append(' ');
|
|
332
|
+
return sb.toString();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private static void printUsageAndExit() {
|
|
336
|
+
System.out.println("用法:");
|
|
337
|
+
System.out.println(" list java -jar s3-bucket-parser.jar list --bucket <bucket> [--prefix <prefix>] [--region <region>] [--profile <profile>]");
|
|
338
|
+
System.out.println(" read java -jar s3-bucket-parser.jar read --bucket <bucket> --key <key> [--region <region>] [--profile <profile>]");
|
|
339
|
+
System.out.println(" inspect java -jar s3-bucket-parser.jar inspect --bucket <bucket> --key <key> [--region <region>] [--profile <profile>]");
|
|
340
|
+
System.out.println(" download java -jar s3-bucket-parser.jar download --bucket <bucket> [--prefix <prefix>] --out <local-dir> [--region <region>] [--profile <profile>]");
|
|
341
|
+
System.out.println();
|
|
342
|
+
System.out.println("认证:默认使用 AWS 默认凭证链;也可传 --profile <name>,或 --access-key/--secret-key(仅建议本地测试用)");
|
|
343
|
+
System.exit(1);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** 极简命令行参数解析:--key value --key2 value2 */
|
|
347
|
+
private static class Args {
|
|
348
|
+
private final java.util.Map<String, String> map = new java.util.HashMap<>();
|
|
349
|
+
|
|
350
|
+
static Args parse(String[] args, int fromIndex) {
|
|
351
|
+
Args result = new Args();
|
|
352
|
+
for (int i = fromIndex; i < args.length; i++) {
|
|
353
|
+
String a = args[i];
|
|
354
|
+
if (a.startsWith("--")) {
|
|
355
|
+
String k = a.substring(2);
|
|
356
|
+
String v = (i + 1 < args.length && !args[i + 1].startsWith("--")) ? args[++i] : "";
|
|
357
|
+
result.map.put(k, v);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return result;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
String get(String key) {
|
|
364
|
+
return map.get(key);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
String getOrDefault(String key, String def) {
|
|
368
|
+
return map.getOrDefault(key, def);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
package/aws/pom.xml
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
|
3
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
4
|
+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
5
|
+
<modelVersion>4.0.0</modelVersion>
|
|
6
|
+
|
|
7
|
+
<groupId>com.example</groupId>
|
|
8
|
+
<artifactId>s3-bucket-parser</artifactId>
|
|
9
|
+
<version>1.0.0</version>
|
|
10
|
+
<packaging>jar</packaging>
|
|
11
|
+
|
|
12
|
+
<properties>
|
|
13
|
+
<maven.compiler.source>8</maven.compiler.source>
|
|
14
|
+
<maven.compiler.target>8</maven.compiler.target>
|
|
15
|
+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
|
16
|
+
<aws.sdk.version>1.12.194</aws.sdk.version>
|
|
17
|
+
</properties>
|
|
18
|
+
|
|
19
|
+
<dependencies>
|
|
20
|
+
<!-- 只引入 S3 模块,而不是整个 aws-java-sdk,减小体积 -->
|
|
21
|
+
<dependency>
|
|
22
|
+
<groupId>com.amazonaws</groupId>
|
|
23
|
+
<artifactId>aws-java-sdk-s3</artifactId>
|
|
24
|
+
<version>${aws.sdk.version}</version>
|
|
25
|
+
</dependency>
|
|
26
|
+
|
|
27
|
+
<!-- 用于把读取到的 JSON 内容格式化打印(可选,非 JSON 数据不受影响) -->
|
|
28
|
+
<dependency>
|
|
29
|
+
<groupId>com.fasterxml.jackson.core</groupId>
|
|
30
|
+
<artifactId>jackson-databind</artifactId>
|
|
31
|
+
<version>2.13.2</version>
|
|
32
|
+
</dependency>
|
|
33
|
+
</dependencies>
|
|
34
|
+
|
|
35
|
+
<build>
|
|
36
|
+
<plugins>
|
|
37
|
+
<!-- 打包成带所有依赖的可执行 jar,方便直接 java -jar 运行 -->
|
|
38
|
+
<plugin>
|
|
39
|
+
<groupId>org.apache.maven.plugins</groupId>
|
|
40
|
+
<artifactId>maven-shade-plugin</artifactId>
|
|
41
|
+
<version>3.4.1</version>
|
|
42
|
+
<executions>
|
|
43
|
+
<execution>
|
|
44
|
+
<phase>package</phase>
|
|
45
|
+
<goals>
|
|
46
|
+
<goal>shade</goal>
|
|
47
|
+
</goals>
|
|
48
|
+
<configuration>
|
|
49
|
+
<transformers>
|
|
50
|
+
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
|
51
|
+
<mainClass>com.example.s3tool.S3BucketExplorer</mainClass>
|
|
52
|
+
</transformer>
|
|
53
|
+
</transformers>
|
|
54
|
+
</configuration>
|
|
55
|
+
</execution>
|
|
56
|
+
</executions>
|
|
57
|
+
</plugin>
|
|
58
|
+
</plugins>
|
|
59
|
+
</build>
|
|
60
|
+
</project>
|
package/package.json
CHANGED
package/server/env.example
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# opencode server 地址(所有用户共用同一个 server)
|
|
2
|
-
OPENCODE_SERVER_URL=http://127.0.0.1:4096
|
|
3
|
-
|
|
4
|
-
# 如果 opencode server 开启了 OPENCODE_SERVER_PASSWORD,这里填 Basic Auth 密码
|
|
5
|
-
OPENCODE_SERVER_USERNAME=opencode
|
|
6
|
-
OPENCODE_SERVER_PASSWORD=123
|
|
7
|
-
|
|
8
|
-
# 本网关监听端口
|
|
9
|
-
PORT=3000
|
|
10
|
-
|
|
11
|
-
# 给 cookie 签名用的随机字符串,生产环境务必替换
|
|
12
|
-
COOKIE_SECRET=test
|
|
13
|
-
DEBUG_EVENTS=1
|