target-supabase-sdk 0.1.0 → 0.1.1
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/LICENSE +21 -21
- package/README.md +110 -70
- package/package.json +57 -45
- package/src/auth/auth.api.ts +85 -85
- package/src/core.api.ts +338 -338
- package/src/core.interface.ts +74 -74
- package/src/core.utils.ts +98 -98
- package/src/feed/feed.interface.ts +33 -33
- package/src/file/file.api.ts +43 -43
- package/src/file/file.interface.ts +29 -29
- package/src/file-list/file-list.api.ts +64 -64
- package/src/file-list/file-list.interface.ts +24 -24
- package/src/idea/idea.interface.ts +27 -27
- package/src/index.ts +50 -50
- package/src/link/link.interface.ts +21 -21
- package/src/parcel/parcel-manager.ts +209 -209
- package/src/parcel/parcel.interface.ts +40 -40
- package/src/question-list/question-list.api.ts +42 -42
- package/src/question-list/question-list.interface.ts +21 -21
- package/src/review/review.api.ts +145 -145
- package/src/review/review.interface.ts +26 -26
- package/src/review-v2/apis/post-review-setting.api.ts +35 -35
- package/src/review-v2/apis/post-review.api.ts +59 -59
- package/src/review-v2/review-setting.interface.ts +26 -26
- package/src/review-v2/review.interface.ts +23 -23
- package/src/service/api.interface.ts +47 -47
- package/src/service/base.interface.ts +97 -97
- package/src/service/service.interface.ts +27 -27
- package/src/supabase.ts +143 -143
|
@@ -1,209 +1,209 @@
|
|
|
1
|
-
import type { Chunk, Parcel, ParcelDetails } from "./parcel.interface";
|
|
2
|
-
|
|
3
|
-
const MANIFEST_VERSION = 1;
|
|
4
|
-
const DEFAULT_CHUNK_SIZE = 256 * 1024; // 256KB
|
|
5
|
-
const AES_GCM_IV_LENGTH = 12;
|
|
6
|
-
const AES_GCM_TAG_LENGTH = 128;
|
|
7
|
-
|
|
8
|
-
/** IV 存在 Parcel.extra 中的 JSON 键 */
|
|
9
|
-
const EXTRA_IV_KEY = "iv";
|
|
10
|
-
|
|
11
|
-
/** 可插拔的存储适配器:不同文件系统 */
|
|
12
|
-
interface StorageAdapter {
|
|
13
|
-
/** 上传一段数据,返回可下载的 URL */
|
|
14
|
-
upload(data: ArrayBuffer, pathOrKey: string): Promise<{ url: string }>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface CreateOptions {
|
|
18
|
-
/** 每个 chunk 的字节数,默认 256KB */
|
|
19
|
-
chunkSize?: number;
|
|
20
|
-
/** AES-GCM 加密密钥;不传则生成并返回 */
|
|
21
|
-
key?: CryptoKey;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface CreateResult {
|
|
25
|
-
/** 元数据,chunk 的 url 由 save 填充 */
|
|
26
|
-
details: ParcelDetails;
|
|
27
|
-
/** 已加密的 chunk 数据,供 save 上传 */
|
|
28
|
-
chunks: ArrayBuffer[];
|
|
29
|
-
/** Base64 编码的 IV,需在 save 时写入 Parcel.extra 供 reassembly 使用 */
|
|
30
|
-
ivBase64: string;
|
|
31
|
-
/** 若 create 时未传 key,则返回此处,调用方需妥善保存 */
|
|
32
|
-
key?: CryptoKey;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** 调用方提供的 Target 基础字段,用于拼成完整 Parcel */
|
|
36
|
-
type ParcelTargetBase = Pick<Parcel, "id" | "category" | "name" | "value" | "tagList" | "created_at">;
|
|
37
|
-
|
|
38
|
-
function getCrypto(): Crypto {
|
|
39
|
-
if (typeof globalThis !== "undefined" && globalThis.crypto) return globalThis.crypto;
|
|
40
|
-
throw new Error("ParcelManager: crypto not available");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** 计算 ArrayBuffer 的 SHA-256,返回 hex 字符串 */
|
|
44
|
-
async function sha256Hex(buffer: ArrayBuffer): Promise<string> {
|
|
45
|
-
const crypto = getCrypto();
|
|
46
|
-
const hash = await crypto.subtle.digest("SHA-256", buffer);
|
|
47
|
-
return Array.from(new Uint8Array(hash))
|
|
48
|
-
.map((b) => b.toString(16).padStart(2, "0"))
|
|
49
|
-
.join("");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** AES-GCM 加密 */
|
|
53
|
-
async function encrypt(data: ArrayBuffer, key: CryptoKey, iv: Uint8Array): Promise<ArrayBuffer> {
|
|
54
|
-
const crypto = getCrypto();
|
|
55
|
-
const ivCopy = new Uint8Array(iv);
|
|
56
|
-
return crypto.subtle.encrypt({ name: "AES-GCM", iv: ivCopy, tagLength: AES_GCM_TAG_LENGTH }, key, data);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** AES-GCM 解密 */
|
|
60
|
-
async function decrypt(data: ArrayBuffer, key: CryptoKey, iv: Uint8Array): Promise<ArrayBuffer> {
|
|
61
|
-
const crypto = getCrypto();
|
|
62
|
-
const ivCopy = new Uint8Array(iv);
|
|
63
|
-
return crypto.subtle.decrypt({ name: "AES-GCM", iv: ivCopy, tagLength: AES_GCM_TAG_LENGTH }, key, data);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** 生成 AES-GCM 密钥(256 位) */
|
|
67
|
-
async function generateKey(): Promise<CryptoKey> {
|
|
68
|
-
const crypto = getCrypto();
|
|
69
|
-
return crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** 将文件加密后拆分成多个 chunk */
|
|
73
|
-
async function create(file: ArrayBuffer, options: CreateOptions = {}): Promise<CreateResult> {
|
|
74
|
-
const chunkSize = options.chunkSize ?? DEFAULT_CHUNK_SIZE;
|
|
75
|
-
let key = options.key;
|
|
76
|
-
const generatedKey = !key;
|
|
77
|
-
if (!key) key = await generateKey();
|
|
78
|
-
|
|
79
|
-
const crypto = getCrypto();
|
|
80
|
-
const iv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_LENGTH));
|
|
81
|
-
|
|
82
|
-
const encrypted = await encrypt(file, key, iv);
|
|
83
|
-
const totalSize = encrypted.byteLength;
|
|
84
|
-
|
|
85
|
-
const chunks: ArrayBuffer[] = [];
|
|
86
|
-
const chunkList: Chunk[] = [];
|
|
87
|
-
let offset = 0;
|
|
88
|
-
let index = 0;
|
|
89
|
-
|
|
90
|
-
while (offset < totalSize) {
|
|
91
|
-
const end = Math.min(offset + chunkSize, totalSize);
|
|
92
|
-
const chunkBuffer = encrypted.slice(offset, end);
|
|
93
|
-
chunks.push(chunkBuffer);
|
|
94
|
-
const checksum = await sha256Hex(chunkBuffer);
|
|
95
|
-
chunkList.push({
|
|
96
|
-
index,
|
|
97
|
-
size: chunkBuffer.byteLength,
|
|
98
|
-
checksum,
|
|
99
|
-
url: "", // 由 save 填充
|
|
100
|
-
});
|
|
101
|
-
offset = end;
|
|
102
|
-
index += 1;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const checksum = await sha256Hex(file);
|
|
106
|
-
|
|
107
|
-
const details: ParcelDetails = {
|
|
108
|
-
manifestVersion: MANIFEST_VERSION,
|
|
109
|
-
chunkList: chunkList,
|
|
110
|
-
checksum,
|
|
111
|
-
size: file.byteLength,
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const result: CreateResult = {
|
|
115
|
-
details,
|
|
116
|
-
chunks,
|
|
117
|
-
ivBase64: btoa(String.fromCharCode(...iv)),
|
|
118
|
-
};
|
|
119
|
-
if (generatedKey) result.key = key;
|
|
120
|
-
return result;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** 将 chunk 均匀分布到不同文件系统并存储 */
|
|
124
|
-
async function save(
|
|
125
|
-
createResult: CreateResult,
|
|
126
|
-
fileSystems: StorageAdapter[],
|
|
127
|
-
targetBase: ParcelTargetBase
|
|
128
|
-
): Promise<Parcel> {
|
|
129
|
-
if (fileSystems.length === 0) throw new Error("ParcelManager.save: fileSystems 不能为空");
|
|
130
|
-
const { details, chunks, ivBase64 } = createResult;
|
|
131
|
-
|
|
132
|
-
const chunkListWithUrls: Chunk[] = [];
|
|
133
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
134
|
-
const fs = fileSystems[i % fileSystems.length];
|
|
135
|
-
const pathOrKey = `chunk-${i}-${details.checksum.slice(0, 8)}`;
|
|
136
|
-
const { url } = await fs.upload(chunks[i], pathOrKey);
|
|
137
|
-
chunkListWithUrls.push({ ...details.chunkList[i], url });
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const extra = JSON.stringify({ [EXTRA_IV_KEY]: ivBase64 });
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
...targetBase,
|
|
144
|
-
category: "parcel" as Parcel["category"],
|
|
145
|
-
details: {
|
|
146
|
-
...details,
|
|
147
|
-
chunkList: chunkListWithUrls,
|
|
148
|
-
},
|
|
149
|
-
extra,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** 根据 Parcel 下载各 chunk,重组并校验后返回原文件 */
|
|
154
|
-
async function reassembly(parcel: Parcel, key: CryptoKey): Promise<ArrayBuffer> {
|
|
155
|
-
const { details } = parcel;
|
|
156
|
-
let ivBase64: string | undefined;
|
|
157
|
-
try {
|
|
158
|
-
const extra = parcel.extra ? JSON.parse(parcel.extra) : {};
|
|
159
|
-
ivBase64 = extra[EXTRA_IV_KEY];
|
|
160
|
-
} catch {
|
|
161
|
-
throw new Error("ParcelManager.reassembly: 无法从 extra 解析 IV");
|
|
162
|
-
}
|
|
163
|
-
if (!ivBase64 || typeof ivBase64 !== "string") {
|
|
164
|
-
throw new Error("ParcelManager.reassembly: Parcel.extra 中缺少 iv");
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const iv = Uint8Array.from(atob(ivBase64), (c) => c.charCodeAt(0));
|
|
168
|
-
if (iv.length !== AES_GCM_IV_LENGTH) {
|
|
169
|
-
throw new Error("ParcelManager.reassembly: IV 长度无效");
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const sortedChunks = [...details.chunkList].sort((a, b) => a.index - b.index);
|
|
173
|
-
const buffers: ArrayBuffer[] = [];
|
|
174
|
-
|
|
175
|
-
for (const chunkMeta of sortedChunks) {
|
|
176
|
-
const res = await fetch(chunkMeta.url);
|
|
177
|
-
if (!res.ok) throw new Error(`ParcelManager.reassembly: 下载 chunk ${chunkMeta.index} 失败: ${res.status}`);
|
|
178
|
-
const chunkBuffer = await res.arrayBuffer();
|
|
179
|
-
const chunkChecksum = await sha256Hex(chunkBuffer);
|
|
180
|
-
if (chunkChecksum !== chunkMeta.checksum) {
|
|
181
|
-
throw new Error(`ParcelManager.reassembly: chunk ${chunkMeta.index} 校验失败 (checksum 不一致)`);
|
|
182
|
-
}
|
|
183
|
-
buffers.push(chunkBuffer);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const totalEncryptedLength = buffers.reduce((s, b) => s + b.byteLength, 0);
|
|
187
|
-
const encrypted = new Uint8Array(totalEncryptedLength);
|
|
188
|
-
let offset = 0;
|
|
189
|
-
for (const b of buffers) {
|
|
190
|
-
encrypted.set(new Uint8Array(b), offset);
|
|
191
|
-
offset += b.byteLength;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const decrypted = await decrypt(encrypted.buffer, key, iv);
|
|
195
|
-
const fileChecksum = await sha256Hex(decrypted);
|
|
196
|
-
if (fileChecksum !== details.checksum) {
|
|
197
|
-
throw new Error("ParcelManager.reassembly: 重组后文件校验失败 (sha256 与 details.checksum 不一致)");
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return decrypted;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const ParcelManager = {
|
|
204
|
-
create,
|
|
205
|
-
save,
|
|
206
|
-
reassembly,
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
export default ParcelManager;
|
|
1
|
+
import type { Chunk, Parcel, ParcelDetails } from "./parcel.interface";
|
|
2
|
+
|
|
3
|
+
const MANIFEST_VERSION = 1;
|
|
4
|
+
const DEFAULT_CHUNK_SIZE = 256 * 1024; // 256KB
|
|
5
|
+
const AES_GCM_IV_LENGTH = 12;
|
|
6
|
+
const AES_GCM_TAG_LENGTH = 128;
|
|
7
|
+
|
|
8
|
+
/** IV 存在 Parcel.extra 中的 JSON 键 */
|
|
9
|
+
const EXTRA_IV_KEY = "iv";
|
|
10
|
+
|
|
11
|
+
/** 可插拔的存储适配器:不同文件系统 */
|
|
12
|
+
interface StorageAdapter {
|
|
13
|
+
/** 上传一段数据,返回可下载的 URL */
|
|
14
|
+
upload(data: ArrayBuffer, pathOrKey: string): Promise<{ url: string }>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface CreateOptions {
|
|
18
|
+
/** 每个 chunk 的字节数,默认 256KB */
|
|
19
|
+
chunkSize?: number;
|
|
20
|
+
/** AES-GCM 加密密钥;不传则生成并返回 */
|
|
21
|
+
key?: CryptoKey;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface CreateResult {
|
|
25
|
+
/** 元数据,chunk 的 url 由 save 填充 */
|
|
26
|
+
details: ParcelDetails;
|
|
27
|
+
/** 已加密的 chunk 数据,供 save 上传 */
|
|
28
|
+
chunks: ArrayBuffer[];
|
|
29
|
+
/** Base64 编码的 IV,需在 save 时写入 Parcel.extra 供 reassembly 使用 */
|
|
30
|
+
ivBase64: string;
|
|
31
|
+
/** 若 create 时未传 key,则返回此处,调用方需妥善保存 */
|
|
32
|
+
key?: CryptoKey;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 调用方提供的 Target 基础字段,用于拼成完整 Parcel */
|
|
36
|
+
type ParcelTargetBase = Pick<Parcel, "id" | "category" | "name" | "value" | "tagList" | "created_at">;
|
|
37
|
+
|
|
38
|
+
function getCrypto(): Crypto {
|
|
39
|
+
if (typeof globalThis !== "undefined" && globalThis.crypto) return globalThis.crypto;
|
|
40
|
+
throw new Error("ParcelManager: crypto not available");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 计算 ArrayBuffer 的 SHA-256,返回 hex 字符串 */
|
|
44
|
+
async function sha256Hex(buffer: ArrayBuffer): Promise<string> {
|
|
45
|
+
const crypto = getCrypto();
|
|
46
|
+
const hash = await crypto.subtle.digest("SHA-256", buffer);
|
|
47
|
+
return Array.from(new Uint8Array(hash))
|
|
48
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
49
|
+
.join("");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** AES-GCM 加密 */
|
|
53
|
+
async function encrypt(data: ArrayBuffer, key: CryptoKey, iv: Uint8Array): Promise<ArrayBuffer> {
|
|
54
|
+
const crypto = getCrypto();
|
|
55
|
+
const ivCopy = new Uint8Array(iv);
|
|
56
|
+
return crypto.subtle.encrypt({ name: "AES-GCM", iv: ivCopy, tagLength: AES_GCM_TAG_LENGTH }, key, data);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** AES-GCM 解密 */
|
|
60
|
+
async function decrypt(data: ArrayBuffer, key: CryptoKey, iv: Uint8Array): Promise<ArrayBuffer> {
|
|
61
|
+
const crypto = getCrypto();
|
|
62
|
+
const ivCopy = new Uint8Array(iv);
|
|
63
|
+
return crypto.subtle.decrypt({ name: "AES-GCM", iv: ivCopy, tagLength: AES_GCM_TAG_LENGTH }, key, data);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 生成 AES-GCM 密钥(256 位) */
|
|
67
|
+
async function generateKey(): Promise<CryptoKey> {
|
|
68
|
+
const crypto = getCrypto();
|
|
69
|
+
return crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 将文件加密后拆分成多个 chunk */
|
|
73
|
+
async function create(file: ArrayBuffer, options: CreateOptions = {}): Promise<CreateResult> {
|
|
74
|
+
const chunkSize = options.chunkSize ?? DEFAULT_CHUNK_SIZE;
|
|
75
|
+
let key = options.key;
|
|
76
|
+
const generatedKey = !key;
|
|
77
|
+
if (!key) key = await generateKey();
|
|
78
|
+
|
|
79
|
+
const crypto = getCrypto();
|
|
80
|
+
const iv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_LENGTH));
|
|
81
|
+
|
|
82
|
+
const encrypted = await encrypt(file, key, iv);
|
|
83
|
+
const totalSize = encrypted.byteLength;
|
|
84
|
+
|
|
85
|
+
const chunks: ArrayBuffer[] = [];
|
|
86
|
+
const chunkList: Chunk[] = [];
|
|
87
|
+
let offset = 0;
|
|
88
|
+
let index = 0;
|
|
89
|
+
|
|
90
|
+
while (offset < totalSize) {
|
|
91
|
+
const end = Math.min(offset + chunkSize, totalSize);
|
|
92
|
+
const chunkBuffer = encrypted.slice(offset, end);
|
|
93
|
+
chunks.push(chunkBuffer);
|
|
94
|
+
const checksum = await sha256Hex(chunkBuffer);
|
|
95
|
+
chunkList.push({
|
|
96
|
+
index,
|
|
97
|
+
size: chunkBuffer.byteLength,
|
|
98
|
+
checksum,
|
|
99
|
+
url: "", // 由 save 填充
|
|
100
|
+
});
|
|
101
|
+
offset = end;
|
|
102
|
+
index += 1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const checksum = await sha256Hex(file);
|
|
106
|
+
|
|
107
|
+
const details: ParcelDetails = {
|
|
108
|
+
manifestVersion: MANIFEST_VERSION,
|
|
109
|
+
chunkList: chunkList,
|
|
110
|
+
checksum,
|
|
111
|
+
size: file.byteLength,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const result: CreateResult = {
|
|
115
|
+
details,
|
|
116
|
+
chunks,
|
|
117
|
+
ivBase64: btoa(String.fromCharCode(...iv)),
|
|
118
|
+
};
|
|
119
|
+
if (generatedKey) result.key = key;
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 将 chunk 均匀分布到不同文件系统并存储 */
|
|
124
|
+
async function save(
|
|
125
|
+
createResult: CreateResult,
|
|
126
|
+
fileSystems: StorageAdapter[],
|
|
127
|
+
targetBase: ParcelTargetBase
|
|
128
|
+
): Promise<Parcel> {
|
|
129
|
+
if (fileSystems.length === 0) throw new Error("ParcelManager.save: fileSystems 不能为空");
|
|
130
|
+
const { details, chunks, ivBase64 } = createResult;
|
|
131
|
+
|
|
132
|
+
const chunkListWithUrls: Chunk[] = [];
|
|
133
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
134
|
+
const fs = fileSystems[i % fileSystems.length];
|
|
135
|
+
const pathOrKey = `chunk-${i}-${details.checksum.slice(0, 8)}`;
|
|
136
|
+
const { url } = await fs.upload(chunks[i], pathOrKey);
|
|
137
|
+
chunkListWithUrls.push({ ...details.chunkList[i], url });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const extra = JSON.stringify({ [EXTRA_IV_KEY]: ivBase64 });
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
...targetBase,
|
|
144
|
+
category: "parcel" as Parcel["category"],
|
|
145
|
+
details: {
|
|
146
|
+
...details,
|
|
147
|
+
chunkList: chunkListWithUrls,
|
|
148
|
+
},
|
|
149
|
+
extra,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 根据 Parcel 下载各 chunk,重组并校验后返回原文件 */
|
|
154
|
+
async function reassembly(parcel: Parcel, key: CryptoKey): Promise<ArrayBuffer> {
|
|
155
|
+
const { details } = parcel;
|
|
156
|
+
let ivBase64: string | undefined;
|
|
157
|
+
try {
|
|
158
|
+
const extra = parcel.extra ? JSON.parse(parcel.extra) : {};
|
|
159
|
+
ivBase64 = extra[EXTRA_IV_KEY];
|
|
160
|
+
} catch {
|
|
161
|
+
throw new Error("ParcelManager.reassembly: 无法从 extra 解析 IV");
|
|
162
|
+
}
|
|
163
|
+
if (!ivBase64 || typeof ivBase64 !== "string") {
|
|
164
|
+
throw new Error("ParcelManager.reassembly: Parcel.extra 中缺少 iv");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const iv = Uint8Array.from(atob(ivBase64), (c) => c.charCodeAt(0));
|
|
168
|
+
if (iv.length !== AES_GCM_IV_LENGTH) {
|
|
169
|
+
throw new Error("ParcelManager.reassembly: IV 长度无效");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const sortedChunks = [...details.chunkList].sort((a, b) => a.index - b.index);
|
|
173
|
+
const buffers: ArrayBuffer[] = [];
|
|
174
|
+
|
|
175
|
+
for (const chunkMeta of sortedChunks) {
|
|
176
|
+
const res = await fetch(chunkMeta.url);
|
|
177
|
+
if (!res.ok) throw new Error(`ParcelManager.reassembly: 下载 chunk ${chunkMeta.index} 失败: ${res.status}`);
|
|
178
|
+
const chunkBuffer = await res.arrayBuffer();
|
|
179
|
+
const chunkChecksum = await sha256Hex(chunkBuffer);
|
|
180
|
+
if (chunkChecksum !== chunkMeta.checksum) {
|
|
181
|
+
throw new Error(`ParcelManager.reassembly: chunk ${chunkMeta.index} 校验失败 (checksum 不一致)`);
|
|
182
|
+
}
|
|
183
|
+
buffers.push(chunkBuffer);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const totalEncryptedLength = buffers.reduce((s, b) => s + b.byteLength, 0);
|
|
187
|
+
const encrypted = new Uint8Array(totalEncryptedLength);
|
|
188
|
+
let offset = 0;
|
|
189
|
+
for (const b of buffers) {
|
|
190
|
+
encrypted.set(new Uint8Array(b), offset);
|
|
191
|
+
offset += b.byteLength;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const decrypted = await decrypt(encrypted.buffer, key, iv);
|
|
195
|
+
const fileChecksum = await sha256Hex(decrypted);
|
|
196
|
+
if (fileChecksum !== details.checksum) {
|
|
197
|
+
throw new Error("ParcelManager.reassembly: 重组后文件校验失败 (sha256 与 details.checksum 不一致)");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return decrypted;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const ParcelManager = {
|
|
204
|
+
create,
|
|
205
|
+
save,
|
|
206
|
+
reassembly,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export default ParcelManager;
|
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
import { Target } from "../core.interface";
|
|
2
|
-
|
|
3
|
-
export interface Parcel extends Target {
|
|
4
|
-
category: CategoryParcel;
|
|
5
|
-
details: ParcelDetails;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export enum CategoryParcel {
|
|
9
|
-
PARCEL = "parcel",
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface ParcelDetails {
|
|
13
|
-
manifestVersion: number;
|
|
14
|
-
chunkList: Chunk[];
|
|
15
|
-
/** sha256 */
|
|
16
|
-
checksum: string;
|
|
17
|
-
size: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export enum LifecycleStatus {
|
|
21
|
-
STORED = "STORED",
|
|
22
|
-
SOFT_DELETED = "SOFT_DELETED",
|
|
23
|
-
HARD_DELETED = "HARD_DELETED",
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface Lifecycle {
|
|
27
|
-
status: LifecycleStatus;
|
|
28
|
-
storedAt: string;
|
|
29
|
-
softDeletedAt: string | null;
|
|
30
|
-
hardDeletedAt: string | null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface Chunk {
|
|
34
|
-
index: number;
|
|
35
|
-
/** TODO Storage service provider */
|
|
36
|
-
// shardKey: string;
|
|
37
|
-
size: number;
|
|
38
|
-
checksum: string;
|
|
39
|
-
url: string;
|
|
40
|
-
}
|
|
1
|
+
import { Target } from "../core.interface";
|
|
2
|
+
|
|
3
|
+
export interface Parcel extends Target {
|
|
4
|
+
category: CategoryParcel;
|
|
5
|
+
details: ParcelDetails;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export enum CategoryParcel {
|
|
9
|
+
PARCEL = "parcel",
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ParcelDetails {
|
|
13
|
+
manifestVersion: number;
|
|
14
|
+
chunkList: Chunk[];
|
|
15
|
+
/** sha256 */
|
|
16
|
+
checksum: string;
|
|
17
|
+
size: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export enum LifecycleStatus {
|
|
21
|
+
STORED = "STORED",
|
|
22
|
+
SOFT_DELETED = "SOFT_DELETED",
|
|
23
|
+
HARD_DELETED = "HARD_DELETED",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface Lifecycle {
|
|
27
|
+
status: LifecycleStatus;
|
|
28
|
+
storedAt: string;
|
|
29
|
+
softDeletedAt: string | null;
|
|
30
|
+
hardDeletedAt: string | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Chunk {
|
|
34
|
+
index: number;
|
|
35
|
+
/** TODO Storage service provider */
|
|
36
|
+
// shardKey: string;
|
|
37
|
+
size: number;
|
|
38
|
+
checksum: string;
|
|
39
|
+
url: string;
|
|
40
|
+
}
|
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
import { createTarget } from "../core.api";
|
|
2
|
-
import { BaseValidator } from "../core.utils";
|
|
3
|
-
import { QuestionList, CategoryQuestionList, QuestionListDetails } from "./question-list.interface";
|
|
4
|
-
|
|
5
|
-
export class PostCreateQuestionListValidator extends BaseValidator<PostCreateQuestionListPayload> {
|
|
6
|
-
protected requiredFields: (keyof PostCreateQuestionListPayload)[] = ["name", "list"];
|
|
7
|
-
protected optionalFields: (keyof PostCreateQuestionListPayload)[] = [];
|
|
8
|
-
|
|
9
|
-
constructor() {
|
|
10
|
-
super();
|
|
11
|
-
// Add custom
|
|
12
|
-
this.addValidator((val) => {
|
|
13
|
-
return true;
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface PostCreateQuestionListPayload {
|
|
19
|
-
name: QuestionListDetails["name"];
|
|
20
|
-
list: QuestionListDetails["list"];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export const postCreateQuestionList = async (payload: PostCreateQuestionListPayload) => {
|
|
24
|
-
return createTarget<QuestionList, PostCreateQuestionListPayload>({
|
|
25
|
-
payload: payload,
|
|
26
|
-
validator: PostCreateQuestionListValidator,
|
|
27
|
-
createFn: (validPayload) => {
|
|
28
|
-
const { name, list } = validPayload;
|
|
29
|
-
const details: QuestionListDetails = {
|
|
30
|
-
name,
|
|
31
|
-
list,
|
|
32
|
-
};
|
|
33
|
-
return {
|
|
34
|
-
name: validPayload.name,
|
|
35
|
-
category: CategoryQuestionList.QUESTION_LIST,
|
|
36
|
-
value: "",
|
|
37
|
-
tagList: [],
|
|
38
|
-
details,
|
|
39
|
-
};
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
};
|
|
1
|
+
import { createTarget } from "../core.api";
|
|
2
|
+
import { BaseValidator } from "../core.utils";
|
|
3
|
+
import { QuestionList, CategoryQuestionList, QuestionListDetails } from "./question-list.interface";
|
|
4
|
+
|
|
5
|
+
export class PostCreateQuestionListValidator extends BaseValidator<PostCreateQuestionListPayload> {
|
|
6
|
+
protected requiredFields: (keyof PostCreateQuestionListPayload)[] = ["name", "list"];
|
|
7
|
+
protected optionalFields: (keyof PostCreateQuestionListPayload)[] = [];
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
super();
|
|
11
|
+
// Add custom
|
|
12
|
+
this.addValidator((val) => {
|
|
13
|
+
return true;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PostCreateQuestionListPayload {
|
|
19
|
+
name: QuestionListDetails["name"];
|
|
20
|
+
list: QuestionListDetails["list"];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const postCreateQuestionList = async (payload: PostCreateQuestionListPayload) => {
|
|
24
|
+
return createTarget<QuestionList, PostCreateQuestionListPayload>({
|
|
25
|
+
payload: payload,
|
|
26
|
+
validator: PostCreateQuestionListValidator,
|
|
27
|
+
createFn: (validPayload) => {
|
|
28
|
+
const { name, list } = validPayload;
|
|
29
|
+
const details: QuestionListDetails = {
|
|
30
|
+
name,
|
|
31
|
+
list,
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
name: validPayload.name,
|
|
35
|
+
category: CategoryQuestionList.QUESTION_LIST,
|
|
36
|
+
value: "",
|
|
37
|
+
tagList: [],
|
|
38
|
+
details,
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
};
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { Target } from "../core.interface";
|
|
2
|
-
|
|
3
|
-
export interface Question {
|
|
4
|
-
ans: string;
|
|
5
|
-
reminder: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export type QuestionListDetails = {
|
|
9
|
-
/** Human readable name, should be same to Target.name */
|
|
10
|
-
name: string;
|
|
11
|
-
list: Question[];
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export enum CategoryQuestionList {
|
|
15
|
-
QUESTION_LIST = "question-list",
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface QuestionList extends Target {
|
|
19
|
-
category: CategoryQuestionList;
|
|
20
|
-
details: QuestionListDetails;
|
|
21
|
-
}
|
|
1
|
+
import { Target } from "../core.interface";
|
|
2
|
+
|
|
3
|
+
export interface Question {
|
|
4
|
+
ans: string;
|
|
5
|
+
reminder: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type QuestionListDetails = {
|
|
9
|
+
/** Human readable name, should be same to Target.name */
|
|
10
|
+
name: string;
|
|
11
|
+
list: Question[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export enum CategoryQuestionList {
|
|
15
|
+
QUESTION_LIST = "question-list",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface QuestionList extends Target {
|
|
19
|
+
category: CategoryQuestionList;
|
|
20
|
+
details: QuestionListDetails;
|
|
21
|
+
}
|