session-viewer-plugin 0.1.3 → 0.1.4

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.
@@ -1,354 +0,0 @@
1
- import { CloudStorageError } from './cloudError.js';
2
- import { resolveAgcCredentials } from './agcCredentials.js';
3
- // ─── AGC 端点(单点调整区) ────────────────────────────────────
4
- // 文档: https://developer.huawei.com/consumer/cn/doc/AppGallery-connect-References/storage-restapi-listfile-0000001498518878
5
- // 站点对照: token 域名 connect-api*.cloud.huawei.com / 存储域名 ops-server-*.agcstorage.link/v0
6
- const AGC_SITES = {
7
- CN: {
8
- tokenHost: 'https://connect-api.cloud.huawei.com',
9
- storageHost: 'https://ops-server-drcn.agcstorage.link/v0',
10
- downloadHost: 'https://agc-storage-drcn.platform.dbankcloud.cn',
11
- },
12
- DE: {
13
- tokenHost: 'https://connect-api-dre.cloud.huawei.com',
14
- storageHost: 'https://ops-server-dre.agcstorage.link/v0',
15
- // 海外下载域名按 dbankcloud 海外域推导(当前桶为 CN,未实测)
16
- downloadHost: 'https://agc-storage-dre.platform.dbankcloud.com',
17
- },
18
- SG: {
19
- tokenHost: 'https://connect-api-dra.cloud.huawei.com',
20
- storageHost: 'https://ops-server-dra.agcstorage.link/v0',
21
- downloadHost: 'https://agc-storage-dra.platform.dbankcloud.com',
22
- },
23
- RURU: {
24
- tokenHost: 'https://connect-api-drru.cloud.huawei.com',
25
- storageHost: 'https://ops-server-drru.agcstorage.link/v0',
26
- downloadHost: 'https://agc-storage-drru.platform.dbankcloud.com',
27
- },
28
- };
29
- // ─── Access Token(缓存,过期前 60 秒刷新) ───────────────────
30
- let cachedToken = null;
31
- async function getCloudAccessToken(cred) {
32
- if (cachedToken && Date.now() < cachedToken.expiresAt)
33
- return cachedToken.token;
34
- const site = resolveSite(cred.region);
35
- let res;
36
- try {
37
- res = await fetch(`${site.tokenHost}/api/oauth2/v1/token`, {
38
- method: 'POST',
39
- headers: { 'Content-Type': 'application/json' },
40
- body: JSON.stringify({
41
- grant_type: 'client_credentials',
42
- client_id: cred.client_id,
43
- client_secret: cred.client_secret,
44
- }),
45
- });
46
- }
47
- catch (err) {
48
- console.error('AGC token request error:', err);
49
- throw new CloudStorageError('Failed to reach AGC token endpoint', 502);
50
- }
51
- if (!res.ok) {
52
- const text = await res.text().catch(() => '');
53
- console.error('AGC token request failed:', res.status, text);
54
- throw new CloudStorageError(`AGC token request failed (${res.status}): ${text.slice(0, 300)}`, 502);
55
- }
56
- const data = (await res.json());
57
- if (!data.access_token) {
58
- throw new CloudStorageError('AGC token response missing access_token', 502);
59
- }
60
- const expiresInSeconds = Number(data.expires_in) || 172800;
61
- cachedToken = { token: data.access_token, expiresAt: Date.now() + (expiresInSeconds - 60) * 1000 };
62
- return data.access_token;
63
- }
64
- function resolveSite(region) {
65
- return AGC_SITES[region] ?? AGC_SITES.CN;
66
- }
67
- // ─── 公共请求构造 ────────────────────────────────────────────
68
- async function buildAgcHeaders(cred) {
69
- const token = await getCloudAccessToken(cred);
70
- return {
71
- client_id: cred.client_id,
72
- productId: cred.project_id,
73
- Authorization: `Bearer ${token}`,
74
- 'Content-Type': 'application/json',
75
- };
76
- }
77
- async function resolveContext() {
78
- const cred = await resolveAgcCredentials();
79
- const site = resolveSite(cred.region);
80
- const headers = await buildAgcHeaders(cred);
81
- return { cred, storageHost: site.storageHost, downloadHost: site.downloadHost, headers };
82
- }
83
- // 对象路径逐段 encodeURIComponent(见文档「通用注意事项」)
84
- function encodeObjectKey(key) {
85
- return key.split('/').map(encodeURIComponent).join('/');
86
- }
87
- function buildObjectUrl(storageHost, bucketName, key, query) {
88
- const base = `${storageHost}/${encodeURIComponent(bucketName)}/${encodeObjectKey(key)}`;
89
- return query ? `${base}?${query}` : base;
90
- }
91
- async function agcFetch(url, init, action) {
92
- let res;
93
- try {
94
- res = await fetch(url, init);
95
- }
96
- catch (err) {
97
- console.error(`AGC ${action} request error:`, err);
98
- throw new CloudStorageError(`Failed to reach AGC storage endpoint (${action})`, 502);
99
- }
100
- if (!res.ok) {
101
- const text = await res.text().catch(() => '');
102
- console.error(`AGC ${action} failed:`, res.status, text);
103
- throw new CloudStorageError(`AGC ${action} failed (${res.status}): ${text.slice(0, 300)}`, 502);
104
- }
105
- return res;
106
- }
107
- // ─── 列举文件 ─────────────────────────────────────────────────
108
- /** AGC listFiles 响应 → 稳定的 CloudFileListResult(size 字符串转数字) */
109
- function normalizeListResponse(data) {
110
- const obj = (data ?? {});
111
- const files = Array.isArray(obj.contents)
112
- ? obj.contents
113
- .map((f) => ({
114
- name: String(f.key ?? ''),
115
- size: Number(f.size ?? 0),
116
- contentType: String(f.contentType ?? ''),
117
- lastModified: String(f.lastModified ?? ''),
118
- }))
119
- .filter((f) => f.name)
120
- : [];
121
- const folders = Array.isArray(obj.commonPrefixes)
122
- ? obj.commonPrefixes
123
- .map((p) => {
124
- const prefix = String(p.prefix ?? '');
125
- const trimmed = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
126
- const name = trimmed.split('/').pop() || trimmed;
127
- return { prefix, name };
128
- })
129
- .filter((f) => f.name)
130
- : [];
131
- return {
132
- folders,
133
- files,
134
- nextMarker: obj.isTruncated && obj.nextMarker ? String(obj.nextMarker) : null,
135
- };
136
- }
137
- export async function listCloudFiles(options = {}) {
138
- const { prefix = '', marker, limit = 100, delimiter = false } = options;
139
- const { cred, storageHost, headers } = await resolveContext();
140
- const url = new URL(`${storageHost}/${encodeURIComponent(cred.bucket_name)}`);
141
- if (prefix)
142
- url.searchParams.set('prefix', prefix);
143
- if (marker)
144
- url.searchParams.set('marker', marker);
145
- url.searchParams.set('max-keys', String(limit));
146
- if (delimiter)
147
- url.searchParams.set('delimiter', '/');
148
- const res = await agcFetch(url, { headers }, 'listFiles');
149
- try {
150
- return normalizeListResponse(JSON.parse(await res.text()));
151
- }
152
- catch (err) {
153
- console.error('AGC listFiles response parse error:', err);
154
- throw new CloudStorageError('Failed to parse AGC listFiles response', 502);
155
- }
156
- }
157
- /**
158
- * 拉取指定前缀下的全部对象(自动翻页,无 delimiter)。
159
- * 供模糊查询与文件夹递归删除使用;桶内对象量级 1 万以内。
160
- * @param prefix 前缀(可选,空为整桶)
161
- * @param maxPages 翻页上限保护(默认 50 页 = 5 万对象,超出抛 400)
162
- * @returns 全部对象列表(key/size/contentType/lastModified)
163
- */
164
- export async function listAllCloudFiles(prefix = '', maxPages = 50) {
165
- const files = [];
166
- let marker;
167
- for (let page = 0; page < maxPages; page++) {
168
- const result = await listCloudFiles({ prefix, marker, limit: 1000, delimiter: false });
169
- files.push(...result.files);
170
- if (!result.nextMarker)
171
- return files;
172
- marker = result.nextMarker;
173
- }
174
- throw new CloudStorageError(`对象数超过翻页上限(${maxPages * 1000}),请缩小前缀范围`, 400);
175
- }
176
- // ─── 文件夹统计(递归聚合直接子文件夹的大小/对象数/最后修改时间) ──
177
- const FOLDER_STATS_TTL_MS = 5 * 60 * 1000;
178
- const folderStatsCache = new Map();
179
- export function clearCloudFolderStatsCache() {
180
- folderStatsCache.clear();
181
- }
182
- function compareModified(a, b) {
183
- if (a === b)
184
- return 0;
185
- if (!a)
186
- return -1;
187
- if (!b)
188
- return 1;
189
- const ta = Date.parse(a);
190
- const tb = Date.parse(b);
191
- if (Number.isNaN(ta) || Number.isNaN(tb))
192
- return a > b ? 1 : -1;
193
- return ta - tb;
194
- }
195
- /**
196
- * 统计 parent 下每个直接子文件夹的递归聚合信息(一次全量列举,按第一段路径分组)。
197
- * 结果带 5 分钟 TTL 缓存;删除/上传/重命名会清空缓存。
198
- * @param parent 父前缀(空串表示根目录,非空必须以 / 结尾)
199
- * @returns { [子文件夹前缀]: { count, size, lastModified } }
200
- */
201
- export async function getCloudFolderStats(parent) {
202
- const cached = folderStatsCache.get(parent);
203
- if (cached && Date.now() < cached.expiresAt)
204
- return cached.stats;
205
- const files = await listAllCloudFiles(parent);
206
- const stats = {};
207
- for (const file of files) {
208
- const rest = file.name.slice(parent.length);
209
- const slashIdx = rest.indexOf('/');
210
- if (slashIdx <= 0)
211
- continue; // parent 下的直接文件,不属于任何子文件夹
212
- const key = `${parent}${rest.slice(0, slashIdx)}/`;
213
- const entry = (stats[key] ??= { count: 0, size: 0, lastModified: '' });
214
- entry.count++;
215
- entry.size += file.size;
216
- if (compareModified(file.lastModified, entry.lastModified) > 0) {
217
- entry.lastModified = file.lastModified;
218
- }
219
- }
220
- folderStatsCache.set(parent, { expiresAt: Date.now() + FOLDER_STATS_TTL_MS, stats });
221
- return stats;
222
- }
223
- export async function downloadCloudFile(key) {
224
- const { cred, storageHost, headers } = await resolveContext();
225
- const url = buildObjectUrl(storageHost, cred.bucket_name, key);
226
- const res = await agcFetch(url, { headers }, 'downloadFile');
227
- return {
228
- body: res.body,
229
- contentType: res.headers.get('content-type') || 'application/octet-stream',
230
- contentLength: res.headers.get('content-length'),
231
- filename: key.split('/').pop() || 'file',
232
- };
233
- }
234
- // ─── 下载令牌 ─────────────────────────────────────────────────
235
- /**
236
- * 生成对象下载令牌并组装公网下载 URL(免鉴权,效期以 AGC 官方为准)。
237
- * 底层为 ops-server `?token=create`,同对象重复生成返回相同令牌(幂等,已实测)。
238
- * URL 格式与 AGC 控制台「复制下载令牌」一致:下载域名/v0/{bucket}/{encodeURIComponent(key)}?token=...
239
- */
240
- export async function getCloudFileDownloadToken(key) {
241
- const { cred, storageHost, downloadHost, headers } = await resolveContext();
242
- const url = buildObjectUrl(storageHost, cred.bucket_name, key, 'token=create');
243
- const res = await agcFetch(url, { headers }, 'createToken');
244
- const data = (await res.json().catch(() => null));
245
- const token = Array.isArray(data) ? String(data[0] ?? '') : '';
246
- if (!token) {
247
- throw new CloudStorageError('AGC createToken returned empty token', 502);
248
- }
249
- const downloadUrl = `${downloadHost}/v0/${encodeURIComponent(cred.bucket_name)}/${encodeURIComponent(key)}?token=${token}`;
250
- return { token, url: downloadUrl };
251
- }
252
- // ─── 删除文件 ─────────────────────────────────────────────────
253
- export async function deleteCloudFile(key) {
254
- const { cred, storageHost, headers } = await resolveContext();
255
- const url = buildObjectUrl(storageHost, cred.bucket_name, key);
256
- await agcFetch(url, { method: 'DELETE', headers }, 'deleteFile');
257
- clearCloudFolderStatsCache();
258
- }
259
- // ─── 上传 / 重命名 ───────────────────────────────────────────
260
- export async function cloudFileExists(key) {
261
- const { cred, storageHost, headers } = await resolveContext();
262
- const url = buildObjectUrl(storageHost, cred.bucket_name, key, 'metadata=query');
263
- try {
264
- const res = await fetch(url, { headers });
265
- return res.ok;
266
- }
267
- catch {
268
- return false;
269
- }
270
- }
271
- export async function uploadCloudFile(key, content, contentType) {
272
- const { cred, storageHost, headers } = await resolveContext();
273
- const url = buildObjectUrl(storageHost, cred.bucket_name, key);
274
- await agcFetch(url, {
275
- method: 'PUT',
276
- headers: {
277
- ...headers,
278
- 'X-Agc-Content-Type': contentType || 'application/octet-stream',
279
- },
280
- body: new Uint8Array(content),
281
- }, 'uploadFile');
282
- clearCloudFolderStatsCache();
283
- }
284
- export async function renameCloudFile(oldKey, newKey) {
285
- const { cred, storageHost, headers } = await resolveContext();
286
- const sourceUrl = buildObjectUrl(storageHost, cred.bucket_name, oldKey);
287
- const res = await agcFetch(sourceUrl, { headers }, 'downloadFile');
288
- const content = Buffer.from(await res.arrayBuffer());
289
- const contentType = res.headers.get('content-type') || 'application/octet-stream';
290
- await uploadCloudFile(newKey, content, contentType);
291
- await deleteCloudFile(oldKey);
292
- }
293
- // ─── 文件元数据 ──────────────────────────────────────────────
294
- function normalizeMetadata(data) {
295
- const obj = (data ?? {});
296
- const custom = (obj.metadata ?? {});
297
- const customMetadata = {};
298
- for (const [k, v] of Object.entries(custom)) {
299
- customMetadata[k] = String(v);
300
- }
301
- return {
302
- name: String(obj.name ?? ''),
303
- bucket: obj.bucket ? String(obj.bucket) : undefined,
304
- contentType: obj.contentType ? String(obj.contentType) : undefined,
305
- contentDisposition: obj.contentDisposition ? String(obj.contentDisposition) : undefined,
306
- contentEncoding: obj.contentEncoding ? String(obj.contentEncoding) : undefined,
307
- cacheControl: obj.cacheControl ? String(obj.cacheControl) : undefined,
308
- contentLanguage: obj.contentLanguage ? String(obj.contentLanguage) : undefined,
309
- size: obj.size != null ? Number(obj.size) : undefined,
310
- createTime: obj.createTime ? String(obj.createTime) : undefined,
311
- updateTime: obj.updateTime ? String(obj.updateTime) : undefined,
312
- storageType: obj.storageType ? String(obj.storageType) : undefined,
313
- customMetadata,
314
- };
315
- }
316
- export async function getCloudFileMetadata(key) {
317
- const { cred, storageHost, headers } = await resolveContext();
318
- const url = buildObjectUrl(storageHost, cred.bucket_name, key, 'metadata=query');
319
- const res = await agcFetch(url, { headers }, 'getMetadata');
320
- try {
321
- return normalizeMetadata(JSON.parse(await res.text()));
322
- }
323
- catch (err) {
324
- console.error('AGC getMetadata response parse error:', err);
325
- throw new CloudStorageError('Failed to parse AGC metadata response', 502);
326
- }
327
- }
328
- export async function updateCloudFileMetadata(key, update) {
329
- const { cred, storageHost, headers } = await resolveContext();
330
- const url = buildObjectUrl(storageHost, cred.bucket_name, key, 'metadata=update');
331
- const metaHeaders = { ...headers };
332
- if (update.contentType)
333
- metaHeaders['X-Agc-Content-Type'] = update.contentType;
334
- if (update.contentDisposition)
335
- metaHeaders['X-Agc-Content-Disposition'] = update.contentDisposition;
336
- if (update.contentEncoding)
337
- metaHeaders['X-Agc-Content-Encoding'] = update.contentEncoding;
338
- if (update.cacheControl)
339
- metaHeaders['X-Agc-Cache-Control'] = update.cacheControl;
340
- if (update.contentLanguage)
341
- metaHeaders['X-Agc-Content-Language'] = update.contentLanguage;
342
- for (const [k, v] of Object.entries(update.customMetadata ?? {})) {
343
- if (k)
344
- metaHeaders[`X-Agc-meta-${k}`] = String(v);
345
- }
346
- const res = await agcFetch(url, { method: 'GET', headers: metaHeaders }, 'updateMetadata');
347
- try {
348
- return normalizeMetadata(JSON.parse(await res.text()));
349
- }
350
- catch (err) {
351
- console.error('AGC updateMetadata response parse error:', err);
352
- throw new CloudStorageError('Failed to parse AGC updateMetadata response', 502);
353
- }
354
- }
@@ -1 +0,0 @@
1
- export {};