mlclaw 0.1.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.
- package/.agents/skills/mlclaw/SKILL.md +214 -0
- package/.agents/skills/mlclaw/agents/openai.yaml +4 -0
- package/.gitattributes +35 -0
- package/Dockerfile +45 -0
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/assets/mlclaw.svg +143 -0
- package/dist/hf-state-sync.js +9532 -0
- package/dist/mlclaw-space-runtime.js +5010 -0
- package/dist/mlclaw.mjs +16502 -0
- package/entrypoint.sh +87 -0
- package/mlclaw.ps1 +108 -0
- package/mlclaw.sh +117 -0
- package/openclaw.default.json +67 -0
- package/package.json +66 -0
- package/scripts/configure-huggingface-model.mjs +86 -0
- package/scripts/configure-telegram.mjs +55 -0
- package/scripts/report-telegram-probe.mjs +18 -0
- package/space/README.md +42 -0
- package/src/hf-bucket-client/client.ts +217 -0
- package/src/hf-state-sync/archive.ts +137 -0
- package/src/hf-state-sync/cli.ts +92 -0
- package/src/hf-state-sync/hub.ts +81 -0
- package/src/hf-state-sync/manifest.ts +67 -0
- package/src/hf-state-sync/paths.ts +73 -0
- package/src/hf-state-sync/restore.ts +109 -0
- package/src/hf-state-sync/snapshot.ts +133 -0
- package/src/hf-state-sync/sqlite.ts +57 -0
- package/src/hf-state-sync/supervise.ts +256 -0
- package/src/vendor/hfjs-xet/error.ts +52 -0
- package/src/vendor/hfjs-xet/types/public.ts +207 -0
- package/src/vendor/hfjs-xet/utils/ChunkCache.ts +102 -0
- package/src/vendor/hfjs-xet/utils/RangeList.ts +182 -0
- package/src/vendor/hfjs-xet/utils/SplicedBlob.ts +249 -0
- package/src/vendor/hfjs-xet/utils/XetBlob.ts +732 -0
- package/src/vendor/hfjs-xet/utils/checkCredentials.ts +21 -0
- package/src/vendor/hfjs-xet/utils/combineUint8Arrays.ts +13 -0
- package/src/vendor/hfjs-xet/utils/createXorbs.ts +782 -0
- package/src/vendor/hfjs-xet/utils/shardParser.ts +152 -0
- package/src/vendor/hfjs-xet/utils/sum.ts +9 -0
- package/src/vendor/hfjs-xet/utils/uploadShards.ts +443 -0
- package/src/vendor/hfjs-xet/utils/xetWriteToken.ts +101 -0
- package/src/vendor/hfjs-xet/vendor/lz4js/index.ts +540 -0
- package/src/vendor/hfjs-xet/vendor/lz4js/util.ts +57 -0
- package/src/vendor/hfjs-xet/vendor/lz4js/xxh32.ts +99 -0
- package/src/vendor/hfjs-xet/vendor/type-fest/basic.ts +34 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// @ts-nocheck -- vendored upstream code, kept verbatim; our strict tsconfig does not apply.
|
|
2
|
+
// Vendored from huggingface/huggingface.js@f8fdf6be (packages/hub/src/utils/shardParser.ts), MIT License.
|
|
3
|
+
// Delete this directory when bucket support is upstreamed to @huggingface/hub.
|
|
4
|
+
import { SHARD_FOOTER_VERSION, SHARD_HEADER_VERSION, SHARD_MAGIC_TAG } from "./uploadShards";
|
|
5
|
+
|
|
6
|
+
const HASH_LENGTH = 32;
|
|
7
|
+
const XORB_HASH_BOOKEND = "ff".repeat(HASH_LENGTH);
|
|
8
|
+
|
|
9
|
+
// Read 4 uint64 in little endian and convert to hex
|
|
10
|
+
function readHashFromArray(array: Uint8Array, offset: number): string {
|
|
11
|
+
let hash = "";
|
|
12
|
+
for (let i = 0; i < HASH_LENGTH; i += 8) {
|
|
13
|
+
hash += `${array[offset + i + 7].toString(16).padStart(2, "0")}${array[offset + i + 6]
|
|
14
|
+
.toString(16)
|
|
15
|
+
.padStart(2, "0")}${array[offset + i + 5].toString(16).padStart(2, "0")}${array[offset + i + 4]
|
|
16
|
+
.toString(16)
|
|
17
|
+
.padStart(2, "0")}${array[offset + i + 3].toString(16).padStart(2, "0")}${array[offset + i + 2]
|
|
18
|
+
.toString(16)
|
|
19
|
+
.padStart(2, "0")}${array[offset + i + 1].toString(16).padStart(2, "0")}${array[offset + i]
|
|
20
|
+
.toString(16)
|
|
21
|
+
.padStart(2, "0")}`;
|
|
22
|
+
}
|
|
23
|
+
return hash;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ShardData {
|
|
27
|
+
hmacKey: string;
|
|
28
|
+
xorbs: Array<{
|
|
29
|
+
hash: string;
|
|
30
|
+
chunks: Array<{
|
|
31
|
+
hash: string;
|
|
32
|
+
startOffset: number;
|
|
33
|
+
unpackedLength: number;
|
|
34
|
+
}>;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function parseShardData(shardBlob: Blob): Promise<ShardData> {
|
|
39
|
+
const shard = new Uint8Array(await shardBlob.arrayBuffer());
|
|
40
|
+
const shardView = new DataView(shard.buffer);
|
|
41
|
+
|
|
42
|
+
const magicTag = shard.slice(0, SHARD_MAGIC_TAG.length);
|
|
43
|
+
if (!magicTag.every((byte, i) => byte === SHARD_MAGIC_TAG[i])) {
|
|
44
|
+
throw new Error("Invalid shard magic tag");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const version = shardView.getBigUint64(SHARD_MAGIC_TAG.length, true);
|
|
48
|
+
if (version !== SHARD_HEADER_VERSION) {
|
|
49
|
+
throw new Error(`Invalid shard version: ${version}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const footerSize = Number(shardView.getBigUint64(SHARD_MAGIC_TAG.length + 8, true));
|
|
53
|
+
|
|
54
|
+
// Read footer to get section offsets
|
|
55
|
+
const footerStart = shard.length - footerSize;
|
|
56
|
+
const footerVersion = shardView.getBigUint64(footerStart, true);
|
|
57
|
+
if (footerVersion !== SHARD_FOOTER_VERSION) {
|
|
58
|
+
throw new Error(`Invalid shard footer version: ${footerVersion}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// version: u64, // Footer version (must be 1)
|
|
62
|
+
// file_info_offset: u64, // Offset to file info section
|
|
63
|
+
// cas_info_offset: u64, // Offset to CAS info section
|
|
64
|
+
// file_lookup_offset: u64, // Offset to file lookup table
|
|
65
|
+
// file_lookup_num_entry: u64, // Number of file lookup entries
|
|
66
|
+
// cas_lookup_offset: u64, // Offset to CAS lookup table
|
|
67
|
+
// cas_lookup_num_entry: u64, // Number of CAS lookup entries
|
|
68
|
+
// chunk_lookup_offset: u64, // Offset to chunk lookup table
|
|
69
|
+
// chunk_lookup_num_entry: u64, // Number of chunk lookup entries
|
|
70
|
+
// chunk_hash_hmac_key: [u64; 4], // HMAC key for chunk hashes (32 bytes)
|
|
71
|
+
// shard_creation_timestamp: u64, // Creation time (seconds since epoch)
|
|
72
|
+
// shard_key_expiry: u64, // Expiry time (seconds since epoch)
|
|
73
|
+
// _buffer: [u64; 6], // Reserved space (48 bytes)
|
|
74
|
+
// stored_bytes_on_disk: u64, // Total bytes stored on disk
|
|
75
|
+
// materialized_bytes: u64, // Total materialized bytes
|
|
76
|
+
// stored_bytes: u64, // Total stored bytes
|
|
77
|
+
// footer_offset: u64,
|
|
78
|
+
|
|
79
|
+
// const fileInfoStart = Number(shardView.getBigUint64(footerStart + 8, true));
|
|
80
|
+
const xorbInfoStart = Number(shardView.getBigUint64(footerStart + 16, true));
|
|
81
|
+
const fileLookupStart = Number(shardView.getBigUint64(footerStart + 24, true));
|
|
82
|
+
// const numFileLookups = Number(shardView.getBigUint64(footerStart + 32, true));
|
|
83
|
+
// const xorbLookupStart = Number(shardView.getBigUint64(footerStart + 40, true));
|
|
84
|
+
// const numXorbLookups = Number(shardView.getBigUint64(footerStart + 48, true));
|
|
85
|
+
// const chunkLookupStart = Number(shardView.getBigUint64(footerStart + 56, true));
|
|
86
|
+
// const numChunkLookups = Number(shardView.getBigUint64(footerStart + 64, true));
|
|
87
|
+
const hmacKey = readHashFromArray(shard, footerStart + 72);
|
|
88
|
+
// const shardCreationTimestamp = Number(shardView.getBigUint64(footerStart + 104, true));
|
|
89
|
+
// const shardKeyExpiry = Number(shardView.getBigUint64(footerStart + 112, true));
|
|
90
|
+
// const storedBytesOnDisk = Number(shardView.getBigUint64(footerStart + 168, true));
|
|
91
|
+
// const materializedBytes = Number(shardView.getBigUint64(footerStart + 176, true));
|
|
92
|
+
// const storedBytes = Number(shardView.getBigUint64(footerStart + 184, true));
|
|
93
|
+
// const footerOffset = Number(shardView.getBigUint64(footerStart + 192, true));
|
|
94
|
+
|
|
95
|
+
// Parse XORB Info Section
|
|
96
|
+
const xorbs: ShardData["xorbs"] = [];
|
|
97
|
+
let offset = xorbInfoStart;
|
|
98
|
+
|
|
99
|
+
while (offset < fileLookupStart) {
|
|
100
|
+
// Read xorb entry
|
|
101
|
+
const xorbHash = readHashFromArray(shard, offset);
|
|
102
|
+
offset += HASH_LENGTH;
|
|
103
|
+
|
|
104
|
+
if (xorbHash === XORB_HASH_BOOKEND) {
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// const flags = shardView.getUint32(offset, true);
|
|
109
|
+
offset += 4;
|
|
110
|
+
|
|
111
|
+
const chunkCount = shardView.getUint32(offset, true);
|
|
112
|
+
offset += 4;
|
|
113
|
+
|
|
114
|
+
// const numBytesInXorb = shardView.getUint32(offset, true);
|
|
115
|
+
offset += 4;
|
|
116
|
+
|
|
117
|
+
// const numBytesUnpacked = shardView.getUint32(offset, true);
|
|
118
|
+
offset += 4;
|
|
119
|
+
|
|
120
|
+
// Read chunks for this xorb
|
|
121
|
+
const chunks: ShardData["xorbs"][number]["chunks"] = [];
|
|
122
|
+
for (let i = 0; i < chunkCount; i++) {
|
|
123
|
+
const chunkHash = readHashFromArray(shard, offset);
|
|
124
|
+
offset += HASH_LENGTH;
|
|
125
|
+
|
|
126
|
+
const startOffset = shardView.getUint32(offset, true);
|
|
127
|
+
offset += 4;
|
|
128
|
+
|
|
129
|
+
const length = shardView.getUint32(offset, true);
|
|
130
|
+
offset += 4;
|
|
131
|
+
|
|
132
|
+
// Skip reserved 8 bytes
|
|
133
|
+
offset += 8;
|
|
134
|
+
|
|
135
|
+
chunks.push({
|
|
136
|
+
hash: chunkHash,
|
|
137
|
+
startOffset,
|
|
138
|
+
unpackedLength: length,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
xorbs.push({
|
|
143
|
+
hash: xorbHash,
|
|
144
|
+
chunks,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
hmacKey,
|
|
150
|
+
xorbs,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @ts-nocheck -- vendored upstream code, kept verbatim; our strict tsconfig does not apply.
|
|
2
|
+
// Vendored from huggingface/huggingface.js@f8fdf6be (packages/hub/src/utils/sum.ts), MIT License.
|
|
3
|
+
// Delete this directory when bucket support is upstreamed to @huggingface/hub.
|
|
4
|
+
/**
|
|
5
|
+
* Sum of elements in array
|
|
6
|
+
*/
|
|
7
|
+
export function sum(arr: number[]): number {
|
|
8
|
+
return arr.reduce((a, b) => a + b, 0);
|
|
9
|
+
}
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
// @ts-nocheck -- vendored upstream code, kept verbatim; our strict tsconfig does not apply.
|
|
2
|
+
// Vendored from huggingface/huggingface.js@f8fdf6be (packages/hub/src/utils/uploadShards.ts), MIT License.
|
|
3
|
+
// Delete this directory when bucket support is upstreamed to @huggingface/hub.
|
|
4
|
+
import { createApiError } from "../error";
|
|
5
|
+
import type { RepoId } from "../types/public";
|
|
6
|
+
import { createXorbs } from "./createXorbs";
|
|
7
|
+
import { sum } from "./sum";
|
|
8
|
+
import { xetWriteToken } from "./xetWriteToken";
|
|
9
|
+
|
|
10
|
+
const SHARD_MAX_SIZE = 64 * 1024 * 1024;
|
|
11
|
+
const SHARD_HEADER_SIZE = 48;
|
|
12
|
+
const SHARD_FOOTER_SIZE = 200;
|
|
13
|
+
const HASH_LENGTH = 32;
|
|
14
|
+
const XORB_FOOTER_LENGTH = 48;
|
|
15
|
+
const FILE_FOOTER_LENGTH = 48;
|
|
16
|
+
export const SHARD_HEADER_VERSION = 2n;
|
|
17
|
+
export const SHARD_FOOTER_VERSION = 1n;
|
|
18
|
+
|
|
19
|
+
const MDB_FILE_FLAG_WITH_VERIFICATION = 0x80000000; // Cannot define as 1 << 31 because it becomes a negative number
|
|
20
|
+
const MDB_FILE_FLAG_WITH_METADATA_EXT = 0x40000000;
|
|
21
|
+
|
|
22
|
+
export const SHARD_MAGIC_TAG = new Uint8Array([
|
|
23
|
+
"H".charCodeAt(0),
|
|
24
|
+
"F".charCodeAt(0),
|
|
25
|
+
"R".charCodeAt(0),
|
|
26
|
+
"e".charCodeAt(0),
|
|
27
|
+
"p".charCodeAt(0),
|
|
28
|
+
"o".charCodeAt(0),
|
|
29
|
+
"M".charCodeAt(0),
|
|
30
|
+
"e".charCodeAt(0),
|
|
31
|
+
"t".charCodeAt(0),
|
|
32
|
+
"a".charCodeAt(0),
|
|
33
|
+
"D".charCodeAt(0),
|
|
34
|
+
"a".charCodeAt(0),
|
|
35
|
+
"t".charCodeAt(0),
|
|
36
|
+
"a".charCodeAt(0),
|
|
37
|
+
0,
|
|
38
|
+
85,
|
|
39
|
+
105,
|
|
40
|
+
103,
|
|
41
|
+
69,
|
|
42
|
+
106,
|
|
43
|
+
123,
|
|
44
|
+
129,
|
|
45
|
+
87,
|
|
46
|
+
131,
|
|
47
|
+
165,
|
|
48
|
+
189,
|
|
49
|
+
217,
|
|
50
|
+
92,
|
|
51
|
+
205,
|
|
52
|
+
209,
|
|
53
|
+
74,
|
|
54
|
+
169,
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
export interface XetTokenParams {
|
|
58
|
+
sessionId?: string;
|
|
59
|
+
casUrl?: string;
|
|
60
|
+
accessToken?: string;
|
|
61
|
+
expiresAt?: Date;
|
|
62
|
+
refreshWriteTokenUrl: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface UploadShardsParams {
|
|
66
|
+
accessToken: string | undefined;
|
|
67
|
+
hubUrl: string;
|
|
68
|
+
xetParams: XetTokenParams;
|
|
69
|
+
fetch?: typeof fetch;
|
|
70
|
+
repo: RepoId;
|
|
71
|
+
rev: string;
|
|
72
|
+
isPullRequest?: boolean;
|
|
73
|
+
yieldCallback?: (event: { event: "fileProgress"; path: string; progress: number }) => void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Outputs the file sha256 after their xorbs/shards have been uploaded.
|
|
78
|
+
*/
|
|
79
|
+
export async function* uploadShards(
|
|
80
|
+
source: AsyncGenerator<{ content: Blob; path: string; sha256?: string }>,
|
|
81
|
+
params: UploadShardsParams,
|
|
82
|
+
): AsyncGenerator<
|
|
83
|
+
| {
|
|
84
|
+
event: "file";
|
|
85
|
+
path: string;
|
|
86
|
+
xetHash: string;
|
|
87
|
+
sha256: string | undefined;
|
|
88
|
+
dedupRatio: number;
|
|
89
|
+
}
|
|
90
|
+
| { event: "fileProgress"; path: string; progress: number }
|
|
91
|
+
> {
|
|
92
|
+
const xorbHashes: Array<string> = [];
|
|
93
|
+
const seenFileXetHashes = new Set<string>();
|
|
94
|
+
|
|
95
|
+
const fileInfoSection = new Uint8Array(Math.floor(SHARD_MAX_SIZE - SHARD_HEADER_SIZE - SHARD_FOOTER_SIZE) * 0.25);
|
|
96
|
+
const xorbInfoSection = new Uint8Array(Math.floor(SHARD_MAX_SIZE - SHARD_HEADER_SIZE - SHARD_FOOTER_SIZE) * 0.75);
|
|
97
|
+
|
|
98
|
+
const xorbView = new DataView(xorbInfoSection.buffer);
|
|
99
|
+
let xorbViewOffset = 0;
|
|
100
|
+
const fileInfoView = new DataView(fileInfoSection.buffer);
|
|
101
|
+
let fileViewOffset = 0;
|
|
102
|
+
let xorbTotalSize = 0n;
|
|
103
|
+
let fileTotalSize = 0n;
|
|
104
|
+
let xorbTotalUnpackedSize = 0n;
|
|
105
|
+
|
|
106
|
+
for await (const output of createXorbs(source, params)) {
|
|
107
|
+
switch (output.event) {
|
|
108
|
+
case "xorb": {
|
|
109
|
+
xorbHashes.push(output.hash);
|
|
110
|
+
|
|
111
|
+
// Calculate space needed for this xorb entry
|
|
112
|
+
const xorbEntrySize = HASH_LENGTH + 4 + 4 + 4 + 4; // hash + flags + count + unpacked + packed
|
|
113
|
+
const chunksSize = output.chunks.length * (HASH_LENGTH + 4 + 4 + 8); // per chunk: hash + length + offset + reserved
|
|
114
|
+
const totalXorbSize = xorbEntrySize + chunksSize;
|
|
115
|
+
|
|
116
|
+
// Check if adding this xorb would exceed buffer capacity
|
|
117
|
+
if (xorbViewOffset + totalXorbSize > xorbInfoSection.length) {
|
|
118
|
+
// Upload current shard and reset buffers
|
|
119
|
+
if (xorbViewOffset > 0 || fileViewOffset > 0) {
|
|
120
|
+
await uploadShard(createShard(), params);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// todo: handle when going out of bounds
|
|
125
|
+
writeHashToArray(output.hash, xorbInfoSection, xorbViewOffset);
|
|
126
|
+
xorbViewOffset += HASH_LENGTH;
|
|
127
|
+
xorbView.setUint32(xorbViewOffset, 0, true); // flags
|
|
128
|
+
xorbViewOffset += 4;
|
|
129
|
+
xorbView.setUint32(xorbViewOffset, output.chunks.length, true);
|
|
130
|
+
xorbViewOffset += 4;
|
|
131
|
+
const xorbUnpackedSize = sum(output.chunks.map((x) => x.length));
|
|
132
|
+
xorbView.setUint32(xorbViewOffset, xorbUnpackedSize, true);
|
|
133
|
+
xorbTotalUnpackedSize += BigInt(xorbUnpackedSize);
|
|
134
|
+
xorbTotalSize += BigInt(output.xorb.byteLength);
|
|
135
|
+
xorbViewOffset += 4;
|
|
136
|
+
xorbView.setUint32(xorbViewOffset, output.xorb.byteLength, true);
|
|
137
|
+
xorbViewOffset += 4;
|
|
138
|
+
|
|
139
|
+
let chunkBytes = 0;
|
|
140
|
+
for (const chunk of output.chunks) {
|
|
141
|
+
writeHashToArray(chunk.hash, xorbInfoSection, xorbViewOffset);
|
|
142
|
+
xorbViewOffset += HASH_LENGTH;
|
|
143
|
+
// start offset
|
|
144
|
+
xorbView.setUint32(xorbViewOffset, chunkBytes, true);
|
|
145
|
+
xorbViewOffset += 4;
|
|
146
|
+
// chunk length
|
|
147
|
+
xorbView.setUint32(xorbViewOffset, chunk.length, true);
|
|
148
|
+
xorbViewOffset += 4;
|
|
149
|
+
xorbView.setBigUint64(xorbViewOffset, 0n, true); // reserved
|
|
150
|
+
xorbViewOffset += 8;
|
|
151
|
+
chunkBytes += chunk.length;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const file of output.files) {
|
|
155
|
+
yield {
|
|
156
|
+
event: "fileProgress",
|
|
157
|
+
path: file.path,
|
|
158
|
+
progress: file.lastSentProgress,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await uploadXorb(output, params);
|
|
163
|
+
//^ Todo: queue it and do not await it
|
|
164
|
+
|
|
165
|
+
for (const file of output.files) {
|
|
166
|
+
yield { event: "fileProgress", path: file.path, progress: file.progress };
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
case "file": {
|
|
171
|
+
yield {
|
|
172
|
+
event: "file",
|
|
173
|
+
path: output.path,
|
|
174
|
+
xetHash: output.hash,
|
|
175
|
+
sha256: output.sha256,
|
|
176
|
+
dedupRatio: output.dedupRatio,
|
|
177
|
+
}; // Maybe wait until shard is uploaded before yielding.
|
|
178
|
+
|
|
179
|
+
if (seenFileXetHashes.has(output.hash)) {
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
seenFileXetHashes.add(output.hash);
|
|
183
|
+
|
|
184
|
+
// Calculate space needed for this file entry
|
|
185
|
+
const fileHeaderSize = HASH_LENGTH + 4 + 4 + 8; // hash + flags + rep length + reserved
|
|
186
|
+
const representationSize = output.representation.length * (HASH_LENGTH + 4 + 4 + 4 + 4); // per rep: xorb hash + flags + length + offset + endOffset
|
|
187
|
+
const verificationSize = output.representation.length * (HASH_LENGTH + 16); // per rep: range hash + reserved
|
|
188
|
+
const fileSha256 = output.sha256;
|
|
189
|
+
const hasMetadataExt = fileSha256 !== undefined;
|
|
190
|
+
const metadataSize = hasMetadataExt ? HASH_LENGTH + 16 : 0; // sha256 + reserved
|
|
191
|
+
const totalFileSize = fileHeaderSize + representationSize + verificationSize + metadataSize;
|
|
192
|
+
|
|
193
|
+
// Check if adding this file would exceed buffer capacity
|
|
194
|
+
if (fileViewOffset + totalFileSize > fileInfoSection.length) {
|
|
195
|
+
// Upload current shard and reset buffers
|
|
196
|
+
if (xorbViewOffset > 0 || fileViewOffset > 0) {
|
|
197
|
+
await uploadShard(createShard(), params);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
writeHashToArray(output.hash, fileInfoSection, fileViewOffset);
|
|
202
|
+
fileViewOffset += HASH_LENGTH;
|
|
203
|
+
// Cannot use | binary operator since it works with int32 not uint32 and one of the flags is 1 << 31
|
|
204
|
+
fileInfoView.setUint32(
|
|
205
|
+
fileViewOffset,
|
|
206
|
+
MDB_FILE_FLAG_WITH_VERIFICATION + (hasMetadataExt ? MDB_FILE_FLAG_WITH_METADATA_EXT : 0),
|
|
207
|
+
true,
|
|
208
|
+
);
|
|
209
|
+
fileViewOffset += 4;
|
|
210
|
+
fileInfoView.setUint32(fileViewOffset, output.representation.length, true);
|
|
211
|
+
fileViewOffset += 4;
|
|
212
|
+
fileInfoView.setBigUint64(fileViewOffset, 0n, true); // reserved
|
|
213
|
+
fileViewOffset += 8;
|
|
214
|
+
|
|
215
|
+
for (const repItem of output.representation) {
|
|
216
|
+
writeHashToArray(
|
|
217
|
+
typeof repItem.xorbId === "number" ? xorbHashes[repItem.xorbId] : repItem.xorbId,
|
|
218
|
+
fileInfoSection,
|
|
219
|
+
fileViewOffset,
|
|
220
|
+
);
|
|
221
|
+
fileViewOffset += HASH_LENGTH;
|
|
222
|
+
fileInfoView.setUint32(fileViewOffset, 0, true); // Xorb flags
|
|
223
|
+
fileViewOffset += 4;
|
|
224
|
+
fileInfoView.setUint32(fileViewOffset, repItem.length, true);
|
|
225
|
+
fileViewOffset += 4;
|
|
226
|
+
fileInfoView.setUint32(fileViewOffset, repItem.indexStart, true);
|
|
227
|
+
fileViewOffset += 4;
|
|
228
|
+
fileInfoView.setUint32(fileViewOffset, repItem.indexEnd, true);
|
|
229
|
+
fileViewOffset += 4;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// File verification data
|
|
233
|
+
for (const repItem of output.representation) {
|
|
234
|
+
writeHashToArray(repItem.rangeHash, fileInfoSection, fileViewOffset);
|
|
235
|
+
fileViewOffset += HASH_LENGTH;
|
|
236
|
+
// reserved in file verification data
|
|
237
|
+
for (let i = 0; i < 16; i++) {
|
|
238
|
+
fileInfoSection[fileViewOffset + i] = 0;
|
|
239
|
+
}
|
|
240
|
+
fileViewOffset += 16;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (hasMetadataExt) {
|
|
244
|
+
// File metadata ext
|
|
245
|
+
writeHashToArray(fileSha256, fileInfoSection, fileViewOffset);
|
|
246
|
+
fileViewOffset += HASH_LENGTH;
|
|
247
|
+
|
|
248
|
+
// reserved in file metadata ext
|
|
249
|
+
for (let i = 0; i < 16; i++) {
|
|
250
|
+
fileInfoSection[fileViewOffset + i] = 0;
|
|
251
|
+
}
|
|
252
|
+
fileViewOffset += 16;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function createShard(): Uint8Array {
|
|
261
|
+
const shard = new Uint8Array(
|
|
262
|
+
SHARD_HEADER_SIZE + SHARD_FOOTER_SIZE + xorbViewOffset + XORB_FOOTER_LENGTH + fileViewOffset + FILE_FOOTER_LENGTH,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const shardView = new DataView(shard.buffer);
|
|
266
|
+
let shardOffset = 0;
|
|
267
|
+
|
|
268
|
+
// Header
|
|
269
|
+
shard.set(SHARD_MAGIC_TAG, shardOffset);
|
|
270
|
+
shardOffset += SHARD_MAGIC_TAG.length;
|
|
271
|
+
|
|
272
|
+
shardView.setBigUint64(shardOffset, SHARD_HEADER_VERSION, true);
|
|
273
|
+
shardOffset += 8;
|
|
274
|
+
|
|
275
|
+
shardView.setBigUint64(shardOffset, BigInt(SHARD_FOOTER_SIZE), true);
|
|
276
|
+
shardOffset += 8;
|
|
277
|
+
|
|
278
|
+
// File Info Section
|
|
279
|
+
shard.set(fileInfoSection.slice(0, fileViewOffset), shardOffset);
|
|
280
|
+
shardOffset += fileViewOffset;
|
|
281
|
+
|
|
282
|
+
// File info bookend
|
|
283
|
+
for (let i = 0; i < 32; i++) {
|
|
284
|
+
shard[shardOffset + i] = 0xff;
|
|
285
|
+
}
|
|
286
|
+
shardOffset += 32;
|
|
287
|
+
for (let i = 0; i < 16; i++) {
|
|
288
|
+
shard[shardOffset + i] = 0;
|
|
289
|
+
}
|
|
290
|
+
shardOffset += 16;
|
|
291
|
+
|
|
292
|
+
// XORB Info Section
|
|
293
|
+
const xorbInfoOffset = shardOffset;
|
|
294
|
+
shard.set(xorbInfoSection.slice(0, xorbViewOffset), shardOffset);
|
|
295
|
+
shardOffset += xorbViewOffset;
|
|
296
|
+
|
|
297
|
+
// Xorb info bookend
|
|
298
|
+
for (let i = 0; i < 32; i++) {
|
|
299
|
+
shard[shardOffset + i] = 0xff;
|
|
300
|
+
}
|
|
301
|
+
shardOffset += 32;
|
|
302
|
+
for (let i = 0; i < 16; i++) {
|
|
303
|
+
shard[shardOffset + i] = 0;
|
|
304
|
+
}
|
|
305
|
+
shardOffset += 16;
|
|
306
|
+
|
|
307
|
+
// Footer
|
|
308
|
+
const footerOffset = shardOffset;
|
|
309
|
+
|
|
310
|
+
// version: u64, // Footer version (must be 1)
|
|
311
|
+
// file_info_offset: u64, // Offset to file info section
|
|
312
|
+
// cas_info_offset: u64, // Offset to CAS info section
|
|
313
|
+
// reserved 48 bytes
|
|
314
|
+
// chunk_hash_hmac_key: [u64; 4], // HMAC key for chunk hashes (32 bytes)
|
|
315
|
+
// shard_creation_timestamp: u64, // Creation time (seconds since epoch)
|
|
316
|
+
// shard_key_expiry: u64, // Expiry time (seconds since epoch)
|
|
317
|
+
// _buffer: [u64; 6], // Reserved space (48 bytes)
|
|
318
|
+
// stored_bytes_on_disk: u64, // Total bytes stored on disk
|
|
319
|
+
// materialized_bytes: u64, // Total materialized bytes
|
|
320
|
+
// stored_bytes: u64, // Total stored bytes
|
|
321
|
+
// footer_offset: u64,
|
|
322
|
+
|
|
323
|
+
shardView.setBigUint64(shardOffset, SHARD_FOOTER_VERSION, true);
|
|
324
|
+
shardOffset += 8;
|
|
325
|
+
shardView.setBigUint64(shardOffset, BigInt(SHARD_HEADER_SIZE), true); // beginning of fileinfo section
|
|
326
|
+
shardOffset += 8;
|
|
327
|
+
shardView.setBigUint64(shardOffset, BigInt(xorbInfoOffset), true); // beginning of xorbinfo section
|
|
328
|
+
shardOffset += 8;
|
|
329
|
+
|
|
330
|
+
for (let i = 0; i < 48; i++) {
|
|
331
|
+
shardView.setUint8(shardOffset + i, 0);
|
|
332
|
+
}
|
|
333
|
+
shardOffset += 48;
|
|
334
|
+
|
|
335
|
+
// Chunk HMAC
|
|
336
|
+
for (let i = 0; i < 32; i++) {
|
|
337
|
+
shardView.setUint8(shardOffset + i, 0);
|
|
338
|
+
}
|
|
339
|
+
shardOffset += 32;
|
|
340
|
+
|
|
341
|
+
shardView.setBigUint64(shardOffset, BigInt(Math.floor(Date.now() / 1000)), true);
|
|
342
|
+
shardOffset += 8;
|
|
343
|
+
|
|
344
|
+
// Shard key expiration
|
|
345
|
+
shardView.setBigUint64(shardOffset, 0n, true);
|
|
346
|
+
shardOffset += 8;
|
|
347
|
+
|
|
348
|
+
// Reserved space (48 bytes)
|
|
349
|
+
for (let i = 0; i < 48; i++) {
|
|
350
|
+
shardView.setUint8(shardOffset + i, 0);
|
|
351
|
+
}
|
|
352
|
+
shardOffset += 48;
|
|
353
|
+
|
|
354
|
+
shardView.setBigUint64(shardOffset, xorbTotalSize, true);
|
|
355
|
+
shardOffset += 8;
|
|
356
|
+
|
|
357
|
+
shardView.setBigUint64(shardOffset, fileTotalSize, true);
|
|
358
|
+
shardOffset += 8;
|
|
359
|
+
|
|
360
|
+
shardView.setBigUint64(shardOffset, xorbTotalUnpackedSize, true);
|
|
361
|
+
shardOffset += 8;
|
|
362
|
+
|
|
363
|
+
shardView.setBigUint64(shardOffset, BigInt(footerOffset), true);
|
|
364
|
+
|
|
365
|
+
xorbViewOffset = 0;
|
|
366
|
+
fileViewOffset = 0;
|
|
367
|
+
xorbTotalSize = 0n;
|
|
368
|
+
xorbTotalUnpackedSize = 0n;
|
|
369
|
+
fileTotalSize = 0n;
|
|
370
|
+
|
|
371
|
+
return shard;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// If un-uploaded data remains, upload it
|
|
375
|
+
if (xorbViewOffset || fileViewOffset) {
|
|
376
|
+
await uploadShard(createShard(), params);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Todo: switch from hex to non-hex when WASM switches. For now consider hash is hex
|
|
381
|
+
function writeHashToArray(hash: string, array: Uint8Array, offset: number) {
|
|
382
|
+
for (let i = 0; i < hash.length; i += 16) {
|
|
383
|
+
// Write a uint64 in little endian
|
|
384
|
+
array[offset + i / 2] = parseInt(hash.substring(i + 2 * 7, i + 2 * 8), 16);
|
|
385
|
+
array[offset + i / 2 + 1] = parseInt(hash.substring(i + 2 * 6, i + 2 * 7), 16);
|
|
386
|
+
array[offset + i / 2 + 2] = parseInt(hash.substring(i + 2 * 5, i + 2 * 6), 16);
|
|
387
|
+
array[offset + i / 2 + 3] = parseInt(hash.substring(i + 2 * 4, i + 2 * 5), 16);
|
|
388
|
+
array[offset + i / 2 + 4] = parseInt(hash.substring(i + 2 * 3, i + 2 * 4), 16);
|
|
389
|
+
array[offset + i / 2 + 5] = parseInt(hash.substring(i + 2 * 2, i + 2 * 3), 16);
|
|
390
|
+
array[offset + i / 2 + 6] = parseInt(hash.substring(i + 2 * 1, i + 2 * 2), 16);
|
|
391
|
+
array[offset + i / 2 + 7] = parseInt(hash.substring(i + 2 * 0, i + 2 * 1), 16);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function uploadXorb(
|
|
396
|
+
xorb: { hash: string; xorb: Uint8Array; files: Array<{ path: string; progress: number; lastSentProgress: number }> },
|
|
397
|
+
params: UploadShardsParams,
|
|
398
|
+
) {
|
|
399
|
+
const token = await xetWriteToken(params);
|
|
400
|
+
|
|
401
|
+
const resp = await (params.fetch ?? fetch)(`${token.casUrl}/v1/xorbs/default/${xorb.hash}`, {
|
|
402
|
+
method: "POST",
|
|
403
|
+
body: xorb.xorb,
|
|
404
|
+
headers: {
|
|
405
|
+
Authorization: `Bearer ${token.accessToken}`,
|
|
406
|
+
...(params.xetParams.sessionId ? { "X-Xet-Session-Id": params.xetParams.sessionId } : {}),
|
|
407
|
+
},
|
|
408
|
+
...{
|
|
409
|
+
progressHint: {
|
|
410
|
+
progressCallback: (progress: number) => {
|
|
411
|
+
for (const file of xorb.files) {
|
|
412
|
+
params.yieldCallback?.({
|
|
413
|
+
event: "fileProgress",
|
|
414
|
+
path: file.path,
|
|
415
|
+
progress: file.lastSentProgress + (file.progress - file.lastSentProgress) * progress,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
if (!resp.ok) {
|
|
424
|
+
throw await createApiError(resp);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function uploadShard(shard: Uint8Array, params: UploadShardsParams) {
|
|
429
|
+
const token = await xetWriteToken(params);
|
|
430
|
+
|
|
431
|
+
const resp = await (params.fetch ?? fetch)(`${token.casUrl}/v1/shards`, {
|
|
432
|
+
method: "POST",
|
|
433
|
+
body: shard,
|
|
434
|
+
headers: {
|
|
435
|
+
Authorization: `Bearer ${token.accessToken}`,
|
|
436
|
+
...(params.xetParams.sessionId ? { "X-Xet-Session-Id": params.xetParams.sessionId } : {}),
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
if (!resp.ok) {
|
|
441
|
+
throw await createApiError(resp);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// @ts-nocheck -- vendored upstream code, kept verbatim; our strict tsconfig does not apply.
|
|
2
|
+
// Vendored from huggingface/huggingface.js@f8fdf6be (packages/hub/src/utils/xetWriteToken.ts), MIT License.
|
|
3
|
+
// Delete this directory when bucket support is upstreamed to @huggingface/hub.
|
|
4
|
+
import { createApiError } from "../error";
|
|
5
|
+
import type { XetTokenParams } from "./uploadShards";
|
|
6
|
+
|
|
7
|
+
export interface XetWriteTokenParams {
|
|
8
|
+
accessToken: string | undefined;
|
|
9
|
+
fetch?: typeof fetch;
|
|
10
|
+
xetParams: XetTokenParams;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const JWT_SAFETY_PERIOD = 60_000;
|
|
14
|
+
const JWT_CACHE_SIZE = 1_000;
|
|
15
|
+
|
|
16
|
+
const jwtPromises: Map<string, Promise<{ accessToken: string; casUrl: string }>> = new Map();
|
|
17
|
+
/**
|
|
18
|
+
* Cache to store JWTs, to avoid making many auth requests when downloading multiple files from the same repo
|
|
19
|
+
*/
|
|
20
|
+
const jwts: Map<
|
|
21
|
+
string,
|
|
22
|
+
{
|
|
23
|
+
accessToken: string;
|
|
24
|
+
expiresAt: Date;
|
|
25
|
+
casUrl: string;
|
|
26
|
+
}
|
|
27
|
+
> = new Map();
|
|
28
|
+
|
|
29
|
+
export async function xetWriteToken(params: XetWriteTokenParams): Promise<{ accessToken: string; casUrl: string }> {
|
|
30
|
+
if (
|
|
31
|
+
params.xetParams.expiresAt &&
|
|
32
|
+
params.xetParams.casUrl &&
|
|
33
|
+
params.xetParams.accessToken &&
|
|
34
|
+
params.xetParams.expiresAt > new Date(Date.now() + JWT_SAFETY_PERIOD)
|
|
35
|
+
) {
|
|
36
|
+
return { accessToken: params.xetParams.accessToken, casUrl: params.xetParams.casUrl };
|
|
37
|
+
}
|
|
38
|
+
const key = params.xetParams.refreshWriteTokenUrl;
|
|
39
|
+
|
|
40
|
+
const jwt = jwts.get(key);
|
|
41
|
+
|
|
42
|
+
if (jwt && jwt.expiresAt > new Date(Date.now() + JWT_SAFETY_PERIOD)) {
|
|
43
|
+
return { accessToken: jwt.accessToken, casUrl: jwt.casUrl };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// If we already have a promise for this repo, return it
|
|
47
|
+
const existingPromise = jwtPromises.get(key);
|
|
48
|
+
if (existingPromise) {
|
|
49
|
+
return existingPromise;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const promise = (async () => {
|
|
53
|
+
const resp = await (params.fetch ?? fetch)(params.xetParams.refreshWriteTokenUrl, {
|
|
54
|
+
headers: {
|
|
55
|
+
...(params.accessToken
|
|
56
|
+
? {
|
|
57
|
+
Authorization: `Bearer ${params.accessToken}`,
|
|
58
|
+
}
|
|
59
|
+
: {}),
|
|
60
|
+
...(params.xetParams.sessionId ? { "X-Xet-Session-Id": params.xetParams.sessionId } : {}),
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!resp.ok) {
|
|
65
|
+
throw await createApiError(resp);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const json: { accessToken: string; casUrl: string; exp: number } = await resp.json();
|
|
69
|
+
const jwt = {
|
|
70
|
+
accessToken: json.accessToken,
|
|
71
|
+
expiresAt: new Date(json.exp * 1000),
|
|
72
|
+
casUrl: json.casUrl,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
jwtPromises.delete(key);
|
|
76
|
+
|
|
77
|
+
for (const [key, value] of jwts.entries()) {
|
|
78
|
+
if (value.expiresAt < new Date(Date.now() + JWT_SAFETY_PERIOD)) {
|
|
79
|
+
jwts.delete(key);
|
|
80
|
+
} else {
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (jwts.size >= JWT_CACHE_SIZE) {
|
|
85
|
+
const keyToDelete = jwts.keys().next().value;
|
|
86
|
+
if (keyToDelete) {
|
|
87
|
+
jwts.delete(keyToDelete);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
jwts.set(key, jwt);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
accessToken: json.accessToken,
|
|
94
|
+
casUrl: json.casUrl,
|
|
95
|
+
};
|
|
96
|
+
})();
|
|
97
|
+
|
|
98
|
+
jwtPromises.set(key, promise);
|
|
99
|
+
|
|
100
|
+
return promise;
|
|
101
|
+
}
|