session-viewer-plugin 0.1.2 → 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.
- package/README.md +8 -13
- package/frontend/dist-ui/index.mjs +4070 -4452
- package/frontend/dist-ui/style.css +1 -1
- package/package.json +3 -4
- package/server/dist/app.js +1 -2
- package/server/dist/bin.js +3 -3
- package/server/dist/index.js +1 -1
- package/server/dist/services/assembler.js +106 -20
- package/server/dist/services/config.js +1 -0
- package/server/dist/types/index.js +0 -1
- package/server/dist/api/cloudApi.js +0 -267
- package/server/dist/services/agcCredentials.js +0 -117
- package/server/dist/services/cloudError.js +0 -15
- package/server/dist/services/cloudStorage.js +0 -305
- package/server/dist/types/cloud.js +0 -1
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
import express from 'express';
|
|
2
|
-
import { Readable } from 'node:stream';
|
|
3
|
-
import { CloudStorageError } from '../services/cloudError.js';
|
|
4
|
-
import { listCloudFiles, listAllCloudFiles, downloadCloudFile, getCloudFileDownloadToken, deleteCloudFile, getCloudFileMetadata, updateCloudFileMetadata, cloudFileExists, renameCloudFile, } from '../services/cloudStorage.js';
|
|
5
|
-
// 模糊查询返回上限与文件夹单次删除上限(桶内对象量级 1 万以内)
|
|
6
|
-
const SEARCH_RESULT_LIMIT = 500;
|
|
7
|
-
const FOLDER_DELETE_LIMIT = 5000;
|
|
8
|
-
function sendCloudError(res, err, fallback) {
|
|
9
|
-
if (err instanceof CloudStorageError) {
|
|
10
|
-
console.error(`Cloud storage error: ${err.message}`);
|
|
11
|
-
res.status(err.status).json({ error: err.message });
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
console.error(fallback + ':', err);
|
|
15
|
-
res.status(500).json({ error: fallback });
|
|
16
|
-
}
|
|
17
|
-
function requireKey(req) {
|
|
18
|
-
const key = typeof req.query.key === 'string' ? req.query.key.trim() : '';
|
|
19
|
-
return key || null;
|
|
20
|
-
}
|
|
21
|
-
export const cloudApi = express.Router();
|
|
22
|
-
cloudApi.get('/cloud/files', async (req, res) => {
|
|
23
|
-
try {
|
|
24
|
-
const prefix = typeof req.query.prefix === 'string' ? req.query.prefix : '';
|
|
25
|
-
const marker = typeof req.query.marker === 'string' ? req.query.marker : undefined;
|
|
26
|
-
const delimiterRaw = String(req.query.delimiter ?? '');
|
|
27
|
-
const delimiter = delimiterRaw !== '' && delimiterRaw !== '0' && delimiterRaw !== 'false';
|
|
28
|
-
const limitRaw = Number(req.query.limit);
|
|
29
|
-
const limit = Number.isFinite(limitRaw)
|
|
30
|
-
? Math.min(Math.max(Math.trunc(limitRaw), 1), 1000)
|
|
31
|
-
: 100;
|
|
32
|
-
const result = await listCloudFiles({ prefix, marker, limit, delimiter });
|
|
33
|
-
console.log(`GET /api/cloud/files, prefix: '${prefix}', delimiter: ${delimiter}, limit: ${limit}, returning ${result.folders.length} folders, ${result.files.length} files`);
|
|
34
|
-
res.json(result);
|
|
35
|
-
}
|
|
36
|
-
catch (err) {
|
|
37
|
-
sendCloudError(res, err, 'Failed to list cloud files');
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
cloudApi.get('/cloud/file/download', async (req, res) => {
|
|
41
|
-
const key = requireKey(req);
|
|
42
|
-
if (!key) {
|
|
43
|
-
res.status(400).json({ error: 'Missing required query param: key' });
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
try {
|
|
47
|
-
const dl = await downloadCloudFile(key);
|
|
48
|
-
res.setHeader('Content-Type', dl.contentType);
|
|
49
|
-
if (dl.contentLength)
|
|
50
|
-
res.setHeader('Content-Length', dl.contentLength);
|
|
51
|
-
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(dl.filename)}"; filename*=UTF-8''${encodeURIComponent(dl.filename)}`);
|
|
52
|
-
if (dl.body) {
|
|
53
|
-
const stream = Readable.fromWeb(dl.body);
|
|
54
|
-
stream.pipe(res);
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
res.end();
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
catch (err) {
|
|
61
|
-
sendCloudError(res, err, 'Failed to download cloud file');
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
// ─── 下载令牌(AGC 官方 ?token=create,公网免鉴权下载 URL)────
|
|
65
|
-
cloudApi.get('/cloud/file/token', async (req, res) => {
|
|
66
|
-
const key = requireKey(req);
|
|
67
|
-
if (!key) {
|
|
68
|
-
res.status(400).json({ error: 'Missing required query param: key' });
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
try {
|
|
72
|
-
const result = await getCloudFileDownloadToken(key);
|
|
73
|
-
console.log(`GET /api/cloud/file/token, key: ${key}, token: ${result.token}`);
|
|
74
|
-
res.json(result);
|
|
75
|
-
}
|
|
76
|
-
catch (err) {
|
|
77
|
-
sendCloudError(res, err, 'Failed to create cloud file token');
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
cloudApi.delete('/cloud/file', async (req, res) => {
|
|
81
|
-
const key = requireKey(req);
|
|
82
|
-
if (!key) {
|
|
83
|
-
res.status(400).json({ error: 'Missing required query param: key' });
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
try {
|
|
87
|
-
await deleteCloudFile(key);
|
|
88
|
-
console.log(`DELETE /api/cloud/file, key: ${key}, deleted`);
|
|
89
|
-
res.json({ deleted: true, key });
|
|
90
|
-
}
|
|
91
|
-
catch (err) {
|
|
92
|
-
sendCloudError(res, err, 'Failed to delete cloud file');
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
// ─── 模糊查询(AGC 无模糊 API:服务端全量拉取 + 内存 contains 匹配)─────────
|
|
96
|
-
cloudApi.get('/cloud/search', async (req, res) => {
|
|
97
|
-
const query = typeof req.query.query === 'string' ? req.query.query.trim() : '';
|
|
98
|
-
const prefix = typeof req.query.prefix === 'string' ? req.query.prefix.trim() : '';
|
|
99
|
-
if (!query) {
|
|
100
|
-
res.status(400).json({ error: 'Missing required query param: query' });
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
try {
|
|
104
|
-
const all = await listAllCloudFiles(prefix);
|
|
105
|
-
const q = query.toLowerCase();
|
|
106
|
-
const hits = all.filter((f) => f.name.toLowerCase().includes(q));
|
|
107
|
-
const truncated = hits.length > SEARCH_RESULT_LIMIT;
|
|
108
|
-
const files = truncated ? hits.slice(0, SEARCH_RESULT_LIMIT) : hits;
|
|
109
|
-
console.log(`GET /api/cloud/search, query: '${query}', prefix: '${prefix}', scanned ${all.length}, hits ${hits.length}`);
|
|
110
|
-
res.json({ total: hits.length, truncated, scanned: all.length, files });
|
|
111
|
-
}
|
|
112
|
-
catch (err) {
|
|
113
|
-
sendCloudError(res, err, 'Failed to search cloud files');
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
// ─── 文件夹(前缀)预估与递归删除 ──────────────────────────────
|
|
117
|
-
// 合法文件夹前缀:非空、以 / 结尾、不含 ..,避免误删整桶或越权
|
|
118
|
-
function requireFolderPrefix(req) {
|
|
119
|
-
const prefix = typeof req.query.prefix === 'string' ? req.query.prefix.trim() : '';
|
|
120
|
-
if (!prefix || !prefix.endsWith('/') || prefix.includes('..'))
|
|
121
|
-
return null;
|
|
122
|
-
return prefix;
|
|
123
|
-
}
|
|
124
|
-
cloudApi.get('/cloud/folder/count', async (req, res) => {
|
|
125
|
-
const prefix = requireFolderPrefix(req);
|
|
126
|
-
if (!prefix) {
|
|
127
|
-
res.status(400).json({ error: 'prefix 必须非空且以 / 结尾' });
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
try {
|
|
131
|
-
const files = await listAllCloudFiles(prefix);
|
|
132
|
-
res.json({ count: files.length });
|
|
133
|
-
}
|
|
134
|
-
catch (err) {
|
|
135
|
-
sendCloudError(res, err, 'Failed to count cloud folder');
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
cloudApi.delete('/cloud/folder', async (req, res) => {
|
|
139
|
-
const prefix = requireFolderPrefix(req);
|
|
140
|
-
if (!prefix) {
|
|
141
|
-
res.status(400).json({ error: 'prefix 必须非空且以 / 结尾' });
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
try {
|
|
145
|
-
const files = await listAllCloudFiles(prefix, 5);
|
|
146
|
-
if (files.length === 0) {
|
|
147
|
-
res.json({ deleted: 0, failed: 0, errors: [] });
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
if (files.length > FOLDER_DELETE_LIMIT) {
|
|
151
|
-
res.status(400).json({
|
|
152
|
-
error: `该前缀下共 ${files.length} 个对象,超过单次删除上限 ${FOLDER_DELETE_LIMIT},请缩小范围`,
|
|
153
|
-
});
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
// 小并发逐个删除(AGC 无批量删除接口;并发过大会触发限流)
|
|
157
|
-
const failed = [];
|
|
158
|
-
const queue = files.map((f) => f.name);
|
|
159
|
-
let deleted = 0;
|
|
160
|
-
async function worker() {
|
|
161
|
-
for (;;) {
|
|
162
|
-
const key = queue.shift();
|
|
163
|
-
if (key === undefined)
|
|
164
|
-
return;
|
|
165
|
-
try {
|
|
166
|
-
await deleteCloudFile(key);
|
|
167
|
-
deleted++;
|
|
168
|
-
}
|
|
169
|
-
catch {
|
|
170
|
-
failed.push(key);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
await Promise.all(Array.from({ length: Math.min(5, files.length) }, worker));
|
|
175
|
-
console.log(`DELETE /api/cloud/folder, prefix: ${prefix}, deleted ${deleted}, failed ${failed.length}`);
|
|
176
|
-
res.json({ deleted, failed: failed.length, errors: failed.slice(0, 10) });
|
|
177
|
-
}
|
|
178
|
-
catch (err) {
|
|
179
|
-
sendCloudError(res, err, 'Failed to delete cloud folder');
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
cloudApi.put('/cloud/file/rename', async (req, res) => {
|
|
183
|
-
const key = requireKey(req);
|
|
184
|
-
if (!key) {
|
|
185
|
-
res.status(400).json({ error: 'Missing required query param: key' });
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
const name = typeof req.body?.name === 'string' ? req.body.name.trim() : '';
|
|
189
|
-
if (!name) {
|
|
190
|
-
res.status(400).json({ error: 'Missing required body field: name' });
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
if (name.includes('/')) {
|
|
194
|
-
res.status(400).json({ error: '文件名不能包含 "/"' });
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
const dir = key.includes('/') ? key.slice(0, key.lastIndexOf('/') + 1) : '';
|
|
198
|
-
const newKey = `${dir}${name}`;
|
|
199
|
-
if (newKey === key) {
|
|
200
|
-
res.status(400).json({ error: '新文件名与原文件名相同' });
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
if (await cloudFileExists(newKey)) {
|
|
204
|
-
res.status(409).json({ error: `目标文件已存在:${newKey}` });
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
try {
|
|
208
|
-
await renameCloudFile(key, newKey);
|
|
209
|
-
console.log(`PUT /api/cloud/file/rename, ${key} -> ${newKey}`);
|
|
210
|
-
res.json({ renamed: true, key: newKey });
|
|
211
|
-
}
|
|
212
|
-
catch (err) {
|
|
213
|
-
sendCloudError(res, err, 'Failed to rename cloud file');
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
cloudApi.get('/cloud/file/metadata', async (req, res) => {
|
|
217
|
-
const key = requireKey(req);
|
|
218
|
-
if (!key) {
|
|
219
|
-
res.status(400).json({ error: 'Missing required query param: key' });
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
try {
|
|
223
|
-
const metadata = await getCloudFileMetadata(key);
|
|
224
|
-
res.json(metadata);
|
|
225
|
-
}
|
|
226
|
-
catch (err) {
|
|
227
|
-
sendCloudError(res, err, 'Failed to get cloud file metadata');
|
|
228
|
-
}
|
|
229
|
-
});
|
|
230
|
-
cloudApi.put('/cloud/file/metadata', async (req, res) => {
|
|
231
|
-
const key = requireKey(req);
|
|
232
|
-
if (!key) {
|
|
233
|
-
res.status(400).json({ error: 'Missing required query param: key' });
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
try {
|
|
237
|
-
const body = (req.body ?? {});
|
|
238
|
-
const update = {};
|
|
239
|
-
const stringFields = [
|
|
240
|
-
'contentType',
|
|
241
|
-
'contentDisposition',
|
|
242
|
-
'contentEncoding',
|
|
243
|
-
'cacheControl',
|
|
244
|
-
'contentLanguage',
|
|
245
|
-
];
|
|
246
|
-
for (const field of stringFields) {
|
|
247
|
-
const v = body[field];
|
|
248
|
-
if (typeof v === 'string' && v.trim())
|
|
249
|
-
update[field] = v.trim();
|
|
250
|
-
}
|
|
251
|
-
if (body.customMetadata && typeof body.customMetadata === 'object' && !Array.isArray(body.customMetadata)) {
|
|
252
|
-
const meta = {};
|
|
253
|
-
for (const [k, v] of Object.entries(body.customMetadata)) {
|
|
254
|
-
if (k.trim())
|
|
255
|
-
meta[k.trim()] = String(v ?? '');
|
|
256
|
-
}
|
|
257
|
-
if (Object.keys(meta).length)
|
|
258
|
-
update.customMetadata = meta;
|
|
259
|
-
}
|
|
260
|
-
const metadata = await updateCloudFileMetadata(key, update);
|
|
261
|
-
console.log(`PUT /api/cloud/file/metadata, key: ${key}, updated fields:`, Object.keys(update));
|
|
262
|
-
res.json(metadata);
|
|
263
|
-
}
|
|
264
|
-
catch (err) {
|
|
265
|
-
sendCloudError(res, err, 'Failed to update cloud file metadata');
|
|
266
|
-
}
|
|
267
|
-
});
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
// AGC(华为云存储)凭证解析:按「环境变量 > 包内默认文件」优先级提供当前生效凭证。
|
|
2
|
-
// 环境变量支持两种形态:AGC_CREDENTIALS_FILE 指向凭证 JSON 文件、AGC_CREDENTIALS_JSON 为内联 JSON 字符串;
|
|
3
|
-
// 包内默认文件为随包发布的 assets/agc-apiclient.json,环境变量未设置时兜底。
|
|
4
|
-
// 凭证在进程生命周期内不变,解析结果进程内缓存。
|
|
5
|
-
import fs from 'fs/promises';
|
|
6
|
-
import path from 'path';
|
|
7
|
-
import { fileURLToPath } from 'url';
|
|
8
|
-
import { CloudStorageError } from './cloudError.js';
|
|
9
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
/** 包内默认凭证文件路径(随包发布,作为最低优先级兜底) */
|
|
11
|
-
const BUNDLED_CREDENTIALS_FILE = path.join(__dirname, '..', 'assets', 'agc-apiclient.json');
|
|
12
|
-
/** 凭证必填字段(缺任一即视为无效凭证) */
|
|
13
|
-
const REQUIRED_FIELDS = [
|
|
14
|
-
'client_id',
|
|
15
|
-
'client_secret',
|
|
16
|
-
'project_id',
|
|
17
|
-
'bucket_name',
|
|
18
|
-
];
|
|
19
|
-
/** 已成功解析的凭证缓存(凭证进程内不变,无需失效机制) */
|
|
20
|
-
let cached = null;
|
|
21
|
-
/**
|
|
22
|
-
* 校验凭证对象是否包含全部必填字段。
|
|
23
|
-
* @param cred 待校验的凭证对象
|
|
24
|
-
* @returns 缺失字段说明;校验通过返回 null
|
|
25
|
-
*/
|
|
26
|
-
function validateCredentials(cred) {
|
|
27
|
-
const missing = REQUIRED_FIELDS.filter((field) => !String(cred[field] ?? '').trim());
|
|
28
|
-
return missing.length > 0 ? `缺少必填字段: ${missing.join(', ')}` : null;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* 读取并解析 JSON 凭证文件。
|
|
32
|
-
* @param filePath 凭证文件绝对路径
|
|
33
|
-
* @returns 解析后的凭证对象
|
|
34
|
-
* @throws 文件不存在或内容不是合法 JSON 时抛错
|
|
35
|
-
*/
|
|
36
|
-
async function readCredentialsFile(filePath) {
|
|
37
|
-
const raw = await fs.readFile(filePath, 'utf-8');
|
|
38
|
-
return JSON.parse(raw);
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* 从环境变量读取凭证(文件路径或内联 JSON 二选一)。
|
|
42
|
-
* @returns 凭证对象;未设置相关环境变量时返回 null
|
|
43
|
-
*/
|
|
44
|
-
async function resolveFromEnv() {
|
|
45
|
-
const filePath = process.env.AGC_CREDENTIALS_FILE?.trim();
|
|
46
|
-
if (filePath) {
|
|
47
|
-
return readCredentialsFile(path.resolve(filePath));
|
|
48
|
-
}
|
|
49
|
-
const inlineJson = process.env.AGC_CREDENTIALS_JSON?.trim();
|
|
50
|
-
if (inlineJson) {
|
|
51
|
-
return JSON.parse(inlineJson);
|
|
52
|
-
}
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* 补齐凭证可选字段默认值(region 缺省按 CN 站点处理)。
|
|
57
|
-
* @param cred 原始凭证对象(应已通过必填校验)
|
|
58
|
-
* @returns 字段完整的凭证对象
|
|
59
|
-
*/
|
|
60
|
-
function normalizeCredentials(cred) {
|
|
61
|
-
return {
|
|
62
|
-
type: cred.type ?? '',
|
|
63
|
-
developer_id: cred.developer_id ?? '',
|
|
64
|
-
project_id: cred.project_id ?? '',
|
|
65
|
-
client_id: cred.client_id ?? '',
|
|
66
|
-
client_secret: cred.client_secret ?? '',
|
|
67
|
-
configuration_version: cred.configuration_version ?? '',
|
|
68
|
-
region: cred.region?.trim() || 'CN',
|
|
69
|
-
bucket_name: cred.bucket_name ?? '',
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* 按优先级解析凭证:环境变量 > 包内默认文件。
|
|
74
|
-
* 已设置的来源优先级最高,即使其内容无效也不再降级(避免静默使用错误配置)。
|
|
75
|
-
* @returns 成功时返回凭证与来源;全部来源不可用时返回错误说明
|
|
76
|
-
*/
|
|
77
|
-
async function resolveCredentialSources() {
|
|
78
|
-
try {
|
|
79
|
-
const envCred = await resolveFromEnv();
|
|
80
|
-
if (envCred) {
|
|
81
|
-
const invalid = validateCredentials(envCred);
|
|
82
|
-
if (invalid)
|
|
83
|
-
return { error: `环境变量凭证无效: ${invalid}` };
|
|
84
|
-
return { cred: normalizeCredentials(envCred), source: 'env' };
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
catch (err) {
|
|
88
|
-
return { error: `环境变量凭证读取失败: ${err.message}` };
|
|
89
|
-
}
|
|
90
|
-
try {
|
|
91
|
-
const bundledCred = await readCredentialsFile(BUNDLED_CREDENTIALS_FILE);
|
|
92
|
-
const invalid = validateCredentials(bundledCred);
|
|
93
|
-
if (invalid)
|
|
94
|
-
return { error: `包内默认凭证无效: ${invalid}` };
|
|
95
|
-
return { cred: normalizeCredentials(bundledCred), source: 'default' };
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
return {
|
|
99
|
-
error: '未配置 AGC 凭证:请设置 AGC_CREDENTIALS_FILE 或 AGC_CREDENTIALS_JSON 环境变量(发布包不含凭证文件,源码运行时可用 src/assets/agc-apiclient.json 兜底)',
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* 获取当前生效的 AGC 凭证(结果进程内缓存)。
|
|
105
|
-
* @returns 当前生效的凭证对象
|
|
106
|
-
* @throws 所有来源均不可用时抛出 CloudStorageError(503)
|
|
107
|
-
*/
|
|
108
|
-
export async function resolveAgcCredentials() {
|
|
109
|
-
if (cached)
|
|
110
|
-
return cached.cred;
|
|
111
|
-
const result = await resolveCredentialSources();
|
|
112
|
-
if ('error' in result) {
|
|
113
|
-
throw new CloudStorageError(result.error, 503);
|
|
114
|
-
}
|
|
115
|
-
cached = { cred: result.cred, source: result.source };
|
|
116
|
-
return result.cred;
|
|
117
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// 云端存储统一错误类型:携带 HTTP 状态码,供 AGC 凭证解析与存储操作共用。
|
|
2
|
-
export class CloudStorageError extends Error {
|
|
3
|
-
/** 期望返回给调用方的 HTTP 状态码 */
|
|
4
|
-
status;
|
|
5
|
-
/**
|
|
6
|
-
* 创建云端存储错误。
|
|
7
|
-
* @param message 错误描述
|
|
8
|
-
* @param status HTTP 状态码,默认 502(上游网关错误)
|
|
9
|
-
*/
|
|
10
|
-
constructor(message, status = 502) {
|
|
11
|
-
super(message);
|
|
12
|
-
this.name = 'CloudStorageError';
|
|
13
|
-
this.status = status;
|
|
14
|
-
}
|
|
15
|
-
}
|