@lumeweb/pinner 0.0.1 → 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/LICENSE +9 -0
- package/README.md +690 -28
- package/dist/cjs/_virtual/rolldown_runtime.cjs +29 -0
- package/dist/cjs/adapters/pinata/adapter.cjs +88 -0
- package/dist/cjs/adapters/pinata/adapter.cjs.map +1 -0
- package/dist/cjs/adapters/pinata/adapter.d.cts +35 -0
- package/dist/cjs/adapters/pinata/builder.cjs +194 -0
- package/dist/cjs/adapters/pinata/builder.cjs.map +1 -0
- package/dist/cjs/adapters/pinata/index.cjs +3 -0
- package/dist/cjs/adapters/pinata/list-builder.cjs +52 -0
- package/dist/cjs/adapters/pinata/list-builder.cjs.map +1 -0
- package/dist/cjs/blockstore/index.cjs +2 -0
- package/dist/cjs/blockstore/unstorage-base.cjs +240 -0
- package/dist/cjs/blockstore/unstorage-base.cjs.map +1 -0
- package/dist/cjs/blockstore/unstorage-base.d.cts +23 -0
- package/dist/cjs/blockstore/unstorage.cjs +39 -0
- package/dist/cjs/blockstore/unstorage.cjs.map +1 -0
- package/dist/cjs/blockstore/unstorage.d.cts +36 -0
- package/dist/cjs/config.d.cts +51 -0
- package/dist/cjs/encoder/base64.cjs +38 -0
- package/dist/cjs/encoder/base64.cjs.map +1 -0
- package/dist/cjs/encoder/csv/csv-formatter.cjs +81 -0
- package/dist/cjs/encoder/csv/csv-formatter.cjs.map +1 -0
- package/dist/cjs/encoder/csv/field-formatter.cjs +76 -0
- package/dist/cjs/encoder/csv/field-formatter.cjs.map +1 -0
- package/dist/cjs/encoder/csv/row-formatter.cjs +159 -0
- package/dist/cjs/encoder/csv/row-formatter.cjs.map +1 -0
- package/dist/cjs/encoder/csv.cjs +44 -0
- package/dist/cjs/encoder/csv.cjs.map +1 -0
- package/dist/cjs/encoder/error.cjs +19 -0
- package/dist/cjs/encoder/error.cjs.map +1 -0
- package/dist/cjs/encoder/index.cjs +6 -0
- package/dist/cjs/encoder/json.cjs +36 -0
- package/dist/cjs/encoder/json.cjs.map +1 -0
- package/dist/cjs/encoder/text.cjs +35 -0
- package/dist/cjs/encoder/text.cjs.map +1 -0
- package/dist/cjs/encoder/url.cjs +39 -0
- package/dist/cjs/encoder/url.cjs.map +1 -0
- package/dist/cjs/errors/index.cjs +104 -0
- package/dist/cjs/errors/index.cjs.map +1 -0
- package/dist/cjs/errors/index.d.cts +47 -0
- package/dist/cjs/index.cjs +42 -0
- package/dist/cjs/index.d.cts +14 -0
- package/dist/cjs/pin/client.cjs +96 -0
- package/dist/cjs/pin/client.cjs.map +1 -0
- package/dist/cjs/pin/index.cjs +1 -0
- package/dist/cjs/pinner.cjs +126 -0
- package/dist/cjs/pinner.cjs.map +1 -0
- package/dist/cjs/pinner.d.cts +77 -0
- package/dist/cjs/types/constants.cjs +34 -0
- package/dist/cjs/types/constants.cjs.map +1 -0
- package/dist/cjs/types/mime-types.cjs +11 -0
- package/dist/cjs/types/mime-types.cjs.map +1 -0
- package/dist/cjs/types/mime-types.d.cts +7 -0
- package/dist/cjs/types/pin.d.cts +74 -0
- package/dist/cjs/types/pinata.d.cts +99 -0
- package/dist/cjs/types/type-guards.cjs +20 -0
- package/dist/cjs/types/type-guards.cjs.map +1 -0
- package/dist/cjs/types/type-guards.d.cts +15 -0
- package/dist/cjs/types/upload.cjs +18 -0
- package/dist/cjs/types/upload.cjs.map +1 -0
- package/dist/cjs/types/upload.d.cts +189 -0
- package/dist/cjs/upload/base-upload.cjs +135 -0
- package/dist/cjs/upload/base-upload.cjs.map +1 -0
- package/dist/cjs/upload/builder.cjs +174 -0
- package/dist/cjs/upload/builder.cjs.map +1 -0
- package/dist/cjs/upload/builder.d.cts +60 -0
- package/dist/cjs/upload/car.cjs +129 -0
- package/dist/cjs/upload/car.cjs.map +1 -0
- package/dist/cjs/upload/car.d.cts +19 -0
- package/dist/cjs/upload/constants.cjs +9 -0
- package/dist/cjs/upload/constants.cjs.map +1 -0
- package/dist/cjs/upload/index.cjs +8 -0
- package/dist/cjs/upload/manager.cjs +249 -0
- package/dist/cjs/upload/manager.cjs.map +1 -0
- package/dist/cjs/upload/manager.d.cts +35 -0
- package/dist/cjs/upload/normalize.cjs +28 -0
- package/dist/cjs/upload/normalize.cjs.map +1 -0
- package/dist/cjs/upload/tus-upload.cjs +74 -0
- package/dist/cjs/upload/tus-upload.cjs.map +1 -0
- package/dist/cjs/upload/xhr-upload.cjs +41 -0
- package/dist/cjs/upload/xhr-upload.cjs.map +1 -0
- package/dist/cjs/utils/env.cjs +12 -0
- package/dist/cjs/utils/env.cjs.map +1 -0
- package/dist/cjs/utils/stream.cjs +141 -0
- package/dist/cjs/utils/stream.cjs.map +1 -0
- package/dist/cjs/utils/stream.d.cts +23 -0
- package/dist/cjs/utils/tus-patch.cjs +50 -0
- package/dist/cjs/utils/tus-patch.cjs.map +1 -0
- package/dist/cjs/utils/validation.cjs +62 -0
- package/dist/cjs/utils/validation.cjs.map +1 -0
- package/dist/esm/_virtual/rolldown_runtime.js +8 -0
- package/dist/esm/adapters/pinata/adapter.d.ts +35 -0
- package/dist/esm/adapters/pinata/adapter.js +87 -0
- package/dist/esm/adapters/pinata/adapter.js.map +1 -0
- package/dist/esm/adapters/pinata/builder.d.ts +1 -0
- package/dist/esm/adapters/pinata/builder.js +187 -0
- package/dist/esm/adapters/pinata/builder.js.map +1 -0
- package/dist/esm/adapters/pinata/index.d.ts +4 -0
- package/dist/esm/adapters/pinata/index.js +3 -0
- package/dist/esm/adapters/pinata/list-builder.d.ts +1 -0
- package/dist/esm/adapters/pinata/list-builder.js +51 -0
- package/dist/esm/adapters/pinata/list-builder.js.map +1 -0
- package/dist/esm/blockstore/index.d.ts +2 -0
- package/dist/esm/blockstore/index.js +2 -0
- package/dist/esm/blockstore/unstorage-base.d.ts +23 -0
- package/dist/esm/blockstore/unstorage-base.js +231 -0
- package/dist/esm/blockstore/unstorage-base.js.map +1 -0
- package/dist/esm/blockstore/unstorage.d.ts +36 -0
- package/dist/esm/blockstore/unstorage.js +38 -0
- package/dist/esm/blockstore/unstorage.js.map +1 -0
- package/dist/esm/config.d.ts +51 -0
- package/dist/esm/encoder/base64.js +37 -0
- package/dist/esm/encoder/base64.js.map +1 -0
- package/dist/esm/encoder/csv/csv-formatter.js +81 -0
- package/dist/esm/encoder/csv/csv-formatter.js.map +1 -0
- package/dist/esm/encoder/csv/field-formatter.js +75 -0
- package/dist/esm/encoder/csv/field-formatter.js.map +1 -0
- package/dist/esm/encoder/csv/row-formatter.js +159 -0
- package/dist/esm/encoder/csv/row-formatter.js.map +1 -0
- package/dist/esm/encoder/csv.js +43 -0
- package/dist/esm/encoder/csv.js.map +1 -0
- package/dist/esm/encoder/error.js +18 -0
- package/dist/esm/encoder/error.js.map +1 -0
- package/dist/esm/encoder/index.js +6 -0
- package/dist/esm/encoder/json.js +35 -0
- package/dist/esm/encoder/json.js.map +1 -0
- package/dist/esm/encoder/text.js +34 -0
- package/dist/esm/encoder/text.js.map +1 -0
- package/dist/esm/encoder/url.js +36 -0
- package/dist/esm/encoder/url.js.map +1 -0
- package/dist/esm/errors/index.d.ts +47 -0
- package/dist/esm/errors/index.js +93 -0
- package/dist/esm/errors/index.js.map +1 -0
- package/dist/esm/index.d.ts +16 -0
- package/dist/esm/index.js +14 -0
- package/dist/esm/pin/client.js +95 -0
- package/dist/esm/pin/client.js.map +1 -0
- package/dist/esm/pin/index.js +1 -0
- package/dist/esm/pinner.d.ts +77 -0
- package/dist/esm/pinner.js +125 -0
- package/dist/esm/pinner.js.map +1 -0
- package/dist/esm/types/constants.js +29 -0
- package/dist/esm/types/constants.js.map +1 -0
- package/dist/esm/types/mime-types.d.ts +7 -0
- package/dist/esm/types/mime-types.js +8 -0
- package/dist/esm/types/mime-types.js.map +1 -0
- package/dist/esm/types/pin.d.ts +74 -0
- package/dist/esm/types/pinata.d.ts +99 -0
- package/dist/esm/types/type-guards.d.ts +15 -0
- package/dist/esm/types/type-guards.js +19 -0
- package/dist/esm/types/type-guards.js.map +1 -0
- package/dist/esm/types/upload.d.ts +189 -0
- package/dist/esm/types/upload.js +16 -0
- package/dist/esm/types/upload.js.map +1 -0
- package/dist/esm/upload/base-upload.js +132 -0
- package/dist/esm/upload/base-upload.js.map +1 -0
- package/dist/esm/upload/builder.d.ts +60 -0
- package/dist/esm/upload/builder.js +173 -0
- package/dist/esm/upload/builder.js.map +1 -0
- package/dist/esm/upload/car.d.ts +19 -0
- package/dist/esm/upload/car.js +125 -0
- package/dist/esm/upload/car.js.map +1 -0
- package/dist/esm/upload/constants.js +7 -0
- package/dist/esm/upload/constants.js.map +1 -0
- package/dist/esm/upload/index.js +8 -0
- package/dist/esm/upload/manager.d.ts +35 -0
- package/dist/esm/upload/manager.js +248 -0
- package/dist/esm/upload/manager.js.map +1 -0
- package/dist/esm/upload/normalize.js +28 -0
- package/dist/esm/upload/normalize.js.map +1 -0
- package/dist/esm/upload/tus-upload.js +72 -0
- package/dist/esm/upload/tus-upload.js.map +1 -0
- package/dist/esm/upload/xhr-upload.js +39 -0
- package/dist/esm/upload/xhr-upload.js.map +1 -0
- package/dist/esm/utils/env.js +11 -0
- package/dist/esm/utils/env.js.map +1 -0
- package/dist/esm/utils/stream.d.ts +23 -0
- package/dist/esm/utils/stream.js +134 -0
- package/dist/esm/utils/stream.js.map +1 -0
- package/dist/esm/utils/tus-patch.js +51 -0
- package/dist/esm/utils/tus-patch.js.map +1 -0
- package/dist/esm/utils/validation.js +60 -0
- package/dist/esm/utils/validation.js.map +1 -0
- package/package.json +95 -8
- package/public/mockServiceWorker.js +349 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { jsonToFile } from "../encoder/json.js";
|
|
2
|
+
import { base64ToFile } from "../encoder/base64.js";
|
|
3
|
+
import { urlToFile } from "../encoder/url.js";
|
|
4
|
+
import { csvToFile } from "../encoder/csv.js";
|
|
5
|
+
import { textToFile } from "../encoder/text.js";
|
|
6
|
+
import "../encoder/index.js";
|
|
7
|
+
import { validateUrl } from "../utils/validation.js";
|
|
8
|
+
|
|
9
|
+
//#region src/upload/builder.ts
|
|
10
|
+
/**
|
|
11
|
+
* Base upload builder with common name/keyvalues functionality.
|
|
12
|
+
*/
|
|
13
|
+
var BaseUploadBuilder = class {
|
|
14
|
+
_name;
|
|
15
|
+
_keyvalues;
|
|
16
|
+
_waitForOperation;
|
|
17
|
+
_operationPollingOptions;
|
|
18
|
+
constructor(pinner) {
|
|
19
|
+
this.pinner = pinner;
|
|
20
|
+
}
|
|
21
|
+
name(name) {
|
|
22
|
+
this._name = name;
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
keyvalues(kv) {
|
|
26
|
+
this._keyvalues = kv;
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
waitForOperation(wait = true) {
|
|
30
|
+
this._waitForOperation = wait;
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
operationPollingOptions(options) {
|
|
34
|
+
this._operationPollingOptions = options;
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
buildOptions() {
|
|
38
|
+
const options = {};
|
|
39
|
+
if (this._name !== void 0) options.name = this._name;
|
|
40
|
+
if (this._keyvalues !== void 0) options.keyvalues = this._keyvalues;
|
|
41
|
+
if (this._waitForOperation !== void 0) options.waitForOperation = this._waitForOperation;
|
|
42
|
+
if (this._operationPollingOptions !== void 0) options.operationPollingOptions = this._operationPollingOptions;
|
|
43
|
+
return options;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* File upload builder.
|
|
48
|
+
*/
|
|
49
|
+
var FileUploadBuilder = class extends BaseUploadBuilder {
|
|
50
|
+
constructor(pinner, file) {
|
|
51
|
+
super(pinner);
|
|
52
|
+
this.file = file;
|
|
53
|
+
}
|
|
54
|
+
async pin() {
|
|
55
|
+
return this.pinner.upload(this.file, this.buildOptions());
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Encoded upload builder - handles JSON, Base64, and URL uploads using encoders.
|
|
60
|
+
*/
|
|
61
|
+
var EncodedUploadBuilder = class extends BaseUploadBuilder {
|
|
62
|
+
constructor(pinner, encoderFn) {
|
|
63
|
+
super(pinner);
|
|
64
|
+
this.encoderFn = encoderFn;
|
|
65
|
+
}
|
|
66
|
+
async pin() {
|
|
67
|
+
const encoded = await this.encoderFn(this._name, this._keyvalues);
|
|
68
|
+
return this.pinner.upload(encoded.file, encoded.options);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Raw CAR upload builder - handles raw CAR file uploads without preprocessing.
|
|
73
|
+
*/
|
|
74
|
+
var RawUploadBuilder = class extends BaseUploadBuilder {
|
|
75
|
+
constructor(pinner, carInput) {
|
|
76
|
+
super(pinner);
|
|
77
|
+
this.carInput = carInput;
|
|
78
|
+
}
|
|
79
|
+
async pin() {
|
|
80
|
+
return this.pinner.uploadCar(this.carInput, this.buildOptions());
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Pinner upload builder namespace.
|
|
85
|
+
* Provides fluent API for file, JSON, Base64, and URL uploads.
|
|
86
|
+
*/
|
|
87
|
+
var UploadBuilderNamespace = class {
|
|
88
|
+
constructor(pinner) {
|
|
89
|
+
this.pinner = pinner;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Upload a file.
|
|
93
|
+
* Returns a builder for chaining name/keyvalues.
|
|
94
|
+
*/
|
|
95
|
+
file(file) {
|
|
96
|
+
return new FileUploadBuilder(this.pinner, file);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Upload JSON data (encoded as JSON file).
|
|
100
|
+
* Returns a builder for chaining name/keyvalues.
|
|
101
|
+
*/
|
|
102
|
+
json(data) {
|
|
103
|
+
return new EncodedUploadBuilder(this.pinner, (name, keyvalues) => jsonToFile(data, {
|
|
104
|
+
name,
|
|
105
|
+
keyvalues
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Upload Base64 encoded content (decoded and uploaded as file).
|
|
110
|
+
* Returns a builder for chaining name/keyvalues.
|
|
111
|
+
*/
|
|
112
|
+
base64(base64String) {
|
|
113
|
+
return new EncodedUploadBuilder(this.pinner, (name, keyvalues) => base64ToFile(base64String, {
|
|
114
|
+
name,
|
|
115
|
+
keyvalues
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Upload content from a URL (fetched and uploaded as file).
|
|
120
|
+
* Returns a builder for chaining name/keyvalues.
|
|
121
|
+
*/
|
|
122
|
+
url(urlString) {
|
|
123
|
+
validateUrl(urlString);
|
|
124
|
+
return new EncodedUploadBuilder(this.pinner, (name, keyvalues) => urlToFile(urlString, {
|
|
125
|
+
name,
|
|
126
|
+
keyvalues
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Upload CSV data (string, array of objects, or array of arrays).
|
|
131
|
+
* Returns a builder for chaining name/keyvalues.
|
|
132
|
+
*/
|
|
133
|
+
csv(data) {
|
|
134
|
+
return new EncodedUploadBuilder(this.pinner, (name, keyvalues) => csvToFile(data, {
|
|
135
|
+
name,
|
|
136
|
+
keyvalues
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Upload raw CAR data without preprocessing.
|
|
141
|
+
* Useful for passthrough of pre-generated CAR files.
|
|
142
|
+
* Returns a builder for chaining name/keyvalues.
|
|
143
|
+
*/
|
|
144
|
+
raw(carInput) {
|
|
145
|
+
return new RawUploadBuilder(this.pinner, carInput);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Upload text content (encoded as text file).
|
|
149
|
+
* Returns a builder for chaining name/keyvalues.
|
|
150
|
+
*/
|
|
151
|
+
text(textData) {
|
|
152
|
+
return new EncodedUploadBuilder(this.pinner, (name, keyvalues) => textToFile(textData, {
|
|
153
|
+
name,
|
|
154
|
+
keyvalues
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Alias for text() - upload text content.
|
|
159
|
+
*/
|
|
160
|
+
get content() {
|
|
161
|
+
return (textData) => this.text(textData);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Create a Pinner upload builder namespace.
|
|
166
|
+
*/
|
|
167
|
+
function createUploadBuilderNamespace(pinner) {
|
|
168
|
+
return new UploadBuilderNamespace(pinner);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
//#endregion
|
|
172
|
+
export { UploadBuilderNamespace, createUploadBuilderNamespace };
|
|
173
|
+
//# sourceMappingURL=builder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builder.js","names":[],"sources":["../../../src/upload/builder.ts"],"sourcesContent":["import type { Pinner } from \"@/pinner\";\nimport type {\n PinnerUploadBuilder,\n UploadOperation,\n UploadOptions,\n} from \"@/types/upload\";\nimport type { OperationPollingOptions } from \"@lumeweb/portal-sdk\";\nimport {\n base64ToFile,\n csvToFile,\n jsonToFile,\n textToFile,\n urlToFile,\n} from \"@/encoder\";\nimport { validateUrl } from \"@/utils/validation\";\n\n/**\n * Base upload builder with common name/keyvalues functionality.\n */\nabstract class BaseUploadBuilder implements PinnerUploadBuilder {\n protected _name?: string;\n protected _keyvalues?: Record<string, string>;\n protected _waitForOperation?: boolean;\n protected _operationPollingOptions?: OperationPollingOptions;\n\n constructor(protected pinner: Pinner) {}\n\n name(name: string): this {\n this._name = name;\n return this;\n }\n\n keyvalues(kv: Record<string, string>): this {\n this._keyvalues = kv;\n return this;\n }\n\n waitForOperation(wait: boolean = true): this {\n this._waitForOperation = wait;\n return this;\n }\n\n operationPollingOptions(options: OperationPollingOptions): this {\n this._operationPollingOptions = options;\n return this;\n }\n\n abstract pin(): Promise<UploadOperation>;\n\n protected buildOptions(): UploadOptions {\n const options: UploadOptions = {};\n if (this._name !== undefined) {\n options.name = this._name;\n }\n if (this._keyvalues !== undefined) {\n options.keyvalues = this._keyvalues;\n }\n if (this._waitForOperation !== undefined) {\n options.waitForOperation = this._waitForOperation;\n }\n if (this._operationPollingOptions !== undefined) {\n options.operationPollingOptions = this._operationPollingOptions;\n }\n return options;\n }\n}\n\n/**\n * File upload builder.\n */\nclass FileUploadBuilder extends BaseUploadBuilder {\n constructor(\n pinner: Pinner,\n private file: File,\n ) {\n super(pinner);\n }\n\n async pin(): Promise<UploadOperation> {\n return this.pinner.upload(this.file, this.buildOptions());\n }\n}\n\n/**\n * Encoded upload builder - handles JSON, Base64, and URL uploads using encoders.\n */\nclass EncodedUploadBuilder extends BaseUploadBuilder {\n constructor(\n pinner: Pinner,\n private encoderFn: (\n name?: string,\n keyvalues?: Record<string, string>,\n ) => Promise<{ file: File; options: UploadOptions }>,\n ) {\n super(pinner);\n }\n\n async pin(): Promise<UploadOperation> {\n const encoded = await this.encoderFn(this._name, this._keyvalues);\n return this.pinner.upload(encoded.file, encoded.options);\n }\n}\n\n/**\n * Raw CAR upload builder - handles raw CAR file uploads without preprocessing.\n */\nclass RawUploadBuilder extends BaseUploadBuilder {\n constructor(\n pinner: Pinner,\n private carInput: File | ReadableStream<Uint8Array>,\n ) {\n super(pinner);\n }\n\n async pin(): Promise<UploadOperation> {\n return this.pinner.uploadCar(this.carInput, this.buildOptions());\n }\n}\n\n/**\n * Pinner upload builder namespace.\n * Provides fluent API for file, JSON, Base64, and URL uploads.\n */\nexport class UploadBuilderNamespace {\n constructor(private pinner: Pinner) {}\n\n /**\n * Upload a file.\n * Returns a builder for chaining name/keyvalues.\n */\n file(file: File): PinnerUploadBuilder {\n return new FileUploadBuilder(this.pinner, file);\n }\n\n /**\n * Upload JSON data (encoded as JSON file).\n * Returns a builder for chaining name/keyvalues.\n */\n json(data: object): PinnerUploadBuilder {\n return new EncodedUploadBuilder(this.pinner, (name, keyvalues) =>\n jsonToFile(data, { name, keyvalues }),\n );\n }\n\n /**\n * Upload Base64 encoded content (decoded and uploaded as file).\n * Returns a builder for chaining name/keyvalues.\n */\n base64(base64String: string): PinnerUploadBuilder {\n return new EncodedUploadBuilder(this.pinner, (name, keyvalues) =>\n base64ToFile(base64String, { name, keyvalues }),\n );\n }\n\n /**\n * Upload content from a URL (fetched and uploaded as file).\n * Returns a builder for chaining name/keyvalues.\n */\n url(urlString: string): PinnerUploadBuilder {\n // Validate URL to prevent SSRF attacks\n validateUrl(urlString);\n\n return new EncodedUploadBuilder(this.pinner, (name, keyvalues) =>\n urlToFile(urlString, { name, keyvalues }),\n );\n }\n\n /**\n * Upload CSV data (string, array of objects, or array of arrays).\n * Returns a builder for chaining name/keyvalues.\n */\n csv(data: string | object[] | any[][]): PinnerUploadBuilder {\n return new EncodedUploadBuilder(this.pinner, (name, keyvalues) =>\n csvToFile(data, { name, keyvalues }),\n );\n }\n\n /**\n * Upload raw CAR data without preprocessing.\n * Useful for passthrough of pre-generated CAR files.\n * Returns a builder for chaining name/keyvalues.\n */\n raw(carInput: File | ReadableStream<Uint8Array>): PinnerUploadBuilder {\n return new RawUploadBuilder(this.pinner, carInput);\n }\n\n /**\n * Upload text content (encoded as text file).\n * Returns a builder for chaining name/keyvalues.\n */\n text(textData: string): PinnerUploadBuilder {\n return new EncodedUploadBuilder(this.pinner, (name, keyvalues) =>\n textToFile(textData, { name, keyvalues }),\n );\n }\n\n /**\n * Alias for text() - upload text content.\n */\n get content(): (textData: string) => PinnerUploadBuilder {\n return (textData: string) => this.text(textData);\n }\n}\n\n/**\n * Create a Pinner upload builder namespace.\n */\nexport function createUploadBuilderNamespace(\n pinner: Pinner,\n): UploadBuilderNamespace {\n return new UploadBuilderNamespace(pinner);\n}\n\n/**\n * Combined interface for upload that works as both a method and a builder namespace.\n */\nexport type UploadMethodAndBuilder = ((\n file: File,\n options?: UploadOptions,\n) => Promise<UploadOperation>) &\n UploadBuilderNamespace;\n\n/**\n * Export PinnerUploadBuilder for external use.\n */\nexport type { PinnerUploadBuilder };\n"],"mappings":";;;;;;;;;;;;AAmBA,IAAe,oBAAf,MAAgE;CAC9D,AAAU;CACV,AAAU;CACV,AAAU;CACV,AAAU;CAEV,YAAY,AAAU,QAAgB;EAAhB;;CAEtB,KAAK,MAAoB;AACvB,OAAK,QAAQ;AACb,SAAO;;CAGT,UAAU,IAAkC;AAC1C,OAAK,aAAa;AAClB,SAAO;;CAGT,iBAAiB,OAAgB,MAAY;AAC3C,OAAK,oBAAoB;AACzB,SAAO;;CAGT,wBAAwB,SAAwC;AAC9D,OAAK,2BAA2B;AAChC,SAAO;;CAKT,AAAU,eAA8B;EACtC,MAAM,UAAyB,EAAE;AACjC,MAAI,KAAK,UAAU,OACjB,SAAQ,OAAO,KAAK;AAEtB,MAAI,KAAK,eAAe,OACtB,SAAQ,YAAY,KAAK;AAE3B,MAAI,KAAK,sBAAsB,OAC7B,SAAQ,mBAAmB,KAAK;AAElC,MAAI,KAAK,6BAA6B,OACpC,SAAQ,0BAA0B,KAAK;AAEzC,SAAO;;;;;;AAOX,IAAM,oBAAN,cAAgC,kBAAkB;CAChD,YACE,QACA,AAAQ,MACR;AACA,QAAM,OAAO;EAFL;;CAKV,MAAM,MAAgC;AACpC,SAAO,KAAK,OAAO,OAAO,KAAK,MAAM,KAAK,cAAc,CAAC;;;;;;AAO7D,IAAM,uBAAN,cAAmC,kBAAkB;CACnD,YACE,QACA,AAAQ,WAIR;AACA,QAAM,OAAO;EALL;;CAQV,MAAM,MAAgC;EACpC,MAAM,UAAU,MAAM,KAAK,UAAU,KAAK,OAAO,KAAK,WAAW;AACjE,SAAO,KAAK,OAAO,OAAO,QAAQ,MAAM,QAAQ,QAAQ;;;;;;AAO5D,IAAM,mBAAN,cAA+B,kBAAkB;CAC/C,YACE,QACA,AAAQ,UACR;AACA,QAAM,OAAO;EAFL;;CAKV,MAAM,MAAgC;AACpC,SAAO,KAAK,OAAO,UAAU,KAAK,UAAU,KAAK,cAAc,CAAC;;;;;;;AAQpE,IAAa,yBAAb,MAAoC;CAClC,YAAY,AAAQ,QAAgB;EAAhB;;;;;;CAMpB,KAAK,MAAiC;AACpC,SAAO,IAAI,kBAAkB,KAAK,QAAQ,KAAK;;;;;;CAOjD,KAAK,MAAmC;AACtC,SAAO,IAAI,qBAAqB,KAAK,SAAS,MAAM,cAClD,WAAW,MAAM;GAAE;GAAM;GAAW,CAAC,CACtC;;;;;;CAOH,OAAO,cAA2C;AAChD,SAAO,IAAI,qBAAqB,KAAK,SAAS,MAAM,cAClD,aAAa,cAAc;GAAE;GAAM;GAAW,CAAC,CAChD;;;;;;CAOH,IAAI,WAAwC;AAE1C,cAAY,UAAU;AAEtB,SAAO,IAAI,qBAAqB,KAAK,SAAS,MAAM,cAClD,UAAU,WAAW;GAAE;GAAM;GAAW,CAAC,CAC1C;;;;;;CAOH,IAAI,MAAwD;AAC1D,SAAO,IAAI,qBAAqB,KAAK,SAAS,MAAM,cAClD,UAAU,MAAM;GAAE;GAAM;GAAW,CAAC,CACrC;;;;;;;CAQH,IAAI,UAAkE;AACpE,SAAO,IAAI,iBAAiB,KAAK,QAAQ,SAAS;;;;;;CAOpD,KAAK,UAAuC;AAC1C,SAAO,IAAI,qBAAqB,KAAK,SAAS,MAAM,cAClD,WAAW,UAAU;GAAE;GAAM;GAAW,CAAC,CAC1C;;;;;CAMH,IAAI,UAAqD;AACvD,UAAQ,aAAqB,KAAK,KAAK,SAAS;;;;;;AAOpD,SAAgB,6BACd,QACwB;AACxB,QAAO,IAAI,uBAAuB,OAAO"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Datastore } from "interface-datastore";
|
|
2
|
+
|
|
3
|
+
//#region src/upload/car.d.ts
|
|
4
|
+
interface CarPreprocessOptions {
|
|
5
|
+
name?: string;
|
|
6
|
+
onProgress?: (percentage: number) => void;
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
}
|
|
9
|
+
interface CarPreprocessResult {
|
|
10
|
+
carStream: ReadableStream<Uint8Array>;
|
|
11
|
+
rootCid: string;
|
|
12
|
+
size: bigint;
|
|
13
|
+
}
|
|
14
|
+
declare function preprocessToCar(input: File | ReadableStream<Uint8Array> | File[], options?: CarPreprocessOptions): Promise<CarPreprocessResult>;
|
|
15
|
+
declare function isCarFile(file: File): Promise<boolean>;
|
|
16
|
+
declare function destroyCarPreprocessor(): Promise<void>;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { CarPreprocessOptions, CarPreprocessResult, destroyCarPreprocessor, isCarFile, preprocessToCar };
|
|
19
|
+
//# sourceMappingURL=car.d.ts.map
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { FILE_EXTENSION_CAR, MIME_TYPE_CAR } from "../types/mime-types.js";
|
|
2
|
+
import { asyncGeneratorToReadableStream, calculateStreamSize, readableStreamToAsyncIterable, streamToBlob } from "../utils/stream.js";
|
|
3
|
+
import { createBlockstore, createDatastore } from "../blockstore/unstorage.js";
|
|
4
|
+
import "../blockstore/index.js";
|
|
5
|
+
import { car } from "@helia/car";
|
|
6
|
+
import { createHeliaHTTP } from "@helia/http";
|
|
7
|
+
import { unixfs } from "@helia/unixfs";
|
|
8
|
+
import { CarReader } from "@ipld/car";
|
|
9
|
+
|
|
10
|
+
//#region src/upload/car.ts
|
|
11
|
+
let helia = null;
|
|
12
|
+
let blockstore = null;
|
|
13
|
+
let datastore = null;
|
|
14
|
+
let config = {};
|
|
15
|
+
function configureCar(carConfig) {
|
|
16
|
+
config = carConfig;
|
|
17
|
+
}
|
|
18
|
+
async function getHelia() {
|
|
19
|
+
if (helia) return helia;
|
|
20
|
+
const BlockstoreClass = createBlockstore();
|
|
21
|
+
const DatastoreClass = createDatastore();
|
|
22
|
+
blockstore = new BlockstoreClass({
|
|
23
|
+
prefix: "pinner-helia-blocks",
|
|
24
|
+
base: config.datastoreName
|
|
25
|
+
});
|
|
26
|
+
datastore = config.datastore || new DatastoreClass({
|
|
27
|
+
prefix: "pinner-helia-data",
|
|
28
|
+
base: config.datastoreName
|
|
29
|
+
});
|
|
30
|
+
helia = await createHeliaHTTP({
|
|
31
|
+
blockstore,
|
|
32
|
+
datastore
|
|
33
|
+
});
|
|
34
|
+
return helia;
|
|
35
|
+
}
|
|
36
|
+
async function cleanupHelia() {
|
|
37
|
+
if (datastore?.close) await datastore.close();
|
|
38
|
+
helia = null;
|
|
39
|
+
blockstore = null;
|
|
40
|
+
datastore = null;
|
|
41
|
+
}
|
|
42
|
+
async function* fileSource(files, onProgress, signal) {
|
|
43
|
+
const seenDirs = /* @__PURE__ */ new Set();
|
|
44
|
+
let totalBytes = 0n;
|
|
45
|
+
let processedBytes = 0n;
|
|
46
|
+
for (const file of files) totalBytes += BigInt(file.size);
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
if (signal?.aborted) throw new Error("Aborted");
|
|
49
|
+
const fullPath = file.webkitRelativePath ?? file.name;
|
|
50
|
+
if (fullPath.includes("/.")) continue;
|
|
51
|
+
const parts = fullPath.split("/").filter((part) => part.length > 0);
|
|
52
|
+
for (let i = 1; i < parts.length; i++) {
|
|
53
|
+
if (signal?.aborted) throw new Error("Aborted");
|
|
54
|
+
const dirPath = parts.slice(0, i).join("/");
|
|
55
|
+
if (!seenDirs.has(dirPath)) {
|
|
56
|
+
seenDirs.add(dirPath);
|
|
57
|
+
yield {
|
|
58
|
+
content: (async function* () {})(),
|
|
59
|
+
path: dirPath
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
yield {
|
|
64
|
+
content: readableStreamToAsyncIterable(file.stream()),
|
|
65
|
+
path: fullPath
|
|
66
|
+
};
|
|
67
|
+
if (onProgress && totalBytes > 0n) {
|
|
68
|
+
processedBytes += BigInt(file.size);
|
|
69
|
+
onProgress(Number(processedBytes * 100n / totalBytes));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function preprocessToCar(input, options) {
|
|
74
|
+
let files;
|
|
75
|
+
if (input instanceof ReadableStream) {
|
|
76
|
+
const [streamForSize$1, streamForFile] = input.tee();
|
|
77
|
+
await calculateStreamSize(streamForSize$1, options?.signal);
|
|
78
|
+
const streamBlob = await streamToBlob(streamForFile, "application/octet-stream");
|
|
79
|
+
files = [new File([streamBlob], options?.name || "upload", { type: streamBlob.type })];
|
|
80
|
+
} else if (Array.isArray(input)) files = input;
|
|
81
|
+
else files = [input];
|
|
82
|
+
const heliaInstance = await getHelia();
|
|
83
|
+
const fs = unixfs(heliaInstance);
|
|
84
|
+
const c = car(heliaInstance);
|
|
85
|
+
let rootCid;
|
|
86
|
+
let blocksCount = 0n;
|
|
87
|
+
const src = fileSource(files, options?.onProgress, options?.signal);
|
|
88
|
+
let hasFiles = false;
|
|
89
|
+
for await (const result of fs.addAll(src, {
|
|
90
|
+
cidVersion: 1,
|
|
91
|
+
rawLeaves: false,
|
|
92
|
+
signal: options?.signal,
|
|
93
|
+
onProgress(event) {
|
|
94
|
+
if (event.type === "blocks:put:blockstore:put") blocksCount++;
|
|
95
|
+
}
|
|
96
|
+
})) {
|
|
97
|
+
if (options?.signal?.aborted) throw new Error("Aborted");
|
|
98
|
+
rootCid = result.cid;
|
|
99
|
+
hasFiles = true;
|
|
100
|
+
}
|
|
101
|
+
if (!hasFiles || !rootCid) throw new Error("No files to process");
|
|
102
|
+
const [streamForSize, streamForProcessing] = asyncGeneratorToReadableStream(c.export(rootCid, { signal: options?.signal })).tee();
|
|
103
|
+
const size = await calculateStreamSize(streamForSize, options?.signal);
|
|
104
|
+
return {
|
|
105
|
+
carStream: streamForProcessing,
|
|
106
|
+
rootCid: rootCid.toString(),
|
|
107
|
+
size
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async function isCarFile(file) {
|
|
111
|
+
if (file.type !== MIME_TYPE_CAR && !file.name.endsWith(FILE_EXTENSION_CAR)) return false;
|
|
112
|
+
try {
|
|
113
|
+
const iterable = readableStreamToAsyncIterable(file.stream());
|
|
114
|
+
return (await (await CarReader.fromIterable(iterable)).getRoots()).length > 0;
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function destroyCarPreprocessor() {
|
|
120
|
+
await cleanupHelia();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
//#endregion
|
|
124
|
+
export { configureCar, destroyCarPreprocessor, isCarFile, preprocessToCar };
|
|
125
|
+
//# sourceMappingURL=car.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"car.js","names":["createUnstorageDatastore","streamForSize"],"sources":["../../../src/upload/car.ts"],"sourcesContent":["import { car } from \"@helia/car\";\nimport { createHeliaHTTP } from \"@helia/http\";\nimport { unixfs } from \"@helia/unixfs\";\nimport {\n createBlockstore,\n createDatastore as createUnstorageDatastore,\n} from \"@/blockstore\";\nimport type { CID } from \"multiformats/cid\";\nimport { CarReader } from \"@ipld/car\";\nimport type { Datastore } from \"interface-datastore\";\n\nimport {\n asyncGeneratorToReadableStream,\n calculateStreamSize,\n readableStreamToAsyncIterable,\n streamToBlob,\n} from \"@/utils/stream\";\nimport { FILE_EXTENSION_CAR, MIME_TYPE_CAR } from \"@/types/mime-types\";\n\nexport interface CarPreprocessOptions {\n name?: string;\n onProgress?: (percentage: number) => void;\n signal?: AbortSignal;\n}\n\nexport interface CarPreprocessResult {\n carStream: ReadableStream<Uint8Array>;\n rootCid: string;\n size: bigint;\n}\n\nexport interface CarConfig {\n /**\n * Custom datastore instance for Helia.\n * If provided, this datastore will be used directly without creating one from storage.\n * Highest priority - takes precedence over storage and datastoreName.\n */\n datastore?: Datastore;\n\n /**\n * Custom base name for Helia storage.\n * Passed as the `base` option to both blockstore and datastore storage instances.\n * Only used when datastore is not provided.\n * @default \"pinner-helia-data\"\n */\n datastoreName?: string;\n}\n\nlet helia: any = null;\nlet blockstore: any = null;\nlet datastore: any = null;\nlet config: CarConfig = {};\n\nexport function configureCar(carConfig: CarConfig) {\n config = carConfig;\n}\n\nasync function getHelia() {\n if (helia) return helia;\n\n const BlockstoreClass = createBlockstore();\n const DatastoreClass = createUnstorageDatastore();\n\n blockstore = new BlockstoreClass({\n prefix: \"pinner-helia-blocks\",\n base: config.datastoreName,\n });\n datastore =\n config.datastore ||\n new DatastoreClass({\n prefix: \"pinner-helia-data\",\n base: config.datastoreName,\n });\n\n helia = await createHeliaHTTP({\n blockstore,\n datastore,\n });\n\n return helia;\n}\n\nasync function cleanupHelia() {\n if (datastore?.close) {\n await datastore.close();\n }\n helia = null;\n blockstore = null;\n datastore = null;\n}\n\nasync function* fileSource(\n files: File[],\n onProgress?: (percentage: number) => void,\n signal?: AbortSignal,\n): AsyncGenerator<{\n content: AsyncIterable<Uint8Array> | undefined;\n path: string;\n}> {\n const seenDirs = new Set<string>();\n let totalBytes = 0n;\n let processedBytes = 0n;\n\n for (const file of files) {\n totalBytes += BigInt(file.size);\n }\n\n for (const file of files) {\n if (signal?.aborted) {\n throw new Error(\"Aborted\");\n }\n\n const fullPath = (file as any).webkitRelativePath ?? file.name;\n\n if (fullPath.includes(\"/.\")) {\n continue;\n }\n\n const parts = fullPath.split(\"/\").filter((part: string) => part.length > 0);\n\n for (let i = 1; i < parts.length; i++) {\n if (signal?.aborted) {\n throw new Error(\"Aborted\");\n }\n\n const dirPath = parts.slice(0, i).join(\"/\");\n\n if (!seenDirs.has(dirPath)) {\n seenDirs.add(dirPath);\n yield {\n content: (async function* () {})(),\n path: dirPath,\n };\n }\n }\n\n yield {\n content: readableStreamToAsyncIterable(file.stream()),\n path: fullPath,\n };\n\n if (onProgress && totalBytes > 0n) {\n processedBytes += BigInt(file.size);\n const progressPercent = Number((processedBytes * 100n) / totalBytes);\n onProgress(progressPercent);\n }\n }\n}\n\nexport async function preprocessToCar(\n input: File | ReadableStream<Uint8Array> | File[],\n options?: CarPreprocessOptions,\n): Promise<CarPreprocessResult> {\n let files: File[];\n\n if (input instanceof ReadableStream) {\n const [streamForSize, streamForFile] = input.tee();\n const size = await calculateStreamSize(streamForSize, options?.signal);\n const streamBlob = await streamToBlob(\n streamForFile,\n \"application/octet-stream\",\n );\n files = [\n new File([streamBlob], options?.name || \"upload\", {\n type: streamBlob.type,\n }),\n ];\n } else if (Array.isArray(input)) {\n files = input;\n } else {\n files = [input];\n }\n\n const heliaInstance = await getHelia();\n const fs = unixfs(heliaInstance);\n const c = car(heliaInstance);\n\n let rootCid: CID | undefined;\n let blocksCount = 0n;\n\n const src = fileSource(files, options?.onProgress, options?.signal);\n\n let hasFiles = false;\n for await (const result of fs.addAll(src, {\n cidVersion: 1,\n rawLeaves: false,\n signal: options?.signal,\n onProgress(event) {\n if (event.type === \"blocks:put:blockstore:put\") {\n blocksCount++;\n }\n },\n })) {\n if (options?.signal?.aborted) {\n throw new Error(\"Aborted\");\n }\n rootCid = result.cid;\n hasFiles = true;\n }\n\n if (!hasFiles || !rootCid) {\n throw new Error(\"No files to process\");\n }\n\n // c.export() now returns an async generator directly (was renamed from 'stream')\n const carAsyncGenerator = c.export(rootCid!, { signal: options?.signal });\n const carStream = asyncGeneratorToReadableStream(carAsyncGenerator);\n\n // Use stream tee to create two identical streams - one for size calculation, one for processing\n const [streamForSize, streamForProcessing] = carStream.tee();\n\n const size = await calculateStreamSize(streamForSize, options?.signal);\n\n return {\n carStream: streamForProcessing,\n rootCid: rootCid!.toString(),\n size,\n };\n}\n\nexport async function isCarFile(file: File): Promise<boolean> {\n if (file.type !== MIME_TYPE_CAR && !file.name.endsWith(FILE_EXTENSION_CAR)) {\n return false;\n }\n\n try {\n const iterable = readableStreamToAsyncIterable(file.stream());\n const reader = await CarReader.fromIterable(iterable);\n const roots = await reader.getRoots();\n return roots.length > 0;\n } catch {\n return false;\n }\n}\n\nexport async function destroyCarPreprocessor() {\n await cleanupHelia();\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.js","names":[],"sources":["../../../src/upload/constants.ts"],"sourcesContent":["export const UPLOAD_SOURCE_XHR = \"xhr-upload\" as const;\nexport const UPLOAD_SOURCE_TUS = \"tus-upload\" as const;\n"],"mappings":";AAAA,MAAa,oBAAoB;AACjC,MAAa,oBAAoB"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { UploadResultSymbol, isUploadResult } from "../types/upload.js";
|
|
2
|
+
import { normalizeUploadInput } from "./normalize.js";
|
|
3
|
+
import { BaseUploadHandler } from "./base-upload.js";
|
|
4
|
+
import { XHRUploadHandler } from "./xhr-upload.js";
|
|
5
|
+
import { TUSUploadHandler } from "./tus-upload.js";
|
|
6
|
+
import { configureCar, destroyCarPreprocessor, isCarFile, preprocessToCar } from "./car.js";
|
|
7
|
+
import { UploadManager } from "./manager.js";
|
|
8
|
+
import { UploadBuilderNamespace, createUploadBuilderNamespace } from "./builder.js";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { PinnerConfig } from "../config.js";
|
|
2
|
+
import { UploadInput, UploadOperation, UploadOptions, UploadResult } from "../types/upload.js";
|
|
3
|
+
import { OperationPollingOptions } from "@lumeweb/portal-sdk";
|
|
4
|
+
|
|
5
|
+
//#region src/upload/manager.d.ts
|
|
6
|
+
declare class UploadManager {
|
|
7
|
+
#private;
|
|
8
|
+
private xhrHandler;
|
|
9
|
+
private tusHandler;
|
|
10
|
+
private portalSdk;
|
|
11
|
+
private uploadLimit;
|
|
12
|
+
private limitFetched;
|
|
13
|
+
constructor(config: PinnerConfig);
|
|
14
|
+
fetchUploadLimit(): Promise<number>;
|
|
15
|
+
getUploadLimit(): number;
|
|
16
|
+
upload(input: UploadInput, options?: UploadOptions): Promise<UploadOperation>;
|
|
17
|
+
uploadCar(input: File | ReadableStream<Uint8Array>, options?: UploadOptions): Promise<UploadOperation>;
|
|
18
|
+
/**
|
|
19
|
+
* Wait for an operation to complete or reach a settled state.
|
|
20
|
+
*
|
|
21
|
+
* Handles two scenarios:
|
|
22
|
+
* 1. If an operationId is provided (in UploadResult), uses it directly
|
|
23
|
+
* 2. If only CID is available, lists operations filtered by CID and polls the first result
|
|
24
|
+
*
|
|
25
|
+
* @param input Either an operation ID (number) or an UploadResult
|
|
26
|
+
* @param options Polling options (interval, timeout, settledStates)
|
|
27
|
+
* @returns UploadResult with operation status merged in
|
|
28
|
+
*/
|
|
29
|
+
waitForOperation(input: number | UploadResult, options?: OperationPollingOptions): Promise<UploadResult>;
|
|
30
|
+
uploadDirectory(files: File[], options?: UploadOptions): Promise<UploadOperation>;
|
|
31
|
+
destroy(): void;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { UploadManager };
|
|
35
|
+
//# sourceMappingURL=manager.d.ts.map
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { isUploadResult } from "../types/upload.js";
|
|
2
|
+
import { FILE_EXTENSION_CAR, MIME_TYPE_CAR, MIME_TYPE_OCTET_STREAM } from "../types/mime-types.js";
|
|
3
|
+
import { calculateStreamSize, streamToBlob } from "../utils/stream.js";
|
|
4
|
+
import { XHRUploadHandler } from "./xhr-upload.js";
|
|
5
|
+
import { TUSUploadHandler } from "./tus-upload.js";
|
|
6
|
+
import { DEFAULT_ENDPOINT, TUS_SIZE_THRESHOLD } from "../types/constants.js";
|
|
7
|
+
import { configureCar, destroyCarPreprocessor, isCarFile, preprocessToCar } from "./car.js";
|
|
8
|
+
import { EmptyFileError } from "../errors/index.js";
|
|
9
|
+
import { OPERATION_STATUS, Sdk } from "@lumeweb/portal-sdk";
|
|
10
|
+
import { createEqFilter } from "@lumeweb/query-builder";
|
|
11
|
+
|
|
12
|
+
//#region src/upload/manager.ts
|
|
13
|
+
var UploadManager = class {
|
|
14
|
+
xhrHandler;
|
|
15
|
+
tusHandler;
|
|
16
|
+
portalSdk;
|
|
17
|
+
uploadLimit = TUS_SIZE_THRESHOLD;
|
|
18
|
+
limitFetched = false;
|
|
19
|
+
constructor(config) {
|
|
20
|
+
this.xhrHandler = new XHRUploadHandler(config);
|
|
21
|
+
this.tusHandler = new TUSUploadHandler(config);
|
|
22
|
+
this.portalSdk = new Sdk(config.endpoint || DEFAULT_ENDPOINT);
|
|
23
|
+
configureCar({
|
|
24
|
+
datastoreName: config.datastoreName,
|
|
25
|
+
datastore: config.datastore
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
async fetchUploadLimit() {
|
|
29
|
+
if (this.limitFetched) return this.uploadLimit;
|
|
30
|
+
try {
|
|
31
|
+
const result = await this.portalSdk.account().uploadLimit();
|
|
32
|
+
if (result.success && result.data?.limit) this.uploadLimit = result.data.limit;
|
|
33
|
+
} catch {
|
|
34
|
+
this.uploadLimit = TUS_SIZE_THRESHOLD;
|
|
35
|
+
}
|
|
36
|
+
this.limitFetched = true;
|
|
37
|
+
return this.uploadLimit;
|
|
38
|
+
}
|
|
39
|
+
getUploadLimit() {
|
|
40
|
+
return this.uploadLimit;
|
|
41
|
+
}
|
|
42
|
+
async upload(input, options) {
|
|
43
|
+
this.#validateInput(input, options);
|
|
44
|
+
return this.#uploadInput(input, options);
|
|
45
|
+
}
|
|
46
|
+
async uploadCar(input, options) {
|
|
47
|
+
this.#validateInput(input, options);
|
|
48
|
+
return this.#uploadCarFile(input, options);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Wait for an operation to complete or reach a settled state.
|
|
52
|
+
*
|
|
53
|
+
* Handles two scenarios:
|
|
54
|
+
* 1. If an operationId is provided (in UploadResult), uses it directly
|
|
55
|
+
* 2. If only CID is available, lists operations filtered by CID and polls the first result
|
|
56
|
+
*
|
|
57
|
+
* @param input Either an operation ID (number) or an UploadResult
|
|
58
|
+
* @param options Polling options (interval, timeout, settledStates)
|
|
59
|
+
* @returns UploadResult with operation status merged in
|
|
60
|
+
*/
|
|
61
|
+
async waitForOperation(input, options) {
|
|
62
|
+
if (isUploadResult(input) && input.operationId) {
|
|
63
|
+
const result = await this.portalSdk.account().waitForOperation(input.operationId, options);
|
|
64
|
+
if (!result.success) throw new Error(result.error?.message || `Operation ${input.operationId} failed`);
|
|
65
|
+
const operation = result.data;
|
|
66
|
+
if (!operation.cid) throw new Error(`Operation ${input.operationId} completed without CID`);
|
|
67
|
+
if (operation.status?.toLowerCase() === OPERATION_STATUS.FAILED || operation.status?.toLowerCase() === OPERATION_STATUS.ERROR) throw new Error(`Operation ${input.operationId} failed: ${operation.error || operation.status_message || "Unknown error"}`);
|
|
68
|
+
return {
|
|
69
|
+
...input,
|
|
70
|
+
cid: operation.cid,
|
|
71
|
+
operationId: operation.id
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (isUploadResult(input) && input.cid) return await this.#waitForOperationByCid(input, options);
|
|
75
|
+
const operationId = typeof input === "number" ? input : void 0;
|
|
76
|
+
if (operationId) {
|
|
77
|
+
const result = await this.portalSdk.account().waitForOperation(operationId, options);
|
|
78
|
+
if (!result.success) throw new Error(result.error?.message || `Operation ${operationId} failed`);
|
|
79
|
+
const operation = result.data;
|
|
80
|
+
if (!operation.cid) throw new Error(`Operation ${operationId} completed without CID`);
|
|
81
|
+
if (operation.status?.toLowerCase() === OPERATION_STATUS.FAILED || operation.status?.toLowerCase() === OPERATION_STATUS.ERROR) throw new Error(`Operation ${operationId} failed: ${operation.error || operation.status_message || "Unknown error"}`);
|
|
82
|
+
return {
|
|
83
|
+
id: operationId.toString(),
|
|
84
|
+
cid: operation.cid,
|
|
85
|
+
name: operation.operation_display_name || "Unknown",
|
|
86
|
+
size: 0,
|
|
87
|
+
mimeType: "",
|
|
88
|
+
createdAt: new Date(operation.started_at),
|
|
89
|
+
numberOfFiles: 1,
|
|
90
|
+
operationId: operation.id
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
throw new Error("No operation ID or CID provided, cannot wait for operation");
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Wait for an operation by CID.
|
|
97
|
+
*
|
|
98
|
+
* This is used when we have a CID from an upload but no operation ID.
|
|
99
|
+
* We list operations filtered by CID to find the operation ID,
|
|
100
|
+
* then use the SDK's waitForOperation for polling.
|
|
101
|
+
*
|
|
102
|
+
* @param uploadResult UploadResult with CID
|
|
103
|
+
* @param options Polling options (interval, timeout, settledStates)
|
|
104
|
+
* @returns UploadResult with operation status merged in
|
|
105
|
+
*/
|
|
106
|
+
async #waitForOperationByCid(uploadResult, options) {
|
|
107
|
+
const params = {
|
|
108
|
+
filters: [createEqFilter("cid", uploadResult.cid)],
|
|
109
|
+
pagination: {
|
|
110
|
+
start: 0,
|
|
111
|
+
end: 1
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const result = await this.portalSdk.account().listOperations(params);
|
|
115
|
+
if (!result.success) throw new Error(`Failed to find operation with CID ${uploadResult.cid}`);
|
|
116
|
+
const operation = result.data.data?.[0];
|
|
117
|
+
if (!operation) throw new Error(`Failed to find operation with CID ${uploadResult.cid}`);
|
|
118
|
+
const operationId = operation.id;
|
|
119
|
+
const waitResult = await this.portalSdk.account().waitForOperation(operationId, options);
|
|
120
|
+
if (!waitResult.success) throw new Error(waitResult.error?.message || `Operation ${operationId} failed`);
|
|
121
|
+
const finalOperation = waitResult.data;
|
|
122
|
+
if (!finalOperation.cid) throw new Error(`Operation ${operationId} completed without CID`);
|
|
123
|
+
if (finalOperation.status?.toLowerCase() === OPERATION_STATUS.FAILED || finalOperation.status?.toLowerCase() === OPERATION_STATUS.ERROR) throw new Error(`Operation ${operationId} failed: ${finalOperation.error || finalOperation.status_message || "Unknown error"}`);
|
|
124
|
+
return {
|
|
125
|
+
...uploadResult,
|
|
126
|
+
cid: finalOperation.cid,
|
|
127
|
+
operationId: finalOperation.id
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
#validateInput(input, options) {
|
|
131
|
+
if (input instanceof File) {
|
|
132
|
+
if (input.size === 0) throw new EmptyFileError(`Cannot upload empty file: ${input.name}`);
|
|
133
|
+
} else if (input instanceof ReadableStream) {
|
|
134
|
+
if (options?.size !== void 0 && options.size === 0) throw new EmptyFileError("Cannot upload empty stream");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async uploadDirectory(files, options) {
|
|
138
|
+
const carResult = await preprocessToCar(files, {
|
|
139
|
+
onProgress: options?.onProgress ? (p) => options.onProgress({
|
|
140
|
+
percentage: p,
|
|
141
|
+
bytesUploaded: 0,
|
|
142
|
+
bytesTotal: 0
|
|
143
|
+
}) : void 0,
|
|
144
|
+
signal: options?.signal
|
|
145
|
+
});
|
|
146
|
+
const operation = await this.#uploadCarResult(carResult, options?.name || "directory", options);
|
|
147
|
+
if (options?.waitForOperation && operation.result) {
|
|
148
|
+
const uploadResult = await operation.result;
|
|
149
|
+
operation.result = this.waitForOperation({
|
|
150
|
+
...uploadResult,
|
|
151
|
+
isDirectory: true,
|
|
152
|
+
numberOfFiles: files.length
|
|
153
|
+
}, options.operationPollingOptions);
|
|
154
|
+
}
|
|
155
|
+
return operation;
|
|
156
|
+
}
|
|
157
|
+
async #uploadInput(input, options) {
|
|
158
|
+
if (await this.#isCarFileUpload(input, options)) return this.#uploadCarFile(input, options);
|
|
159
|
+
const limit = await this.fetchUploadLimit();
|
|
160
|
+
if (input instanceof ReadableStream) {
|
|
161
|
+
const [streamForSize, streamForUpload] = input.tee();
|
|
162
|
+
let size;
|
|
163
|
+
if (options?.size !== void 0) size = BigInt(options.size);
|
|
164
|
+
else size = await calculateStreamSize(streamForSize, options?.signal);
|
|
165
|
+
if (size >= BigInt(limit)) return this.#uploadFile({
|
|
166
|
+
data: streamForUpload,
|
|
167
|
+
name: options?.name || "upload",
|
|
168
|
+
type: options?.name?.endsWith(FILE_EXTENSION_CAR) || options?.isDirectory ? MIME_TYPE_CAR : MIME_TYPE_OCTET_STREAM,
|
|
169
|
+
size: Number(size)
|
|
170
|
+
}, options);
|
|
171
|
+
else {
|
|
172
|
+
const blob = await streamToBlob(streamForUpload, "application/octet-stream");
|
|
173
|
+
const file = new File([blob], options?.name || "upload", { type: blob.type });
|
|
174
|
+
return this.#uploadFile(file, options);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return this.#uploadFile(input, options);
|
|
178
|
+
}
|
|
179
|
+
async #isCarFileUpload(input, options) {
|
|
180
|
+
if (options?.isCarFile === true) return true;
|
|
181
|
+
if (options?.isCarFile === false) return false;
|
|
182
|
+
if (input instanceof File) {
|
|
183
|
+
if (input.type === MIME_TYPE_CAR || input.name.endsWith(FILE_EXTENSION_CAR)) return await isCarFile(input);
|
|
184
|
+
}
|
|
185
|
+
if (input instanceof ReadableStream && options?.name?.endsWith(FILE_EXTENSION_CAR)) return options?.isCarFile !== false;
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
async #uploadCarResult(carResult, name, options) {
|
|
189
|
+
const limit = await this.fetchUploadLimit();
|
|
190
|
+
if (carResult.size >= BigInt(limit)) return this.#uploadFile({
|
|
191
|
+
data: carResult.carStream,
|
|
192
|
+
name: `${name}${FILE_EXTENSION_CAR}`,
|
|
193
|
+
type: MIME_TYPE_CAR,
|
|
194
|
+
size: Number(carResult.size)
|
|
195
|
+
}, options);
|
|
196
|
+
else {
|
|
197
|
+
const blob = await streamToBlob(carResult.carStream, MIME_TYPE_CAR);
|
|
198
|
+
const file = new File([blob], `${name}${FILE_EXTENSION_CAR}`, { type: MIME_TYPE_CAR });
|
|
199
|
+
return this.#uploadFile(file, options);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async #uploadCarFile(input, options) {
|
|
203
|
+
const limit = await this.fetchUploadLimit();
|
|
204
|
+
if (input instanceof ReadableStream) {
|
|
205
|
+
const [streamForSize, streamForUpload] = input.tee();
|
|
206
|
+
let size;
|
|
207
|
+
if (options?.size !== void 0) size = BigInt(options.size);
|
|
208
|
+
else size = await calculateStreamSize(streamForSize, options?.signal);
|
|
209
|
+
if (size >= BigInt(limit)) return this.#uploadFile({
|
|
210
|
+
data: streamForUpload,
|
|
211
|
+
name: options?.name || "upload.car",
|
|
212
|
+
type: MIME_TYPE_CAR,
|
|
213
|
+
size: Number(size)
|
|
214
|
+
}, options);
|
|
215
|
+
else {
|
|
216
|
+
const blob = await streamToBlob(streamForUpload, MIME_TYPE_CAR);
|
|
217
|
+
const file = new File([blob], options?.name || "upload.car", { type: MIME_TYPE_CAR });
|
|
218
|
+
return this.#uploadFile(file, options);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (input.type !== MIME_TYPE_CAR) input = new File([input], input.name, {
|
|
222
|
+
type: MIME_TYPE_CAR,
|
|
223
|
+
lastModified: input.lastModified
|
|
224
|
+
});
|
|
225
|
+
return this.#uploadFile(input, options);
|
|
226
|
+
}
|
|
227
|
+
async #uploadFile(input, options) {
|
|
228
|
+
const limit = await this.fetchUploadLimit();
|
|
229
|
+
let isLargeFile = false;
|
|
230
|
+
if (input instanceof File) isLargeFile = input.size > limit;
|
|
231
|
+
else isLargeFile = true;
|
|
232
|
+
const operation = await (isLargeFile ? this.tusHandler.upload(input, options) : this.xhrHandler.upload(input, options));
|
|
233
|
+
if (options?.waitForOperation && operation.result) {
|
|
234
|
+
const uploadResult = await operation.result;
|
|
235
|
+
operation.result = this.waitForOperation(uploadResult, options.operationPollingOptions);
|
|
236
|
+
}
|
|
237
|
+
return operation;
|
|
238
|
+
}
|
|
239
|
+
destroy() {
|
|
240
|
+
this.xhrHandler.destroy();
|
|
241
|
+
this.tusHandler.destroy();
|
|
242
|
+
destroyCarPreprocessor();
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
//#endregion
|
|
247
|
+
export { UploadManager };
|
|
248
|
+
//# sourceMappingURL=manager.js.map
|