@peers-app/peers-sdk 0.29.2 → 0.30.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/dist/contracts/contract-resolvers.js +1 -0
- package/dist/contracts/index.d.ts +1 -0
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/system/file-storage-contract.d.ts +76 -0
- package/dist/contracts/system/file-storage-contract.js +75 -0
- package/dist/contracts/system/files-contract.d.ts +5 -1
- package/dist/contracts/system/files-contract.js +50 -9
- package/dist/contracts/system/files-shared.d.ts +68 -3
- package/dist/contracts/system/files-shared.js +65 -5
- package/dist/contracts/validate.d.ts +1 -0
- package/dist/contracts/validate.js +6 -1
- package/dist/data/derived-row-id.d.ts +26 -0
- package/dist/data/derived-row-id.js +47 -0
- package/dist/data/files/chunk-store.d.ts +112 -0
- package/dist/data/files/chunk-store.js +107 -0
- package/dist/data/files/file-read-stream.d.ts +32 -3
- package/dist/data/files/file-read-stream.js +44 -29
- package/dist/data/files/file-rows.d.ts +68 -0
- package/dist/data/files/file-rows.js +158 -0
- package/dist/data/files/file-write-stream.d.ts +26 -0
- package/dist/data/files/file-write-stream.js +66 -42
- package/dist/data/files/file.types.d.ts +44 -23
- package/dist/data/files/file.types.js +33 -51
- package/dist/data/files/files.d.ts +27 -24
- package/dist/data/files/files.js +63 -85
- package/dist/data/files/index.d.ts +2 -0
- package/dist/data/files/index.js +2 -0
- package/dist/data/index.d.ts +1 -0
- package/dist/data/index.js +1 -0
- package/dist/data/messages.d.ts +21 -0
- package/dist/data/messages.js +21 -1
- package/dist/data/orm/sql.data-source.d.ts +2 -1
- package/dist/data/orm/sql.data-source.js +1 -0
- package/dist/data/orm/types.js +12 -0
- package/dist/data/package-versions.js +3 -3
- package/dist/define/index.d.ts +1 -1
- package/dist/define/index.js +2 -1
- package/dist/define/system-contracts.d.ts +9 -2
- package/dist/define/system-contracts.js +6 -2
- package/dist/device-pairing/types.d.ts +2 -2
- package/dist/package-build/build-package.d.ts +12 -0
- package/dist/package-build/build-package.js +45 -6
- package/dist/package-build/cli.js +8 -0
- package/dist/package-build/contract-source.js +7 -5
- package/dist/package-installer/package-installer.d.ts +3 -5
- package/dist/package-installer/package-installer.js +5 -21
- package/dist/package-installer/package-propagation.js +6 -17
- package/dist/system-ids.d.ts +2 -0
- package/dist/system-ids.js +3 -1
- package/dist/types/field-type.d.ts +14 -0
- package/dist/types/field-type.js +14 -1
- package/dist/types/zod-types.d.ts +10 -0
- package/dist/types/zod-types.js +25 -7
- package/package.json +1 -1
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { type IFile, type IFileInput } from "./file.types";
|
|
2
2
|
import type { FilesTable } from "./files";
|
|
3
|
+
/**
|
|
4
|
+
* Writes one file in pieces: each full chunk is stored as it arrives, and `finalize` saves the
|
|
5
|
+
* row under the id derived from the content. Open one with {@link FilesTable.createWriteStream}.
|
|
6
|
+
*/
|
|
3
7
|
export declare class FileWriteStream {
|
|
4
8
|
private metadata;
|
|
5
9
|
private fileTable;
|
|
@@ -8,13 +12,35 @@ export declare class FileWriteStream {
|
|
|
8
12
|
private bytesWritten;
|
|
9
13
|
private finalized;
|
|
10
14
|
private aborted;
|
|
15
|
+
/**
|
|
16
|
+
* @param metadata - The file's name, MIME type and `keep`.
|
|
17
|
+
* @param fileTable - The Files table the row is saved to.
|
|
18
|
+
*/
|
|
11
19
|
constructor(metadata: IFileInput, fileTable: FilesTable);
|
|
20
|
+
/**
|
|
21
|
+
* Append bytes. Full chunks are stored as they complete.
|
|
22
|
+
*
|
|
23
|
+
* @param chunk - The next bytes, of any length.
|
|
24
|
+
* @throws When the file would exceed {@link FILE_MAX_SIZE}, or the stream has ended.
|
|
25
|
+
*/
|
|
12
26
|
write(chunk: Uint8Array): Promise<void>;
|
|
13
27
|
private processCompleteChunks;
|
|
28
|
+
private storeChunk;
|
|
29
|
+
/**
|
|
30
|
+
* Store the last chunk and save the file's row. Saving content that already has a row (the
|
|
31
|
+
* same bytes, name, MIME type and `keep`) writes no row and returns the stored one.
|
|
32
|
+
*
|
|
33
|
+
* @returns The file's row.
|
|
34
|
+
*/
|
|
14
35
|
finalize(): Promise<IFile>;
|
|
36
|
+
/** Stop without saving a row. Stored chunks stay until collection frees them. */
|
|
15
37
|
abort(): Promise<void>;
|
|
38
|
+
/** Bytes written so far. */
|
|
16
39
|
getBytesWritten(): number;
|
|
40
|
+
/** Chunks stored so far. */
|
|
17
41
|
getChunkCount(): number;
|
|
42
|
+
/** Whether `finalize` has saved the row. */
|
|
18
43
|
isFinalized(): boolean;
|
|
44
|
+
/** Whether `abort` was called. */
|
|
19
45
|
isAborted(): boolean;
|
|
20
46
|
}
|
|
@@ -2,7 +2,26 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.FileWriteStream = void 0;
|
|
4
4
|
const keys_1 = require("../../keys");
|
|
5
|
+
const chunk_store_1 = require("./chunk-store");
|
|
5
6
|
const file_types_1 = require("./file.types");
|
|
7
|
+
const file_rows_1 = require("./file-rows");
|
|
8
|
+
/**
|
|
9
|
+
* `text` as every host's storage keeps it. The id must be derived from the text as stored, or no
|
|
10
|
+
* other device could rederive it. A lone surrogate (possible in a Windows file name) becomes
|
|
11
|
+
* U+FFFD. The PWA's sql.js also cuts text at a NUL and drops a leading U+FEFF when reading, so
|
|
12
|
+
* neither is kept anywhere.
|
|
13
|
+
*/
|
|
14
|
+
function asStored(text) {
|
|
15
|
+
if (typeof text !== "string")
|
|
16
|
+
return text;
|
|
17
|
+
// ignoreBOM leaves U+FEFF for the replace, which drops every leading one (sql.js drops one).
|
|
18
|
+
const wellFormed = new TextDecoder("utf-8", { ignoreBOM: true }).decode(new TextEncoder().encode(text));
|
|
19
|
+
return wellFormed.split("\0", 1)[0].replace(/^\uFEFF+/, "");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Writes one file in pieces: each full chunk is stored as it arrives, and `finalize` saves the
|
|
23
|
+
* row under the id derived from the content. Open one with {@link FilesTable.createWriteStream}.
|
|
24
|
+
*/
|
|
6
25
|
class FileWriteStream {
|
|
7
26
|
metadata;
|
|
8
27
|
fileTable;
|
|
@@ -11,10 +30,20 @@ class FileWriteStream {
|
|
|
11
30
|
bytesWritten = 0;
|
|
12
31
|
finalized = false;
|
|
13
32
|
aborted = false;
|
|
33
|
+
/**
|
|
34
|
+
* @param metadata - The file's name, MIME type and `keep`.
|
|
35
|
+
* @param fileTable - The Files table the row is saved to.
|
|
36
|
+
*/
|
|
14
37
|
constructor(metadata, fileTable) {
|
|
15
38
|
this.metadata = metadata;
|
|
16
39
|
this.fileTable = fileTable;
|
|
17
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Append bytes. Full chunks are stored as they complete.
|
|
43
|
+
*
|
|
44
|
+
* @param chunk - The next bytes, of any length.
|
|
45
|
+
* @throws When the file would exceed {@link FILE_MAX_SIZE}, or the stream has ended.
|
|
46
|
+
*/
|
|
18
47
|
async write(chunk) {
|
|
19
48
|
if (this.finalized) {
|
|
20
49
|
throw new Error("Cannot write to finalized stream");
|
|
@@ -22,6 +51,9 @@ class FileWriteStream {
|
|
|
22
51
|
if (this.aborted) {
|
|
23
52
|
throw new Error("Cannot write to aborted stream");
|
|
24
53
|
}
|
|
54
|
+
if (this.bytesWritten + chunk.length > file_types_1.FILE_MAX_SIZE) {
|
|
55
|
+
throw new Error(`A file can be at most ${file_types_1.FILE_MAX_SIZE / (1024 * 1024)} MiB`);
|
|
56
|
+
}
|
|
25
57
|
// Add chunk to buffer
|
|
26
58
|
this.buffer = new Uint8Array(Buffer.concat([this.buffer, chunk]));
|
|
27
59
|
this.bytesWritten += chunk.length;
|
|
@@ -29,18 +61,25 @@ class FileWriteStream {
|
|
|
29
61
|
await this.processCompleteChunks();
|
|
30
62
|
}
|
|
31
63
|
async processCompleteChunks() {
|
|
32
|
-
const ops = await (0, file_types_1.getFileOps)();
|
|
33
64
|
while (this.buffer.length >= file_types_1.FILE_CHUNK_SIZE) {
|
|
34
|
-
// Extract one chunk
|
|
35
65
|
const chunkData = this.buffer.subarray(0, file_types_1.FILE_CHUNK_SIZE);
|
|
36
66
|
this.buffer = this.buffer.subarray(file_types_1.FILE_CHUNK_SIZE);
|
|
37
|
-
|
|
38
|
-
const chunkHash = (0, keys_1.hashBytes)(chunkData);
|
|
39
|
-
this.chunkHashes.push(chunkHash);
|
|
40
|
-
const chunkPath = `${file_types_1.CHUNKS_DIR}/${chunkHash}`;
|
|
41
|
-
await ops.writeFile(chunkPath, chunkData);
|
|
67
|
+
await this.storeChunk(chunkData);
|
|
42
68
|
}
|
|
43
69
|
}
|
|
70
|
+
async storeChunk(chunkData) {
|
|
71
|
+
const chunkHash = (0, keys_1.hashBytes)(chunkData);
|
|
72
|
+
this.chunkHashes.push(chunkHash);
|
|
73
|
+
// Stored even when present: a save restores bytes a device has freed, and refreshes the
|
|
74
|
+
// chunk's age so a sweep can't free it under this save.
|
|
75
|
+
await (await (0, chunk_store_1.getChunkStore)()).put(chunkHash, chunkData);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Store the last chunk and save the file's row. Saving content that already has a row (the
|
|
79
|
+
* same bytes, name, MIME type and `keep`) writes no row and returns the stored one.
|
|
80
|
+
*
|
|
81
|
+
* @returns The file's row.
|
|
82
|
+
*/
|
|
44
83
|
async finalize() {
|
|
45
84
|
if (this.finalized) {
|
|
46
85
|
throw new Error("Stream already finalized");
|
|
@@ -48,45 +87,28 @@ class FileWriteStream {
|
|
|
48
87
|
if (this.aborted) {
|
|
49
88
|
throw new Error("Cannot finalize aborted stream");
|
|
50
89
|
}
|
|
51
|
-
// First, process any complete chunks in the buffer
|
|
52
90
|
await this.processCompleteChunks();
|
|
53
|
-
// Then process any remaining partial data as final chunk
|
|
54
91
|
if (this.buffer.length > 0) {
|
|
55
|
-
|
|
56
|
-
const finalChunkHash = (0, keys_1.hashBytes)(this.buffer);
|
|
57
|
-
this.chunkHashes.push(finalChunkHash);
|
|
58
|
-
const chunkPath = `${file_types_1.CHUNKS_DIR}/${finalChunkHash}`;
|
|
59
|
-
await ops.writeFile(chunkPath, this.buffer);
|
|
92
|
+
await this.storeChunk(this.buffer);
|
|
60
93
|
this.buffer = new Uint8Array(0);
|
|
61
94
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
else {
|
|
77
|
-
// Small file: store chunk hashes directly
|
|
78
|
-
fileRecord = {
|
|
79
|
-
...this.metadata,
|
|
80
|
-
fileSize: this.bytesWritten,
|
|
81
|
-
fileHash,
|
|
82
|
-
chunkHashes: this.chunkHashes,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
// Save to database
|
|
86
|
-
await this.fileTable.saveFileRecord(fileRecord);
|
|
95
|
+
const description = {
|
|
96
|
+
name: asStored(this.metadata.name),
|
|
97
|
+
fileSize: this.bytesWritten,
|
|
98
|
+
fileHash: (0, file_types_1.fileHashOf)(this.chunkHashes),
|
|
99
|
+
mimeType: asStored(this.metadata.mimeType ?? ""),
|
|
100
|
+
keep: this.metadata.keep ?? true,
|
|
101
|
+
};
|
|
102
|
+
const fileRecord = {
|
|
103
|
+
fileId: (0, file_rows_1.fileIdFor)(description),
|
|
104
|
+
...description,
|
|
105
|
+
chunkHashes: this.chunkHashes,
|
|
106
|
+
};
|
|
107
|
+
const saved = await this.fileTable.saveFileRecord(fileRecord);
|
|
87
108
|
this.finalized = true;
|
|
88
|
-
return
|
|
109
|
+
return saved;
|
|
89
110
|
}
|
|
111
|
+
/** Stop without saving a row. Stored chunks stay until collection frees them. */
|
|
90
112
|
async abort() {
|
|
91
113
|
if (this.finalized) {
|
|
92
114
|
throw new Error("Cannot abort finalized stream");
|
|
@@ -94,18 +116,20 @@ class FileWriteStream {
|
|
|
94
116
|
this.aborted = true;
|
|
95
117
|
this.buffer = Buffer.alloc(0);
|
|
96
118
|
this.chunkHashes = [];
|
|
97
|
-
// Note: We don't delete chunks since they may be shared with other files
|
|
98
|
-
// Chunk cleanup should be handled by a separate garbage collection process
|
|
99
119
|
}
|
|
120
|
+
/** Bytes written so far. */
|
|
100
121
|
getBytesWritten() {
|
|
101
122
|
return this.bytesWritten;
|
|
102
123
|
}
|
|
124
|
+
/** Chunks stored so far. */
|
|
103
125
|
getChunkCount() {
|
|
104
126
|
return this.chunkHashes.length;
|
|
105
127
|
}
|
|
128
|
+
/** Whether `finalize` has saved the row. */
|
|
106
129
|
isFinalized() {
|
|
107
130
|
return this.finalized;
|
|
108
131
|
}
|
|
132
|
+
/** Whether `abort` was called. */
|
|
109
133
|
isAborted() {
|
|
110
134
|
return this.aborted;
|
|
111
135
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { type ITableMetaData } from "../orm/types";
|
|
3
|
+
/**
|
|
4
|
+
* A `Files` row: one file's description and its chunk list. Rows saved since ids became derived
|
|
5
|
+
* from content have a `000r` id and never change; older rows ("legacy") have random ids.
|
|
6
|
+
*/
|
|
3
7
|
export declare const fileSchema: z.ZodObject<{
|
|
4
8
|
fileId: z.ZodEffects<z.ZodString, string, string>;
|
|
5
9
|
name: z.ZodString;
|
|
@@ -7,47 +11,64 @@ export declare const fileSchema: z.ZodObject<{
|
|
|
7
11
|
fileHash: z.ZodString;
|
|
8
12
|
mimeType: z.ZodOptional<z.ZodString>;
|
|
9
13
|
chunkHashes: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
10
|
-
|
|
11
|
-
indexFileId: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
14
|
+
keep: z.ZodOptional<z.ZodBoolean>;
|
|
12
15
|
}, "strip", z.ZodTypeAny, {
|
|
13
16
|
name: string;
|
|
14
17
|
fileId: string;
|
|
15
18
|
fileSize: number;
|
|
16
19
|
fileHash: string;
|
|
20
|
+
keep?: boolean | undefined;
|
|
17
21
|
mimeType?: string | undefined;
|
|
18
22
|
chunkHashes?: string[] | undefined;
|
|
19
|
-
isIndexFile?: boolean | undefined;
|
|
20
|
-
indexFileId?: string | undefined;
|
|
21
23
|
}, {
|
|
22
24
|
name: string;
|
|
23
25
|
fileId: string;
|
|
24
26
|
fileSize: number;
|
|
25
27
|
fileHash: string;
|
|
28
|
+
keep?: boolean | undefined;
|
|
26
29
|
mimeType?: string | undefined;
|
|
27
30
|
chunkHashes?: string[] | undefined;
|
|
28
|
-
isIndexFile?: boolean | undefined;
|
|
29
|
-
indexFileId?: string | undefined;
|
|
30
31
|
}>;
|
|
32
|
+
/** A `Files` row. */
|
|
31
33
|
export type IFile = z.infer<typeof fileSchema>;
|
|
32
|
-
|
|
34
|
+
/**
|
|
35
|
+
* What a writer chooses about a new file. The id is derived from the content and these, so
|
|
36
|
+
* saving the same bytes with the same name, MIME type and `keep` returns the same file.
|
|
37
|
+
*/
|
|
38
|
+
export interface IFileInput {
|
|
39
|
+
/**
|
|
40
|
+
* Stored as every host can keep it: a lone surrogate becomes U+FFFD, the text ends at a NUL,
|
|
41
|
+
* and a leading U+FEFF is dropped. The saved row has the name as stored.
|
|
42
|
+
*/
|
|
43
|
+
name: string;
|
|
44
|
+
/** Stored like `name`; an omitted MIME type is stored empty. */
|
|
45
|
+
mimeType?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Whether the file is never collected. Defaults to true, so a writer that doesn't declare its
|
|
48
|
+
* references keeps its files. System Files saves and the package installer pass false.
|
|
49
|
+
*/
|
|
50
|
+
keep?: boolean;
|
|
51
|
+
}
|
|
52
|
+
/** The `Files` system table: every context has one. */
|
|
33
53
|
export declare const filesMetaData: ITableMetaData;
|
|
54
|
+
/** Size of each chunk a file is split into; the last chunk may be shorter. */
|
|
34
55
|
export declare const FILE_CHUNK_SIZE: number;
|
|
35
|
-
|
|
36
|
-
export declare
|
|
37
|
-
|
|
56
|
+
/** Most chunks one file may have: a row's chunk list holds every chunk of any file. */
|
|
57
|
+
export declare const FILE_MAX_CHUNKS = 1024;
|
|
58
|
+
/** Largest file Peers stores (1 GiB). It can rise in a later release, never fall. */
|
|
59
|
+
export declare const FILE_MAX_SIZE: number;
|
|
60
|
+
/** Most chunks a read fetches from other devices at once, so a large file isn't one stream. */
|
|
61
|
+
export declare const FILE_FETCH_WINDOW = 16;
|
|
62
|
+
/**
|
|
63
|
+
* A file's `fileHash`: the SHA-256 of its chunk hashes as JSON, in order.
|
|
64
|
+
*
|
|
65
|
+
* @param chunkHashes - The file's chunk hashes.
|
|
66
|
+
*/
|
|
67
|
+
export declare function fileHashOf(chunkHashes: readonly string[]): string;
|
|
38
68
|
/**
|
|
39
|
-
* Compute the file-system hash for content without saving it.
|
|
40
|
-
*
|
|
41
|
-
*
|
|
69
|
+
* Compute the file-system hash for content without saving it. Mirrors the chunking of
|
|
70
|
+
* `FileWriteStream`, so the result matches `IFile.fileHash` for the same bytes.
|
|
71
|
+
*
|
|
72
|
+
* @param content - The bytes, or a string hashed as UTF-8.
|
|
42
73
|
*/
|
|
43
74
|
export declare function computeFileHash(content: string | Uint8Array): string;
|
|
44
|
-
export interface FileOps {
|
|
45
|
-
downloadFileChunk(chunkHash: string): Promise<Uint8Array | null>;
|
|
46
|
-
fileExists(path: string): Promise<boolean>;
|
|
47
|
-
readFile(path: string): Promise<Uint8Array>;
|
|
48
|
-
writeFile(path: string, data: Uint8Array): Promise<void>;
|
|
49
|
-
deletePath(path: string): Promise<void>;
|
|
50
|
-
}
|
|
51
|
-
export declare function setFileOps(ops: FileOps): void;
|
|
52
|
-
export declare function getFileOps(): Promise<FileOps>;
|
|
53
|
-
export declare function resetFileOps(): void;
|
|
@@ -1,80 +1,62 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
exports.
|
|
3
|
+
exports.FILE_FETCH_WINDOW = exports.FILE_MAX_SIZE = exports.FILE_MAX_CHUNKS = exports.FILE_CHUNK_SIZE = exports.filesMetaData = exports.fileSchema = void 0;
|
|
4
|
+
exports.fileHashOf = fileHashOf;
|
|
5
5
|
exports.computeFileHash = computeFileHash;
|
|
6
|
-
exports.setFileOps = setFileOps;
|
|
7
|
-
exports.getFileOps = getFileOps;
|
|
8
|
-
exports.resetFileOps = resetFileOps;
|
|
9
6
|
const zod_1 = require("zod");
|
|
10
7
|
const keys_1 = require("../../keys");
|
|
11
8
|
const zod_types_1 = require("../../types/zod-types");
|
|
12
9
|
const types_1 = require("../orm/types");
|
|
10
|
+
/**
|
|
11
|
+
* A `Files` row: one file's description and its chunk list. Rows saved since ids became derived
|
|
12
|
+
* from content have a `000r` id and never change; older rows ("legacy") have random ids.
|
|
13
|
+
*/
|
|
13
14
|
exports.fileSchema = zod_1.z.object({
|
|
14
15
|
fileId: zod_types_1.zodPeerId,
|
|
15
16
|
name: zod_1.z.string().describe("The name of the file"),
|
|
16
17
|
fileSize: zod_1.z.number().describe("The size of the file in bytes"),
|
|
17
18
|
fileHash: zod_1.z.string().describe("Hash of the chunk hashes array for integrity verification"),
|
|
18
|
-
mimeType: zod_1.z.string().optional().describe("The MIME type of the file"),
|
|
19
|
-
chunkHashes: zod_1.z
|
|
20
|
-
|
|
21
|
-
.optional()
|
|
22
|
-
.describe("SHA-256 hashes of each chunk in order (for small files)"),
|
|
23
|
-
isIndexFile: zod_1.z
|
|
19
|
+
mimeType: zod_1.z.string().optional().describe("The MIME type of the file; empty when unknown"),
|
|
20
|
+
chunkHashes: zod_1.z.array(zod_1.z.string()).optional().describe("SHA-256 hashes of each chunk in order"),
|
|
21
|
+
keep: zod_1.z
|
|
24
22
|
.boolean()
|
|
25
23
|
.optional()
|
|
26
|
-
.describe("True
|
|
27
|
-
indexFileId: zod_types_1.zodPeerId.optional().describe("FileId of index file (for large files)"),
|
|
24
|
+
.describe("True for a file that is never collected, even with nothing referencing it"),
|
|
28
25
|
});
|
|
26
|
+
/** The `Files` system table: every context has one. */
|
|
29
27
|
exports.filesMetaData = {
|
|
30
28
|
name: "Files",
|
|
31
29
|
description: "Files stored in the chunked file system for peer sharing",
|
|
32
30
|
primaryKeyName: "fileId",
|
|
33
31
|
fields: (0, types_1.schemaToFields)(exports.fileSchema),
|
|
34
32
|
};
|
|
35
|
-
|
|
36
|
-
exports.FILE_CHUNK_SIZE = 1024 * 1024;
|
|
37
|
-
|
|
38
|
-
exports.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
/** Size of each chunk a file is split into; the last chunk may be shorter. */
|
|
34
|
+
exports.FILE_CHUNK_SIZE = 1024 * 1024;
|
|
35
|
+
/** Most chunks one file may have: a row's chunk list holds every chunk of any file. */
|
|
36
|
+
exports.FILE_MAX_CHUNKS = 1024;
|
|
37
|
+
/** Largest file Peers stores (1 GiB). It can rise in a later release, never fall. */
|
|
38
|
+
exports.FILE_MAX_SIZE = exports.FILE_MAX_CHUNKS * exports.FILE_CHUNK_SIZE;
|
|
39
|
+
/** Most chunks a read fetches from other devices at once, so a large file isn't one stream. */
|
|
40
|
+
exports.FILE_FETCH_WINDOW = 16;
|
|
41
|
+
/**
|
|
42
|
+
* A file's `fileHash`: the SHA-256 of its chunk hashes as JSON, in order.
|
|
43
|
+
*
|
|
44
|
+
* @param chunkHashes - The file's chunk hashes.
|
|
45
|
+
*/
|
|
46
|
+
function fileHashOf(chunkHashes) {
|
|
47
|
+
return (0, keys_1.hashBytes)(new TextEncoder().encode(JSON.stringify(chunkHashes)));
|
|
42
48
|
}
|
|
43
49
|
/**
|
|
44
|
-
* Compute the file-system hash for content without saving it.
|
|
45
|
-
*
|
|
46
|
-
*
|
|
50
|
+
* Compute the file-system hash for content without saving it. Mirrors the chunking of
|
|
51
|
+
* `FileWriteStream`, so the result matches `IFile.fileHash` for the same bytes.
|
|
52
|
+
*
|
|
53
|
+
* @param content - The bytes, or a string hashed as UTF-8.
|
|
47
54
|
*/
|
|
48
55
|
function computeFileHash(content) {
|
|
49
56
|
const data = typeof content === "string" ? new Uint8Array(Buffer.from(content, "utf8")) : content;
|
|
50
57
|
const chunkHashes = [];
|
|
51
|
-
let offset = 0;
|
|
52
|
-
|
|
53
|
-
const end = Math.min(offset + exports.FILE_CHUNK_SIZE, data.length);
|
|
54
|
-
chunkHashes.push((0, keys_1.hashBytes)(data.subarray(offset, end)));
|
|
55
|
-
offset = end;
|
|
58
|
+
for (let offset = 0; offset < data.length; offset += exports.FILE_CHUNK_SIZE) {
|
|
59
|
+
chunkHashes.push((0, keys_1.hashBytes)(data.subarray(offset, offset + exports.FILE_CHUNK_SIZE)));
|
|
56
60
|
}
|
|
57
|
-
return (
|
|
58
|
-
}
|
|
59
|
-
let fileOps = null;
|
|
60
|
-
let fileOpsReady = null;
|
|
61
|
-
function setFileOps(ops) {
|
|
62
|
-
fileOps = ops;
|
|
63
|
-
if (fileOpsReady) {
|
|
64
|
-
fileOpsReady(ops);
|
|
65
|
-
fileOpsReady = null; // Reset the promise if it was waiting
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
function getFileOps() {
|
|
69
|
-
if (fileOps) {
|
|
70
|
-
return Promise.resolve(fileOps);
|
|
71
|
-
}
|
|
72
|
-
return new Promise((resolve) => {
|
|
73
|
-
fileOpsReady = resolve;
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
// For testing - reset the global state
|
|
77
|
-
function resetFileOps() {
|
|
78
|
-
fileOps = null;
|
|
79
|
-
fileOpsReady = null;
|
|
61
|
+
return fileHashOf(chunkHashes);
|
|
80
62
|
}
|
|
@@ -5,8 +5,10 @@ import { FileReadStream } from "./file-read-stream";
|
|
|
5
5
|
import { FileWriteStream } from "./file-write-stream";
|
|
6
6
|
/**
|
|
7
7
|
* The chunked, content-hashed file store of one data context. Rows describe files; the bytes
|
|
8
|
-
* live in chunks
|
|
9
|
-
* device when it reads.
|
|
8
|
+
* live in chunks in the account's chunk store, shared by hash, and a device fetches a missing
|
|
9
|
+
* chunk from a connected device when it reads. A file's id is derived from its content, and its row never changes.
|
|
10
|
+
* Write through {@link FilesTable.saveFile}, never the table methods. The rules are checked here
|
|
11
|
+
* in every context, and by every device on the rows it receives in a group (`file-rows.ts`).
|
|
10
12
|
*/
|
|
11
13
|
export declare class FilesTable extends Table<IFile> {
|
|
12
14
|
/** @deprecated Direct inserts forbidden; use saveFile() or saveFileRecord() */
|
|
@@ -20,14 +22,18 @@ export declare class FilesTable extends Table<IFile> {
|
|
|
20
22
|
/**
|
|
21
23
|
* Start writing a file in chunks; `finalize()` saves its row.
|
|
22
24
|
*
|
|
23
|
-
* @param metadata - The new file's
|
|
25
|
+
* @param metadata - The new file's name, MIME type and `keep`.
|
|
24
26
|
*/
|
|
25
27
|
createWriteStream(metadata: IFileInput): Promise<FileWriteStream>;
|
|
26
28
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
29
|
+
* Store a file's row as it is, after the checks every device makes: a new row must have the
|
|
30
|
+
* id derived from its content (or be a legacy row), and a stored row never changes. Storing
|
|
31
|
+
* the same file again writes nothing. Writers use {@link saveFile} or a write stream; this is
|
|
32
|
+
* for copying a row from another context with its id.
|
|
33
|
+
*
|
|
34
|
+
* @param fileRecord - The row.
|
|
35
|
+
* @returns The row as stored.
|
|
36
|
+
* @throws When the row is refused, or a different row has its id.
|
|
31
37
|
*/
|
|
32
38
|
saveFileRecord(fileRecord: IFile): Promise<IFile>;
|
|
33
39
|
/**
|
|
@@ -40,11 +46,12 @@ export declare class FilesTable extends Table<IFile> {
|
|
|
40
46
|
*/
|
|
41
47
|
openReadStream(fileId: string, preloadChunksCount?: number, skipVerification?: boolean): Promise<FileReadStream | null>;
|
|
42
48
|
/**
|
|
43
|
-
* Save a whole file: chunk it, store the chunks and write its row.
|
|
49
|
+
* Save a whole file: chunk it, store the chunks and write its row. The same bytes, name, MIME
|
|
50
|
+
* type and `keep` give the same file, whose chunks are stored again.
|
|
44
51
|
*
|
|
45
|
-
* @param metaData - The file's
|
|
52
|
+
* @param metaData - The file's name, MIME type and `keep` (true unless given).
|
|
46
53
|
* @param data - The bytes, or a string saved as UTF-8.
|
|
47
|
-
* @returns The
|
|
54
|
+
* @returns The file's row.
|
|
48
55
|
*/
|
|
49
56
|
saveFile(metaData: IFileInput, data: Uint8Array | string): Promise<IFile>;
|
|
50
57
|
/**
|
|
@@ -58,25 +65,21 @@ export declare class FilesTable extends Table<IFile> {
|
|
|
58
65
|
skipVerification?: boolean;
|
|
59
66
|
}): Promise<Uint8Array | null>;
|
|
60
67
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* @param fileId - File to delete. A missing file is not an error.
|
|
64
|
-
*/
|
|
65
|
-
deleteFile(fileId: string): Promise<void>;
|
|
66
|
-
/**
|
|
67
|
-
* Save a large file's chunk hashes as an index file, itself chunked when large.
|
|
68
|
+
* Make sure this device has every chunk of a file, fetching the missing ones from connected
|
|
69
|
+
* devices {@link FILE_FETCH_WINDOW} at a time, without reading the file into memory.
|
|
68
70
|
*
|
|
69
|
-
* @param
|
|
70
|
-
* @returns
|
|
71
|
+
* @param fileId - File to fetch.
|
|
72
|
+
* @returns Whether every chunk is now on this device; false for a file with no row.
|
|
71
73
|
*/
|
|
72
|
-
|
|
74
|
+
fetchFile(fileId: string): Promise<boolean>;
|
|
73
75
|
/**
|
|
74
|
-
*
|
|
76
|
+
* Delete a legacy bundle row, which the runtime owns. Every other file is permanent: it's
|
|
77
|
+
* collected once nothing references it. Chunks stay stored, since other files may share them.
|
|
75
78
|
*
|
|
76
|
-
* @param
|
|
77
|
-
* @
|
|
79
|
+
* @param fileId - File to delete. A missing file is not an error.
|
|
80
|
+
* @throws When the file isn't a legacy bundle row.
|
|
78
81
|
*/
|
|
79
|
-
|
|
82
|
+
deleteFile(fileId: string): Promise<void>;
|
|
80
83
|
}
|
|
81
84
|
/**
|
|
82
85
|
* The Files table of a data context.
|