@filefreak.io/sdk 0.4.0 → 0.6.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/README.md CHANGED
@@ -5,7 +5,8 @@ later and in browsers. No dependencies.
5
5
 
6
6
  Every endpoint is a function, typed from the same table the API is built
7
7
  from. Around them: uploads sent straight to storage in parts and carried on
8
- after an interruption, downloads fetched in ranges, listings and changes read
8
+ after an interruption, a new version of a large file sending only what
9
+ changed, downloads fetched in ranges, listings and changes read
9
10
  to the end, files under Shard Protection sealed as they are sent and read
10
11
  through their maps, what a share link holds saved with no key, and requests sent again only when that
11
12
  cannot make anything twice. The reference is at
@@ -33,7 +34,9 @@ const bytes = await ff.bytes(file.id); // for a file small enough to hold in mem
33
34
  than memory is fine. `toPath` writes beside the file and gives it its name
34
35
  only once every byte is there: a download that fails leaves nothing behind.
35
36
  A file under Shard Protection has no address, and `download` and `bytes`
36
- read it through its map instead, each fragment checked as it arrives.
37
+ read it through its map instead, each fragment checked as it arrives. One
38
+ written in version 2 of the shard format, in blocks cut by its content, is
39
+ read the same way, its outline checked before any block is fetched.
37
40
  Into a folder under Shard Protection, `upload` seals each part with the
38
41
  file's own key before it is sent, so none of the file is ever plain in
39
42
  storage. The key is asked of the API for the upload and kept nowhere, and
@@ -41,6 +44,28 @@ an upload carried on asks which fragments storage has before it sends any.
41
44
 
42
45
  Keep keys on a server. A key sent from a web page is refused.
43
46
 
47
+ ## A new version of a large file
48
+
49
+ A file of 16 MiB or more is read through before it is sent and cut where
50
+ its content says, and each part is one of its blocks. A new version of
51
+ it, sent with `onConflict: "replace"`, sends only the blocks the file does
52
+ not have already: those it has are put together with the new ones inside
53
+ storage. The SDK and the CLI cut a file alike, so the next version of a file
54
+ either one sent keeps its blocks whichever sends it.
55
+
56
+ ```ts
57
+ await ff.upload(await fromPath("disk.img"), { parentId: folder.id, onConflict: "replace" });
58
+ ```
59
+
60
+ Into a folder under Shard Protection the same holds, with every block
61
+ encrypted before it leaves. A protected file of more than 16 MiB is cut by
62
+ its own key, so that nobody without the key can cut a copy of it the same
63
+ way and compare, and its next version keeps the blocks the last one has
64
+ when the key may read files; a key that may only write starts a new file
65
+ key and sends every block. Each block is read again as it is sent and
66
+ checked against what the upload began with, so a file changed while it is
67
+ sent stops the upload (`source_changed`) rather than storing a mix of two.
68
+
44
69
  ## In a web page
45
70
 
46
71
  A page is given a token by your server instead. Your server makes it with its
@@ -188,7 +213,9 @@ await ff.files.delete(file.id, { ifRevision: file.revision });
188
213
 
189
214
  What reached storage is kept until the upload expires. Keep the upload's id
190
215
  from `onBegun`, and carry on with the same file: only the parts storage does
191
- not have are sent.
216
+ not have are sent. A file cut by its content is carried on block by block,
217
+ each checked against what the upload began with, so carrying on with another
218
+ file is refused at its first block.
192
219
 
193
220
  ```ts
194
221
  import { UploadStopped } from "@filefreak.io/sdk";
package/dist/client.js CHANGED
@@ -6,6 +6,7 @@ import { bind, OPERATIONS, } from "./generated.js";
6
6
  import { sendFrame } from "./frames.js";
7
7
  import { fetchRanges } from "./ranges.js";
8
8
  import { asItIsKept, readProtected } from "./reader.js";
9
+ import { READS_FORMATS } from "./shards.js";
9
10
  import { FileFreakError, sleep, Transport } from "./transport.js";
10
11
  import { upload } from "./uploads.js";
11
12
  /** How long a follow of the changes waits for one before asking again, unless told. */
@@ -139,7 +140,7 @@ export class FileFreak {
139
140
  const { url } = await this.files.downloadUrl(item.id, {}, { signal });
140
141
  await fetchRanges({ url, sizeBytes: item.sizeBytes, sink, signal, progress, ranges: options.ranges, fetch: this.transport.fetch, sleep: (ms) => sleep(ms, signal) });
141
142
  }, () => readProtected({
142
- pages: (from, pageSignal) => this.files.shards(item.id, { from }, { signal: pageSignal }),
143
+ pages: (from, pageSignal) => this.files.shards(item.id, { from, formats: READS_FORMATS }, { signal: pageSignal }),
143
144
  sink,
144
145
  sizeBytes: item.sizeBytes,
145
146
  signal,
package/dist/cut.d.ts ADDED
@@ -0,0 +1,96 @@
1
+ import { type FileKeys, type OutlineBlock } from "./shards.js";
2
+ /** How a file is cut. One rule for every file, so that a file cut once is cut the same way next time, which is what lets its blocks be found again. */
3
+ export interface Rule {
4
+ /** A smaller plain file is not cut: it goes in one or two parts, and there is nothing to gain. */
5
+ cutFrom: number;
6
+ /** FastCDC's sizes. No piece but a file's last is shorter than least, and none is longer than most. */
7
+ least: number;
8
+ average: number;
9
+ most: number;
10
+ /** FastCDC's patterns at level 1: the stricter until a piece reaches the average, the looser after. Within the low 48 bits. */
11
+ strict: bigint;
12
+ loose: bigint;
13
+ /** Past this size pieces are put together in runs, about four to a run, so that a file of 150 GB is still within the API's 10,000 blocks. */
14
+ groupFrom: number;
15
+ /** A run ends once it is this long, so that one more piece, 16 MiB at most, leaves it within the API's 64 MiB. */
16
+ groupMost: number;
17
+ /** The most blocks the API takes. */
18
+ mostBlocks: number;
19
+ /** How much of a Blob is read at a time. */
20
+ readBytes: number;
21
+ }
22
+ export declare const RULE: Rule;
23
+ /** FastCDC's 256 numbers: whole, for the hash at a cut, and their low 48 bits in halves of 24, for the hash as the bytes go by. */
24
+ export declare class CutTable {
25
+ readonly values: BigUint64Array;
26
+ readonly low: Int32Array<ArrayBuffer>;
27
+ readonly high: Int32Array<ArrayBuffer>;
28
+ constructor(values: BigUint64Array);
29
+ }
30
+ /**
31
+ * The fastcdc crate's own table, which a plain file is cut with: for each
32
+ * value, the first eight bytes, big-endian, of the MD5 of 64 bytes of it
33
+ * (the crate's examples/table64.rs, after the C implementation it follows).
34
+ * Made here rather than copied in.
35
+ */
36
+ export declare function publicTable(): CutTable;
37
+ /** A protected file's own table: 2,048 bytes of HKDF-SHA256 from its key, with no salt and the info "filefreak-shard-v2 cut", as 256 big-endian numbers. */
38
+ export declare function keyedTable(raw: Uint8Array): Promise<CutTable>;
39
+ /** A piece of a file: how many bytes, and the hash it was cut at. */
40
+ export interface Piece {
41
+ length: number;
42
+ hash: bigint;
43
+ }
44
+ /**
45
+ * The piece `data` begins with, where `data` runs to the end of the largest
46
+ * piece or of the file, whichever comes first. As the crate cuts: the hash
47
+ * starts `least` bytes in; places are tested two at a time, so the last byte
48
+ * of a piece cut short at an odd length is hashed and never tested; and a
49
+ * piece cut at a match ends before the byte that matched.
50
+ */
51
+ export declare function piece(data: Uint8Array, table: CutTable, rule?: Rule): Piece;
52
+ /**
53
+ * Whether a piece cut at `hash` ends a run, in a file past groupFrom: one in
54
+ * four, decided by the bytes where it was cut, so that a run ends at the
55
+ * same content wherever it moves to. Mixed first, because a hash at a cut
56
+ * matches a pattern and some of its bits are always zero.
57
+ */
58
+ export declare function endsRun(hash: bigint): boolean;
59
+ /** What is cut: its bytes, or a way to read them a window at a time. */
60
+ export type Bytes = Uint8Array | {
61
+ size: number;
62
+ read(from: number, to: number): Promise<Uint8Array>;
63
+ };
64
+ /** A block of a file: how many bytes, and what names it. */
65
+ export interface Named<T> {
66
+ length: number;
67
+ name: T;
68
+ }
69
+ /**
70
+ * A file cut into blocks with the table given: a piece to a block, or in a
71
+ * file past groupFrom, runs of pieces. `name` is given each block's bytes in
72
+ * order. Null as soon as there are more blocks than the API takes, without
73
+ * reading the rest. What is read is kept only while a piece or an open run
74
+ * still needs it.
75
+ */
76
+ export declare function cut<T>(bytes: Bytes, table: CutTable, name: (block: Uint8Array) => Promise<T>, rule?: Rule, signal?: AbortSignal): Promise<Array<Named<T>> | null>;
77
+ /** A plain block as beginning an upload takes it. */
78
+ export interface PlainBlock {
79
+ size: number;
80
+ /** In lower-case hex. */
81
+ sha256: string;
82
+ }
83
+ /**
84
+ * A plain file cut for beginning its upload, each block its size and
85
+ * SHA-256. Null for a file too small to be worth cutting, or cut into more
86
+ * blocks than the API takes: either is sent in parts of one size.
87
+ */
88
+ export declare function cutPlain(bytes: Bytes, rule?: Rule, signal?: AbortSignal): Promise<PlainBlock[] | null>;
89
+ /**
90
+ * A protected file cut for version 2 of the shard format, with its own key's
91
+ * table, each block its id and length, as uploads.blocks takes them and the
92
+ * outline holds them. Null for a file not cut into two to ten thousand
93
+ * blocks, which version 2 does not take: it is written in version 1.
94
+ */
95
+ export declare function cutSealed(bytes: Bytes, raw: Uint8Array, keys: FileKeys, rule?: Rule, signal?: AbortSignal): Promise<OutlineBlock[] | null>;
96
+ export declare function hex(bytes: Uint8Array): string;
package/dist/cut.js ADDED
@@ -0,0 +1,220 @@
1
+ // Cutting a file by its content, so that its next version sends only what
2
+ // changed. The rule is the CLI's (cli/crates/transfer/src/blocks.rs), so
3
+ // that of a file either one sent, the other's next version keeps the blocks.
4
+ //
5
+ // Where a piece ends is decided by the bytes there: FastCDC, as the fastcdc
6
+ // crate runs it (version 5, its 2020 form, normalised at level 1), hashes the
7
+ // bytes as it reads and cuts where the hash matches a pattern. A byte put in
8
+ // near the start of a file moves the end of the piece it falls in and leaves
9
+ // every later piece as it was, where parts of one size would all move. A
10
+ // plain file is cut with the crate's own table of numbers and each block
11
+ // named by its SHA-256, as beginning an upload takes them. A file under Shard
12
+ // Protection, written in version 2 of the shard format, is cut with a table
13
+ // made from the file's own key and each block named by its id
14
+ // (docs/reference/shard-format.md, "Cutting a file"): with the public table,
15
+ // whoever has the database could cut a file they hold and compare the
16
+ // lengths. docs/reference/shard-format-v2.json holds both to the crate.
17
+ //
18
+ // The hash runs to 64 bits, more than a JavaScript number holds exactly. The
19
+ // patterns look only at its low 48 bits, which nothing above them changes, so
20
+ // only those are kept as the bytes go by, as two numbers of 24 bits. The
21
+ // whole hash is wanted only at a cut, where it says whether a run ends in a
22
+ // file past 32 GiB, and it depends only on the last 64 bytes hashed: it is
23
+ // worked out again from those, as a BigInt.
24
+ import { Md5 } from "./md5.js";
25
+ import { blockId } from "./shards.js";
26
+ const MIB = 1024 * 1024;
27
+ const HALF = 0xff_ffff;
28
+ export const RULE = {
29
+ cutFrom: 16 * MIB,
30
+ least: MIB,
31
+ average: 4 * MIB,
32
+ most: 16 * MIB,
33
+ strict: 0x0000d93777537000n,
34
+ loose: 0x0000d91767537000n,
35
+ groupFrom: 32 * 1024 * MIB,
36
+ groupMost: 48 * MIB,
37
+ mostBlocks: 10_000,
38
+ readBytes: 32 * MIB,
39
+ };
40
+ /** FastCDC's 256 numbers: whole, for the hash at a cut, and their low 48 bits in halves of 24, for the hash as the bytes go by. */
41
+ export class CutTable {
42
+ values;
43
+ low = new Int32Array(256);
44
+ high = new Int32Array(256);
45
+ constructor(values) {
46
+ this.values = values;
47
+ if (values.length !== 256)
48
+ throw new RangeError(`A cutting table has 256 numbers, not ${values.length}.`);
49
+ for (let i = 0; i < 256; i++) {
50
+ this.low[i] = Number(values[i] & BigInt(HALF));
51
+ this.high[i] = Number((values[i] >> 24n) & BigInt(HALF));
52
+ }
53
+ }
54
+ }
55
+ let crateTable = null;
56
+ /**
57
+ * The fastcdc crate's own table, which a plain file is cut with: for each
58
+ * value, the first eight bytes, big-endian, of the MD5 of 64 bytes of it
59
+ * (the crate's examples/table64.rs, after the C implementation it follows).
60
+ * Made here rather than copied in.
61
+ */
62
+ export function publicTable() {
63
+ if (!crateTable) {
64
+ const values = new BigUint64Array(256);
65
+ for (let i = 0; i < 256; i++) {
66
+ const digest = new Md5().update(new Uint8Array(64).fill(i)).digest();
67
+ values[i] = new DataView(digest.buffer, digest.byteOffset, 8).getBigUint64(0, false);
68
+ }
69
+ crateTable = new CutTable(values);
70
+ }
71
+ return crateTable;
72
+ }
73
+ /** A protected file's own table: 2,048 bytes of HKDF-SHA256 from its key, with no salt and the info "filefreak-shard-v2 cut", as 256 big-endian numbers. */
74
+ export async function keyedTable(raw) {
75
+ if (raw.length !== 32)
76
+ throw new RangeError(`A shard key is 32 bytes, not ${raw.length}.`);
77
+ const base = await crypto.subtle.importKey("raw", new Uint8Array(raw), "HKDF", false, ["deriveBits"]);
78
+ const bits = await crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(0), info: new TextEncoder().encode("filefreak-shard-v2 cut") }, base, 2048 * 8);
79
+ const view = new DataView(bits);
80
+ const values = new BigUint64Array(256);
81
+ for (let i = 0; i < 256; i++)
82
+ values[i] = view.getBigUint64(i * 8, false);
83
+ return new CutTable(values);
84
+ }
85
+ /** A pattern as two halves of 24 bits, refused if it looks above the 48 bits the hash is kept to. */
86
+ function halves(pattern) {
87
+ if (pattern >> 48n !== 0n)
88
+ throw new RangeError(`A cutting pattern looks only at the low 48 bits of the hash, and ${pattern.toString(16)} does not.`);
89
+ return [Number(pattern & BigInt(HALF)), Number(pattern >> 24n)];
90
+ }
91
+ /**
92
+ * The piece `data` begins with, where `data` runs to the end of the largest
93
+ * piece or of the file, whichever comes first. As the crate cuts: the hash
94
+ * starts `least` bytes in; places are tested two at a time, so the last byte
95
+ * of a piece cut short at an odd length is hashed and never tested; and a
96
+ * piece cut at a match ends before the byte that matched.
97
+ */
98
+ export function piece(data, table, rule = RULE) {
99
+ const left = data.length;
100
+ if (left <= rule.least)
101
+ return { length: left, hash: 0n };
102
+ const end = Math.min(left, rule.most);
103
+ const strictTo = 2 * Math.floor(Math.min(rule.average, end) / 2);
104
+ const testedTo = 2 * Math.floor(end / 2);
105
+ const low = table.low;
106
+ const high = table.high;
107
+ let [maskLow, maskHigh] = halves(rule.strict);
108
+ const [looseLow, looseHigh] = halves(rule.loose);
109
+ let lo = 0;
110
+ let hi = 0;
111
+ for (let i = rule.least; i < testedTo; i++) {
112
+ if (i === strictTo) {
113
+ maskLow = looseLow;
114
+ maskHigh = looseHigh;
115
+ }
116
+ const b = data[i];
117
+ const sum = 2 * lo + low[b];
118
+ hi = (2 * hi + high[b] + (sum >>> 24)) & HALF;
119
+ lo = sum & HALF;
120
+ if ((lo & maskLow) === 0 && (hi & maskHigh) === 0)
121
+ return { length: i, hash: hashAt(data, i, table, rule, i % 2 === 0) };
122
+ }
123
+ return { length: end, hash: hashAt(data, end - 1, table, rule, false) };
124
+ }
125
+ /**
126
+ * The whole hash once byte `i` is hashed, from the bytes that still count in
127
+ * it: those from `least` on, and of them only the last 64, since each byte
128
+ * doubles it and 64 doublings leave nothing of an earlier one. Doubled again
129
+ * for a match at an even place, as the crate gives it.
130
+ */
131
+ function hashAt(data, i, table, rule, doubled) {
132
+ let h = 0n;
133
+ for (let j = Math.max(rule.least, i - 63); j <= i; j++)
134
+ h = BigInt.asUintN(64, (h << 1n) + table.values[data[j]]);
135
+ return doubled ? BigInt.asUintN(64, h << 1n) : h;
136
+ }
137
+ /**
138
+ * Whether a piece cut at `hash` ends a run, in a file past groupFrom: one in
139
+ * four, decided by the bytes where it was cut, so that a run ends at the
140
+ * same content wherever it moves to. Mixed first, because a hash at a cut
141
+ * matches a pattern and some of its bits are always zero.
142
+ */
143
+ export function endsRun(hash) {
144
+ return BigInt.asUintN(64, hash * 0x9e3779b97f4a7c15n) >> 62n === 0n;
145
+ }
146
+ /**
147
+ * A file cut into blocks with the table given: a piece to a block, or in a
148
+ * file past groupFrom, runs of pieces. `name` is given each block's bytes in
149
+ * order. Null as soon as there are more blocks than the API takes, without
150
+ * reading the rest. What is read is kept only while a piece or an open run
151
+ * still needs it.
152
+ */
153
+ export async function cut(bytes, table, name, rule = RULE, signal) {
154
+ const reader = bytes instanceof Uint8Array ? null : bytes;
155
+ const size = sizeOf(bytes);
156
+ let buffer = bytes instanceof Uint8Array ? bytes : new Uint8Array(0);
157
+ let bufferAt = 0;
158
+ const grouping = size > rule.groupFrom;
159
+ const blocks = [];
160
+ let runAt = null;
161
+ for (let at = 0; at < size;) {
162
+ signal?.throwIfAborted();
163
+ const want = Math.min(size, at + rule.most);
164
+ if (reader && bufferAt + buffer.length < want) {
165
+ const keep = runAt ?? at;
166
+ const had = bufferAt + buffer.length;
167
+ const upTo = Math.min(size, Math.max(want, had + rule.readBytes));
168
+ const more = await reader.read(had, upTo);
169
+ if (more.length !== upTo - had)
170
+ throw new RangeError(`Reading bytes ${had} to ${upTo} gave ${more.length}: the file has changed since it was opened.`);
171
+ const joined = new Uint8Array(had - keep + more.length);
172
+ joined.set(buffer.subarray(keep - bufferAt));
173
+ joined.set(more, had - keep);
174
+ buffer = joined;
175
+ bufferAt = keep;
176
+ }
177
+ const { length, hash } = piece(buffer.subarray(at - bufferAt, want - bufferAt), table, rule);
178
+ const end = at + length;
179
+ runAt ??= at;
180
+ if (!grouping || end - runAt >= rule.groupMost || endsRun(hash) || end === size) {
181
+ blocks.push({ length: end - runAt, name: await name(buffer.subarray(runAt - bufferAt, end - bufferAt)) });
182
+ if (blocks.length > rule.mostBlocks)
183
+ return null;
184
+ runAt = null;
185
+ }
186
+ at = end;
187
+ }
188
+ return blocks;
189
+ }
190
+ /**
191
+ * A plain file cut for beginning its upload, each block its size and
192
+ * SHA-256. Null for a file too small to be worth cutting, or cut into more
193
+ * blocks than the API takes: either is sent in parts of one size.
194
+ */
195
+ export async function cutPlain(bytes, rule = RULE, signal) {
196
+ if (sizeOf(bytes) < rule.cutFrom)
197
+ return null;
198
+ const blocks = await cut(bytes, publicTable(), async (block) => hex(new Uint8Array(await crypto.subtle.digest("SHA-256", own(block)))), rule, signal);
199
+ return blocks && blocks.map((b) => ({ size: b.length, sha256: b.name }));
200
+ }
201
+ /**
202
+ * A protected file cut for version 2 of the shard format, with its own key's
203
+ * table, each block its id and length, as uploads.blocks takes them and the
204
+ * outline holds them. Null for a file not cut into two to ten thousand
205
+ * blocks, which version 2 does not take: it is written in version 1.
206
+ */
207
+ export async function cutSealed(bytes, raw, keys, rule = RULE, signal) {
208
+ const blocks = await cut(bytes, await keyedTable(raw), (block) => blockId(keys, block), rule, signal);
209
+ return blocks && blocks.length >= 2 ? blocks.map((b) => ({ id: b.name, length: b.length })) : null;
210
+ }
211
+ function sizeOf(bytes) {
212
+ return bytes instanceof Uint8Array ? bytes.length : bytes.size;
213
+ }
214
+ /** The bytes as WebCrypto's types take them, copied only when they are not in an ArrayBuffer of their own (a SharedArrayBuffer's). */
215
+ function own(bytes) {
216
+ return bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
217
+ }
218
+ export function hex(bytes) {
219
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
220
+ }
@@ -157,11 +157,13 @@ export interface SendTo {
157
157
  }
158
158
  /** A page of a protected file's map: its key, and where each fragment is. */
159
159
  export interface ShardMap {
160
- /** The content's own id, which each fragment's check is bound to with its place and the count. */
160
+ /** The content's own id, which each fragment's check is bound to with its place and the count, and in version 2 the outline. */
161
161
  contentId: string;
162
+ /** The version of the shard format the file is written in: 1, or 2, in which the file is cut into blocks by its content and its next version keeps the blocks that did not change. Only a version named in formats is answered. */
163
+ format: number;
162
164
  /** The file's size. */
163
165
  sizeBytes: number;
164
- /** How many bytes of the file each fragment holds, except the last, which holds what remains. */
166
+ /** How many bytes of the file each fragment holds, except the last, which holds what remains. In version 2, the largest block's: each block's own length is in the outline. */
165
167
  fragmentBytes: number;
166
168
  /** How many fragments the file is. */
167
169
  count: number;
@@ -171,11 +173,23 @@ export interface ShardMap {
171
173
  contentType: string | null;
172
174
  /** The file's revision whose content this is. Its fragments never change, however late they are read. */
173
175
  revision: number;
176
+ /** Version 2, on the first page: every block of the file and the MAC over them. Null on a later page, and in version 1. */
177
+ outline: {
178
+ /** The MAC over the content id, the size and every block's id and length, in base64. Check it with the file's key, as the SDK's checkOutline does, before using a byte of the file. */
179
+ mac: string;
180
+ /** Every block of the file, in order of place. */
181
+ blocks: Array<{
182
+ /** The block's id in base64: 32 bytes, the HMAC of its bytes under the file's key. Each block is checked against it once opened. */
183
+ id: string;
184
+ /** How many bytes of the file the block holds. */
185
+ length: number;
186
+ }>;
187
+ } | null;
174
188
  /** The first place on this page. */
175
189
  from: number;
176
190
  /** Up to 1000, in order of place. */
177
191
  fragments: Array<{
178
- /** Counted from zero. Its bytes of the file begin at place times fragmentBytes. */
192
+ /** Counted from zero. In version 1 its bytes of the file begin at place times fragmentBytes; in version 2 after the blocks before it in the outline. */
179
193
  place: number;
180
194
  /** Where to fetch the stored fragment, with a plain GET and no key: the address is the permission. */
181
195
  url: string;
@@ -314,6 +328,8 @@ export type UploadsBeginBody = {
314
328
  }>;
315
329
  /** true when this program can seal fragments as it sends them, as the SDK and the CLI do. Into a protected place the answer's shards is then set, and each part is a fragment. */
316
330
  protectable?: boolean;
331
+ /** With protectable: the versions of the shard format this program can write. Left out, 1 alone. With 2, a sealed file of more than 16 MiB is written in version 2. */
332
+ shardFormats?: Array<number>;
317
333
  };
318
334
  /** What POST /v1/uploads answers with. */
319
335
  export type UploadsBeginAnswer = {
@@ -324,11 +340,13 @@ export type UploadsBeginAnswer = {
324
340
  reused: Array<number>;
325
341
  /** Set when the upload is sealed as it is sent: how the file is cut into fragments. The key to seal them with is given by uploads.get. Null for an upload sent as it is. */
326
342
  shards: {
327
- /** The content's own id, which each fragment's seal is bound to with its place and the count. */
343
+ /** The version of the shard format to write it in: 1, or 2 when shardFormats names it and the file is large enough. */
344
+ format: number;
345
+ /** The content's own id, which each fragment's seal is bound to with its place and the count, and in version 2 the outline. */
328
346
  contentId: string;
329
- /** How many bytes of the file each fragment holds, except the last, which holds what remains. */
347
+ /** How many bytes of the file each fragment holds, except the last, which holds what remains. In version 2, 0: the blocks are declared with uploads.blocks. */
330
348
  fragmentBytes: number;
331
- /** How many fragments, one to each part. */
349
+ /** How many fragments, one to each part. In version 2, 0 until the blocks are declared. */
332
350
  count: number;
333
351
  } | null;
334
352
  };
@@ -346,16 +364,42 @@ export type UploadsGetAnswer = {
346
364
  reused: Array<number>;
347
365
  /** For an upload sealed as it is sent: how the file is cut, and the key to seal each fragment with. Null for an upload sent as it is, and for one that has ended. */
348
366
  shards: {
349
- /** The content's own id, which each fragment's seal is bound to with its place and the count. */
367
+ /** The version of the shard format to write it in. */
368
+ format: number;
369
+ /** The content's own id, which each fragment's seal is bound to with its place and the count, and in version 2 the outline. */
350
370
  contentId: string;
351
371
  /** The file's own key, in base64: 32 bytes, which open its fragments. In this answer and nowhere else. */
352
372
  key: string;
353
- /** How many bytes of the file each fragment holds, except the last, which holds what remains. */
373
+ /** How many bytes of the file each fragment holds, except the last, which holds what remains. In version 2, the largest block's once they are declared, and 0 before. */
354
374
  fragmentBytes: number;
355
- /** How many fragments, one to each part. */
375
+ /** How many fragments, one to each part. In version 2, how many blocks once they are declared, and 0 before. */
356
376
  count: number;
377
+ /** Version 2: the blocks as declared, in order, or null until they are. Null in version 1. */
378
+ blocks: Array<{
379
+ size: number;
380
+ /** Its id, in base64. */
381
+ id: string;
382
+ }> | null;
357
383
  } | null;
358
384
  };
385
+ /** What POST /v1/uploads/:id/blocks takes as its body. */
386
+ export type UploadsBlocksBody = {
387
+ /** Every block of the file, in order. */
388
+ blocks: Array<{
389
+ /** How many bytes of the file the block holds. */
390
+ size: number;
391
+ /** The block's id, in base64: 32 bytes. */
392
+ id: string;
393
+ }>;
394
+ };
395
+ /** What POST /v1/uploads/:id/blocks answers with. */
396
+ export type UploadsBlocksAnswer = {
397
+ upload: Upload;
398
+ /** The places whose block the file has already, under its key. Never sent, and counted as arrived. */
399
+ reused: Array<number>;
400
+ /** Addresses for the first blocks to send, each at its place. Places in reused are not among them. */
401
+ parts: Array<Part>;
402
+ };
359
403
  /** What GET /v1/uploads/:id/parts takes in its query. */
360
404
  export type UploadsPartsQuery = {
361
405
  /** The first part wanted, counted from zero. */
@@ -384,6 +428,8 @@ export type UploadsFragmentsAnswer = {
384
428
  export type UploadsCompleteBody = {
385
429
  /** The file's SHA-256 in hex, as you worked it out. Kept with the file as your word, since this service never sees the bytes, and given back as clientSha256, so a copy can be checked later without reading the file again. */
386
430
  sha256?: string;
431
+ /** For an upload written in version 2 of the shard format, and required for one: the outline's MAC in base64, as the SDK's outlineMac makes it from the content id, the size and the declared blocks. Checked with the file's key. */
432
+ outline?: string;
387
433
  };
388
434
  /** What POST /v1/uploads/:id/complete answers with. */
389
435
  export type UploadsCompleteAnswer = {
@@ -417,6 +463,8 @@ export type FilesDownloadUrlAnswer = {
417
463
  export type FilesShardsQuery = {
418
464
  /** The first fragment wanted, counted from zero: 0, or the `next` of the page before. */
419
465
  from?: number;
466
+ /** The versions of the shard format this program reads, as 1,2. Left out, 1 alone. */
467
+ formats?: string;
420
468
  };
421
469
  /** What GET /v1/files/:id/shards answers with. */
422
470
  export type FilesShardsAnswer = ShardMap;
@@ -689,6 +737,12 @@ export declare const OPERATIONS: {
689
737
  readonly idempotent: false;
690
738
  readonly repeatable: false;
691
739
  };
740
+ readonly "uploads.blocks": {
741
+ readonly method: "POST";
742
+ readonly path: "/v1/uploads/:id/blocks";
743
+ readonly idempotent: false;
744
+ readonly repeatable: true;
745
+ };
692
746
  readonly "uploads.parts": {
693
747
  readonly method: "GET";
694
748
  readonly path: "/v1/uploads/:id/parts";
@@ -910,6 +964,8 @@ export declare function bind(call: Call): {
910
964
  *
911
965
  * A file under Shard Protection is kept as fragments, each sealed with the file's own key and stored across several storage accounts. Its map says where each fragment is, up to 1,000 at a time, with the key. Fetch each fragment from its address (no key: the address is the permission), check and open it as the SDK's openFragment does, and write it at its place. The first page counts the file's size toward the month's downloads; later pages count nothing. Addresses work for four hours: before then, ask for the page again from the first fragment not yet fetched. The key is in this answer and nowhere else. This API never keeps it, and neither should a program, beyond the read.
912
966
  *
967
+ * A file in version 2 of the shard format (format 2) is kept as blocks of their own lengths, cut by its content. The first page carries its outline: check it with the key before using a byte, open each block as the SDK's openBlock does, and write it after the blocks before it. A program says which versions it reads with formats, and a file in another is answered with 409 format_unsupported, so a program made before version 2 is told to update rather than failing on the file.
968
+ *
913
969
  * GET /v1/files/:id/shards. Needs the scope files:read.
914
970
  */
915
971
  shards: (id: string, query?: FilesShardsQuery, options?: CallOptions) => Promise<FilesShardsAnswer>;
@@ -966,6 +1022,8 @@ export declare function bind(call: Call): {
966
1022
  *
967
1023
  * Into a place under Shard Protection, an upload begun with protectable true is sealed as it is sent. A place is protected by the setting nearest it (files.update's protection), or with none by the drive's default: an app's, which its owner turns on from the app's page, or for a device the account's. The answer's shards then says how the file is cut, and uploads.get gives its key. Each part is a fragment: the file's bytes from its number times fragmentBytes, sealed with that key as the SDK's sealFragment does, under a nonce of its own, and sent to the part's address. uploads.fragments confirms them, and completing makes the file, protected from its first byte, with a null clientSha256. A sealed upload sets blocks aside: none is kept, and none reused. Begun without protectable, a file lands as an ordinary file and the service protects it within a few minutes; from then on it is read through its map (files.shards).
968
1024
  *
1025
+ * A program that writes version 2 of the shard format says so with shardFormats. A sealed file of more than 16 MiB is then written in it: shards says format 2, with no fragments and no parts yet. Get the key from uploads.get, cut the file into blocks by its content with it, and declare them with uploads.blocks, which answers with the parts to send. Replacing a file whose content is in version 2, with a key that may read the file, the key is the file's own, and the blocks the file has already are not sent again. Complete it with outline, the outline's MAC.
1026
+ *
969
1027
  * POST /v1/uploads. Needs the scope files:write. Honours an Idempotency-Key.
970
1028
  */
971
1029
  begin: (body: UploadsBeginBody, options?: CallOptions) => Promise<UploadsBeginAnswer>;
@@ -977,6 +1035,14 @@ export declare function bind(call: Call): {
977
1035
  * GET /v1/uploads/:id. Needs the scope files:write.
978
1036
  */
979
1037
  get: (id: string, options?: CallOptions) => Promise<UploadsGetAnswer>;
1038
+ /**
1039
+ * Declare a sealed upload's blocks.
1040
+ *
1041
+ * For an upload written in version 2 of the shard format (its shards say format 2): the blocks the file is cut into by its content with the file's key, in order, each its size and its id, the HMAC of its bytes under the file's key. From 2 to 10,000 blocks of 1 byte to 64 MiB, adding up to sizeBytes. Answers with the places not to send, whose block the file has already under its key, and addresses for the first of the rest: ask for more with uploads.parts, and confirm them with uploads.fragments. Each part is its block, sealed as the SDK's sealBlock does. Declared once: the same blocks again are answered the same, and other blocks are refused with 409 blocks_declared.
1042
+ *
1043
+ * POST /v1/uploads/:id/blocks. Needs the scope files:write. Safe to send again.
1044
+ */
1045
+ blocks: (id: string, body: UploadsBlocksBody, options?: CallOptions) => Promise<UploadsBlocksAnswer>;
980
1046
  /**
981
1047
  * Addresses for more parts.
982
1048
  *
package/dist/generated.js CHANGED
@@ -16,6 +16,7 @@ export const OPERATIONS = {
16
16
  "changes.list": { method: "GET", path: "/v1/changes", idempotent: false, repeatable: false },
17
17
  "uploads.begin": { method: "POST", path: "/v1/uploads", idempotent: true, repeatable: false },
18
18
  "uploads.get": { method: "GET", path: "/v1/uploads/:id", idempotent: false, repeatable: false },
19
+ "uploads.blocks": { method: "POST", path: "/v1/uploads/:id/blocks", idempotent: false, repeatable: true },
19
20
  "uploads.parts": { method: "GET", path: "/v1/uploads/:id/parts", idempotent: false, repeatable: false },
20
21
  "uploads.fragments": { method: "POST", path: "/v1/uploads/:id/fragments", idempotent: false, repeatable: true },
21
22
  "uploads.complete": { method: "POST", path: "/v1/uploads/:id/complete", idempotent: false, repeatable: true },
@@ -115,6 +116,8 @@ export function bind(call) {
115
116
  *
116
117
  * A file under Shard Protection is kept as fragments, each sealed with the file's own key and stored across several storage accounts. Its map says where each fragment is, up to 1,000 at a time, with the key. Fetch each fragment from its address (no key: the address is the permission), check and open it as the SDK's openFragment does, and write it at its place. The first page counts the file's size toward the month's downloads; later pages count nothing. Addresses work for four hours: before then, ask for the page again from the first fragment not yet fetched. The key is in this answer and nowhere else. This API never keeps it, and neither should a program, beyond the read.
117
118
  *
119
+ * A file in version 2 of the shard format (format 2) is kept as blocks of their own lengths, cut by its content. The first page carries its outline: check it with the key before using a byte, open each block as the SDK's openBlock does, and write it after the blocks before it. A program says which versions it reads with formats, and a file in another is answered with 409 format_unsupported, so a program made before version 2 is told to update rather than failing on the file.
120
+ *
118
121
  * GET /v1/files/:id/shards. Needs the scope files:read.
119
122
  */
120
123
  shards: (id, query, options) => call("files.shards", { params: { id }, query }, options),
@@ -171,6 +174,8 @@ export function bind(call) {
171
174
  *
172
175
  * Into a place under Shard Protection, an upload begun with protectable true is sealed as it is sent. A place is protected by the setting nearest it (files.update's protection), or with none by the drive's default: an app's, which its owner turns on from the app's page, or for a device the account's. The answer's shards then says how the file is cut, and uploads.get gives its key. Each part is a fragment: the file's bytes from its number times fragmentBytes, sealed with that key as the SDK's sealFragment does, under a nonce of its own, and sent to the part's address. uploads.fragments confirms them, and completing makes the file, protected from its first byte, with a null clientSha256. A sealed upload sets blocks aside: none is kept, and none reused. Begun without protectable, a file lands as an ordinary file and the service protects it within a few minutes; from then on it is read through its map (files.shards).
173
176
  *
177
+ * A program that writes version 2 of the shard format says so with shardFormats. A sealed file of more than 16 MiB is then written in it: shards says format 2, with no fragments and no parts yet. Get the key from uploads.get, cut the file into blocks by its content with it, and declare them with uploads.blocks, which answers with the parts to send. Replacing a file whose content is in version 2, with a key that may read the file, the key is the file's own, and the blocks the file has already are not sent again. Complete it with outline, the outline's MAC.
178
+ *
174
179
  * POST /v1/uploads. Needs the scope files:write. Honours an Idempotency-Key.
175
180
  */
176
181
  begin: (body, options) => call("uploads.begin", { body }, options),
@@ -182,6 +187,14 @@ export function bind(call) {
182
187
  * GET /v1/uploads/:id. Needs the scope files:write.
183
188
  */
184
189
  get: (id, options) => call("uploads.get", { params: { id } }, options),
190
+ /**
191
+ * Declare a sealed upload's blocks.
192
+ *
193
+ * For an upload written in version 2 of the shard format (its shards say format 2): the blocks the file is cut into by its content with the file's key, in order, each its size and its id, the HMAC of its bytes under the file's key. From 2 to 10,000 blocks of 1 byte to 64 MiB, adding up to sizeBytes. Answers with the places not to send, whose block the file has already under its key, and addresses for the first of the rest: ask for more with uploads.parts, and confirm them with uploads.fragments. Each part is its block, sealed as the SDK's sealBlock does. Declared once: the same blocks again are answered the same, and other blocks are refused with 409 blocks_declared.
194
+ *
195
+ * POST /v1/uploads/:id/blocks. Needs the scope files:write. Safe to send again.
196
+ */
197
+ blocks: (id, body, options) => call("uploads.blocks", { params: { id }, body }, options),
185
198
  /**
186
199
  * Addresses for more parts.
187
200
  *
package/dist/index.d.ts CHANGED
@@ -3,6 +3,6 @@ export * from "./generated.js";
3
3
  export { FRAME_MAX_BYTES, takesFrame } from "./frames.js";
4
4
  export { get, type GetOptions, type Got, type Into, type LinkedFile } from "./links.js";
5
5
  export { type Sink } from "./ranges.js";
6
- export { checkOutline, fileKeys, MOST_BLOCK_BYTES, openBlock, openFragment, outlineMac, sealBlock, sealFragment, SHARD_OVERHEAD, SHARD_VERSION, SHARD_VERSION_2, ShardError, shardKey, ShardLayout, type FileKeys, type FragmentAt, type Outline, type OutlineBlock, type ShardErrorCode, type ShardKey, } from "./shards.js";
6
+ export { BlockLayout, checkOutline, fileKeys, MOST_BLOCK_BYTES, openBlock, openFragment, outlineMac, READS_FORMATS, sealBlock, sealFragment, SHARD_OVERHEAD, SHARD_VERSION, SHARD_VERSION_2, ShardError, shardKey, ShardLayout, type FileKeys, type FragmentAt, type Layout, type Outline, type OutlineBlock, type ShardErrorCode, type ShardKey, } from "./shards.js";
7
7
  export { FileFreakError, type CallOptions, type ClientOptions, type TokenSource } from "./transport.js";
8
8
  export { UploadStopped, type Source, type UploadOptions } from "./uploads.js";
package/dist/index.js CHANGED
@@ -13,6 +13,6 @@ export { FileFreak, Walk } from "./client.js";
13
13
  export * from "./generated.js";
14
14
  export { FRAME_MAX_BYTES, takesFrame } from "./frames.js";
15
15
  export { get } from "./links.js";
16
- export { checkOutline, fileKeys, MOST_BLOCK_BYTES, openBlock, openFragment, outlineMac, sealBlock, sealFragment, SHARD_OVERHEAD, SHARD_VERSION, SHARD_VERSION_2, ShardError, shardKey, ShardLayout, } from "./shards.js";
16
+ export { BlockLayout, checkOutline, fileKeys, MOST_BLOCK_BYTES, openBlock, openFragment, outlineMac, READS_FORMATS, sealBlock, sealFragment, SHARD_OVERHEAD, SHARD_VERSION, SHARD_VERSION_2, ShardError, shardKey, ShardLayout, } from "./shards.js";
17
17
  export { FileFreakError } from "./transport.js";
18
18
  export { UploadStopped } from "./uploads.js";