@stacksjs/storage 0.70.88 → 0.70.90
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/adapters/bun.d.ts +31 -0
- package/dist/adapters/bun.js +226 -0
- package/dist/adapters/index.d.ts +6 -0
- package/dist/adapters/index.js +5 -0
- package/dist/adapters/local.d.ts +38 -0
- package/dist/adapters/local.js +225 -0
- package/dist/adapters/memory.d.ts +38 -0
- package/dist/adapters/memory.js +318 -0
- package/dist/adapters/s3.d.ts +53 -0
- package/dist/adapters/s3.js +471 -0
- package/dist/adapters/scoped.d.ts +68 -0
- package/dist/adapters/scoped.js +142 -0
- package/dist/copy.d.ts +3 -0
- package/dist/copy.js +30 -0
- package/dist/delete.d.ts +8 -0
- package/dist/delete.js +103 -0
- package/dist/drivers/aws.d.ts +4 -0
- package/dist/drivers/aws.js +94 -0
- package/dist/drivers/bun.d.ts +4 -0
- package/dist/drivers/bun.js +88 -0
- package/dist/drivers/index.d.ts +4 -0
- package/dist/drivers/index.js +4 -0
- package/dist/drivers/local.d.ts +4 -0
- package/dist/drivers/local.js +88 -0
- package/dist/drivers/memory.d.ts +4 -0
- package/dist/drivers/memory.js +67 -0
- package/dist/facade.d.ts +53 -0
- package/dist/facade.js +226 -0
- package/dist/files.d.ts +52 -0
- package/dist/files.js +126 -0
- package/dist/folders.d.ts +18 -0
- package/dist/folders.js +36 -0
- package/dist/fs.d.ts +4 -0
- package/dist/fs.js +7 -0
- package/dist/glob.d.ts +13 -0
- package/dist/glob.js +40 -0
- package/dist/hash.d.ts +5 -0
- package/dist/hash.js +33 -0
- package/dist/helpers.d.ts +7 -0
- package/dist/helpers.js +28 -0
- package/dist/image.d.ts +55 -0
- package/dist/image.js +29 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +27 -0
- package/dist/mime-verify.d.ts +65 -0
- package/dist/mime-verify.js +47 -0
- package/dist/move.d.ts +6 -0
- package/dist/move.js +55 -0
- package/dist/path-sanitize.d.ts +92 -0
- package/dist/path-sanitize.js +84 -0
- package/dist/put-file.d.ts +53 -0
- package/dist/put-file.js +85 -0
- package/dist/s3-presigned-post.d.ts +52 -0
- package/dist/s3-presigned-post.js +68 -0
- package/dist/signed-url.d.ts +69 -0
- package/dist/signed-url.js +86 -0
- package/dist/static-serve.d.ts +37 -0
- package/dist/static-serve.js +110 -0
- package/dist/storage.d.ts +9 -0
- package/dist/storage.js +9 -0
- package/dist/types/filesystem.d.ts +131 -0
- package/dist/types/filesystem.js +40 -0
- package/dist/types.d.ts +229 -0
- package/dist/types.js +25 -0
- package/dist/uploaded-file.d.ts +38 -0
- package/dist/uploaded-file.js +114 -0
- package/dist/visibility.d.ts +3 -0
- package/dist/visibility.js +3 -0
- package/dist/zip.d.ts +16 -0
- package/dist/zip.js +41 -0
- package/package.json +6 -6
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { createDirectoryListing, normalizeExpiryToDate } from "../types";
|
|
4
|
+
|
|
5
|
+
export class InMemoryStorageAdapter {
|
|
6
|
+
files;
|
|
7
|
+
directories;
|
|
8
|
+
constructor() {
|
|
9
|
+
this.files = new Map;
|
|
10
|
+
this.directories = new Set;
|
|
11
|
+
this.directories.add("");
|
|
12
|
+
}
|
|
13
|
+
normalizePath(path) {
|
|
14
|
+
return path.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
15
|
+
}
|
|
16
|
+
getDirectoryPath(path) {
|
|
17
|
+
const parts = path.split("/").filter(Boolean);
|
|
18
|
+
parts.pop();
|
|
19
|
+
return parts.join("/");
|
|
20
|
+
}
|
|
21
|
+
async contentsToUint8Array(contents) {
|
|
22
|
+
if (typeof contents === "string")
|
|
23
|
+
return new TextEncoder().encode(contents);
|
|
24
|
+
else if (contents instanceof Buffer)
|
|
25
|
+
return new Uint8Array(contents);
|
|
26
|
+
else if (contents instanceof Uint8Array)
|
|
27
|
+
return contents;
|
|
28
|
+
else {
|
|
29
|
+
if (typeof contents.getReader !== "function")
|
|
30
|
+
throw TypeError("[storage/memory] contents must be a web-standard ReadableStream (with .getReader()), not a Node stream.Readable. Convert via Readable.toWeb(nodeStream) before passing.");
|
|
31
|
+
const reader = contents.getReader(), chunks = [];
|
|
32
|
+
while (!0) {
|
|
33
|
+
const { done, value } = await reader.read();
|
|
34
|
+
if (done)
|
|
35
|
+
break;
|
|
36
|
+
if (value)
|
|
37
|
+
chunks.push(value);
|
|
38
|
+
}
|
|
39
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0), result = new Uint8Array(totalLength);
|
|
40
|
+
let offset = 0;
|
|
41
|
+
for (const chunk of chunks) {
|
|
42
|
+
result.set(chunk, offset);
|
|
43
|
+
offset += chunk.length;
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async write(path, contents) {
|
|
49
|
+
const normalized = this.normalizePath(path), dirPath = this.getDirectoryPath(normalized);
|
|
50
|
+
if (dirPath)
|
|
51
|
+
await this.createDirectory(dirPath);
|
|
52
|
+
const data = await this.contentsToUint8Array(contents), lastModified = Date.now(), mimeType = this.detectMimeType(normalized);
|
|
53
|
+
this.files.set(normalized, {
|
|
54
|
+
contents: data,
|
|
55
|
+
visibility: "private",
|
|
56
|
+
mimeType,
|
|
57
|
+
lastModified
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
path: normalized,
|
|
61
|
+
size: data.length,
|
|
62
|
+
contentType: mimeType,
|
|
63
|
+
lastModified
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async read(path) {
|
|
67
|
+
const normalized = this.normalizePath(path), file = this.files.get(normalized);
|
|
68
|
+
if (!file)
|
|
69
|
+
throw Error(`File not found: ${path}`);
|
|
70
|
+
return file.contents;
|
|
71
|
+
}
|
|
72
|
+
async getStream(path, _options) {
|
|
73
|
+
const normalized = this.normalizePath(path), file = this.files.get(normalized);
|
|
74
|
+
if (!file)
|
|
75
|
+
throw Error(`File not found: ${path}`);
|
|
76
|
+
const bytes = file.contents;
|
|
77
|
+
return new ReadableStream({
|
|
78
|
+
start(controller) {
|
|
79
|
+
controller.enqueue(bytes);
|
|
80
|
+
controller.close();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async putStream(path, stream, options) {
|
|
85
|
+
const normalized = this.normalizePath(path), dirPath = this.getDirectoryPath(normalized);
|
|
86
|
+
if (dirPath)
|
|
87
|
+
await this.createDirectory(dirPath);
|
|
88
|
+
const chunks = [], reader = stream.getReader(), abort = options?.signal;
|
|
89
|
+
try {
|
|
90
|
+
while (!0) {
|
|
91
|
+
if (abort?.aborted)
|
|
92
|
+
throw Error("aborted");
|
|
93
|
+
const { done, value } = await reader.read();
|
|
94
|
+
if (done)
|
|
95
|
+
break;
|
|
96
|
+
if (value)
|
|
97
|
+
chunks.push(value);
|
|
98
|
+
}
|
|
99
|
+
} finally {
|
|
100
|
+
try {
|
|
101
|
+
reader.releaseLock();
|
|
102
|
+
} catch {}
|
|
103
|
+
}
|
|
104
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.length, 0), data = new Uint8Array(totalLength);
|
|
105
|
+
let offset = 0;
|
|
106
|
+
for (const c of chunks) {
|
|
107
|
+
data.set(c, offset);
|
|
108
|
+
offset += c.length;
|
|
109
|
+
}
|
|
110
|
+
const lastModified = Date.now(), mimeType = options?.contentType ?? this.detectMimeType(normalized);
|
|
111
|
+
this.files.set(normalized, {
|
|
112
|
+
contents: data,
|
|
113
|
+
visibility: "private",
|
|
114
|
+
mimeType,
|
|
115
|
+
lastModified
|
|
116
|
+
});
|
|
117
|
+
return { path: normalized, size: data.length, contentType: mimeType, lastModified };
|
|
118
|
+
}
|
|
119
|
+
async readToString(path) {
|
|
120
|
+
const data = await this.read(path);
|
|
121
|
+
return new TextDecoder().decode(data);
|
|
122
|
+
}
|
|
123
|
+
async readToBuffer(path) {
|
|
124
|
+
const data = await this.read(path);
|
|
125
|
+
return Buffer.from(data);
|
|
126
|
+
}
|
|
127
|
+
async readToUint8Array(path) {
|
|
128
|
+
return await this.read(path);
|
|
129
|
+
}
|
|
130
|
+
async deleteFile(path) {
|
|
131
|
+
const normalized = this.normalizePath(path);
|
|
132
|
+
if (!this.files.has(normalized))
|
|
133
|
+
throw Error(`File not found: ${path}`);
|
|
134
|
+
this.files.delete(normalized);
|
|
135
|
+
}
|
|
136
|
+
async deleteDirectory(path) {
|
|
137
|
+
const normalized = this.normalizePath(path), prefix = normalized ? `${normalized}/` : "";
|
|
138
|
+
for (const filePath of this.files.keys())
|
|
139
|
+
if (filePath === normalized || filePath.startsWith(prefix))
|
|
140
|
+
this.files.delete(filePath);
|
|
141
|
+
for (const dir of this.directories)
|
|
142
|
+
if (dir === normalized || dir.startsWith(prefix))
|
|
143
|
+
this.directories.delete(dir);
|
|
144
|
+
}
|
|
145
|
+
async createDirectory(path) {
|
|
146
|
+
const normalized = this.normalizePath(path);
|
|
147
|
+
if (!normalized)
|
|
148
|
+
return;
|
|
149
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
150
|
+
let current = "";
|
|
151
|
+
for (const part of parts) {
|
|
152
|
+
current = current ? `${current}/${part}` : part;
|
|
153
|
+
this.directories.add(current);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async moveFile(from, to) {
|
|
157
|
+
const normalizedFrom = this.normalizePath(from), normalizedTo = this.normalizePath(to), file = this.files.get(normalizedFrom);
|
|
158
|
+
if (!file)
|
|
159
|
+
throw Error(`File not found: ${from}`);
|
|
160
|
+
const toDir = this.getDirectoryPath(normalizedTo);
|
|
161
|
+
if (toDir)
|
|
162
|
+
await this.createDirectory(toDir);
|
|
163
|
+
this.files.set(normalizedTo, { ...file, lastModified: Date.now() });
|
|
164
|
+
this.files.delete(normalizedFrom);
|
|
165
|
+
}
|
|
166
|
+
async copyFile(from, to) {
|
|
167
|
+
const normalizedFrom = this.normalizePath(from), normalizedTo = this.normalizePath(to), file = this.files.get(normalizedFrom);
|
|
168
|
+
if (!file)
|
|
169
|
+
throw Error(`File not found: ${from}`);
|
|
170
|
+
const toDir = this.getDirectoryPath(normalizedTo);
|
|
171
|
+
if (toDir)
|
|
172
|
+
await this.createDirectory(toDir);
|
|
173
|
+
const contentsCopy = new Uint8Array(file.contents);
|
|
174
|
+
this.files.set(normalizedTo, {
|
|
175
|
+
...file,
|
|
176
|
+
contents: contentsCopy,
|
|
177
|
+
lastModified: Date.now()
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
async stat(path) {
|
|
181
|
+
const normalized = this.normalizePath(path), file = this.files.get(normalized);
|
|
182
|
+
if (file)
|
|
183
|
+
return {
|
|
184
|
+
path: normalized,
|
|
185
|
+
type: "file",
|
|
186
|
+
visibility: file.visibility,
|
|
187
|
+
size: file.contents.length,
|
|
188
|
+
lastModified: file.lastModified,
|
|
189
|
+
mimeType: file.mimeType
|
|
190
|
+
};
|
|
191
|
+
if (this.directories.has(normalized) || normalized === "")
|
|
192
|
+
return {
|
|
193
|
+
path: normalized,
|
|
194
|
+
type: "directory",
|
|
195
|
+
visibility: "private",
|
|
196
|
+
size: 0,
|
|
197
|
+
lastModified: Date.now()
|
|
198
|
+
};
|
|
199
|
+
throw Error(`Path not found: ${path}`);
|
|
200
|
+
}
|
|
201
|
+
list(path, options = {}) {
|
|
202
|
+
return this.createAsyncIterator(path, options.deep || !1);
|
|
203
|
+
}
|
|
204
|
+
async* createAsyncIterator(path, deep) {
|
|
205
|
+
const normalized = this.normalizePath(path), prefix = normalized ? `${normalized}/` : "", entries = [], seen = new Set;
|
|
206
|
+
for (const [filePath] of this.files)
|
|
207
|
+
if (filePath.startsWith(prefix) || prefix === "") {
|
|
208
|
+
const relativePath = prefix ? filePath.slice(prefix.length) : filePath;
|
|
209
|
+
if (!deep) {
|
|
210
|
+
if (relativePath.split("/").filter(Boolean).length === 1)
|
|
211
|
+
entries.push({
|
|
212
|
+
path: filePath,
|
|
213
|
+
type: "file"
|
|
214
|
+
});
|
|
215
|
+
} else
|
|
216
|
+
entries.push({
|
|
217
|
+
path: filePath,
|
|
218
|
+
type: "file"
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
for (const dir of this.directories)
|
|
222
|
+
if ((dir.startsWith(prefix) || prefix === "") && dir !== normalized) {
|
|
223
|
+
const relativePath = prefix ? dir.slice(prefix.length) : dir;
|
|
224
|
+
if (!deep) {
|
|
225
|
+
if (relativePath.split("/").filter(Boolean).length === 1 && !seen.has(dir)) {
|
|
226
|
+
entries.push({
|
|
227
|
+
path: dir,
|
|
228
|
+
type: "directory"
|
|
229
|
+
});
|
|
230
|
+
seen.add(dir);
|
|
231
|
+
}
|
|
232
|
+
} else if (!seen.has(dir)) {
|
|
233
|
+
entries.push({
|
|
234
|
+
path: dir,
|
|
235
|
+
type: "directory"
|
|
236
|
+
});
|
|
237
|
+
seen.add(dir);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
yield* createDirectoryListing(entries);
|
|
241
|
+
}
|
|
242
|
+
async changeVisibility(path, visibility) {
|
|
243
|
+
const normalized = this.normalizePath(path), file = this.files.get(normalized);
|
|
244
|
+
if (!file)
|
|
245
|
+
throw Error(`File not found: ${path}`);
|
|
246
|
+
file.visibility = visibility;
|
|
247
|
+
}
|
|
248
|
+
async visibility(path) {
|
|
249
|
+
const normalized = this.normalizePath(path), file = this.files.get(normalized);
|
|
250
|
+
if (!file)
|
|
251
|
+
throw Error(`File not found: ${path}`);
|
|
252
|
+
return file.visibility;
|
|
253
|
+
}
|
|
254
|
+
async fileExists(path) {
|
|
255
|
+
const normalized = this.normalizePath(path);
|
|
256
|
+
return this.files.has(normalized);
|
|
257
|
+
}
|
|
258
|
+
async directoryExists(path) {
|
|
259
|
+
const normalized = this.normalizePath(path);
|
|
260
|
+
return this.directories.has(normalized) || normalized === "";
|
|
261
|
+
}
|
|
262
|
+
async publicUrl(path, options = {}) {
|
|
263
|
+
return `${(options.domain || process.env.APP_URL || "http://localhost").replace(/\/$/, "")}/${this.normalizePath(path)}`;
|
|
264
|
+
}
|
|
265
|
+
async temporaryUrl(path, options) {
|
|
266
|
+
const expiry = normalizeExpiryToDate(options.expiresIn);
|
|
267
|
+
return `http://localhost/temp/${Buffer.from(`${path}:${expiry.getTime()}`).toString("base64url")}`;
|
|
268
|
+
}
|
|
269
|
+
async signedUrl(_path, _options) {
|
|
270
|
+
throw Error("[storage/memory] signedUrl is not supported on the in-memory adapter \u2014 switch to local or s3 disk for signed URL generation.");
|
|
271
|
+
}
|
|
272
|
+
async checksum(path, options = {}) {
|
|
273
|
+
const algorithm = options.algorithm || "sha256", data = await this.readToUint8Array(path), hasher = new Bun.CryptoHasher(algorithm);
|
|
274
|
+
hasher.update(data);
|
|
275
|
+
return hasher.digest("hex");
|
|
276
|
+
}
|
|
277
|
+
async mimeType(path, _options = {}) {
|
|
278
|
+
const normalized = this.normalizePath(path), file = this.files.get(normalized);
|
|
279
|
+
if (!file)
|
|
280
|
+
throw Error(`File not found: ${path}`);
|
|
281
|
+
return file.mimeType;
|
|
282
|
+
}
|
|
283
|
+
detectMimeType(path) {
|
|
284
|
+
const ext = basename(path).split(".").pop()?.toLowerCase();
|
|
285
|
+
return {
|
|
286
|
+
txt: "text/plain",
|
|
287
|
+
html: "text/html",
|
|
288
|
+
css: "text/css",
|
|
289
|
+
js: "application/javascript",
|
|
290
|
+
json: "application/json",
|
|
291
|
+
xml: "application/xml",
|
|
292
|
+
pdf: "application/pdf",
|
|
293
|
+
zip: "application/zip",
|
|
294
|
+
jpg: "image/jpeg",
|
|
295
|
+
jpeg: "image/jpeg",
|
|
296
|
+
png: "image/png",
|
|
297
|
+
gif: "image/gif",
|
|
298
|
+
svg: "image/svg+xml",
|
|
299
|
+
mp4: "video/mp4",
|
|
300
|
+
mp3: "audio/mpeg",
|
|
301
|
+
wav: "audio/wav"
|
|
302
|
+
}[ext || ""] || "application/octet-stream";
|
|
303
|
+
}
|
|
304
|
+
async lastModified(path) {
|
|
305
|
+
return (await this.stat(path)).lastModified;
|
|
306
|
+
}
|
|
307
|
+
async fileSize(path) {
|
|
308
|
+
return (await this.stat(path)).size;
|
|
309
|
+
}
|
|
310
|
+
clear() {
|
|
311
|
+
this.files.clear();
|
|
312
|
+
this.directories.clear();
|
|
313
|
+
this.directories.add("");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
export function createMemoryStorage() {
|
|
317
|
+
return new InMemoryStorageAdapter;
|
|
318
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import type { ChecksumOptions, DirectoryListing, FileContents, GetStreamOptions, ListOptions, MimeTypeOptions, PresignedUploadPolicy, PresignedUploadPolicyOptions, PresignedUploadUrl, PresignedUploadUrlOptions, PublicUrlOptions, PutResult, PutStreamOptions, SignedUrlOptions, StatEntry, StorageAdapter, StorageAdapterConfig, TemporaryUrlOptions, Visibility } from '../types';
|
|
3
|
+
import type { S3Client } from '@stacksjs/ts-cloud';
|
|
4
|
+
/**
|
|
5
|
+
* Create an S3 storage adapter instance
|
|
6
|
+
*/
|
|
7
|
+
export declare function createS3Storage(client: S3Client | null, config: StorageAdapterConfig): S3StorageAdapter;
|
|
8
|
+
/**
|
|
9
|
+
* Append-and-take byte buffer used by the multipart pipeline
|
|
10
|
+
* (stacksjs/stacks#1886). Holds incoming chunks until they reach
|
|
11
|
+
* the configured part size, then yields them as a single Uint8Array
|
|
12
|
+
* via `take(n)` or `flush()`.
|
|
13
|
+
*/
|
|
14
|
+
declare class ChunkBuffer {
|
|
15
|
+
constructor(_partSize: number);
|
|
16
|
+
get length(): number;
|
|
17
|
+
push(c: Uint8Array): void;
|
|
18
|
+
take(n: number): Uint8Array;
|
|
19
|
+
flush(): Uint8Array;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* AWS S3 storage adapter using ts-cloud S3Client
|
|
23
|
+
*/
|
|
24
|
+
export declare class S3StorageAdapter implements StorageAdapter {
|
|
25
|
+
constructor(client: S3Client | null, config: StorageAdapterConfig);
|
|
26
|
+
write(path: string, contents: FileContents): Promise<PutResult>;
|
|
27
|
+
read(path: string): Promise<FileContents>;
|
|
28
|
+
getStream(path: string, _options?: GetStreamOptions): Promise<ReadableStream<Uint8Array>>;
|
|
29
|
+
putStream(path: string, stream: ReadableStream<Uint8Array>, options?: PutStreamOptions): Promise<PutResult>;
|
|
30
|
+
readToString(path: string): Promise<string>;
|
|
31
|
+
readToBuffer(path: string): Promise<Buffer>;
|
|
32
|
+
readToUint8Array(path: string): Promise<Uint8Array>;
|
|
33
|
+
deleteFile(path: string): Promise<void>;
|
|
34
|
+
deleteDirectory(path: string): Promise<void>;
|
|
35
|
+
createDirectory(_path: string): Promise<void>;
|
|
36
|
+
moveFile(from: string, to: string): Promise<void>;
|
|
37
|
+
copyFile(from: string, to: string): Promise<void>;
|
|
38
|
+
stat(path: string): Promise<StatEntry>;
|
|
39
|
+
list(path: string, options?: ListOptions): DirectoryListing;
|
|
40
|
+
changeVisibility(path: string, vis: Visibility): Promise<void>;
|
|
41
|
+
visibility(path: string): Promise<Visibility>;
|
|
42
|
+
fileExists(path: string): Promise<boolean>;
|
|
43
|
+
directoryExists(path: string): Promise<boolean>;
|
|
44
|
+
publicUrl(path: string, options?: PublicUrlOptions): Promise<string>;
|
|
45
|
+
temporaryUrl(path: string, options: TemporaryUrlOptions): Promise<string>;
|
|
46
|
+
signedUrl(path: string, options: SignedUrlOptions): Promise<string>;
|
|
47
|
+
presignedUploadUrl(options: PresignedUploadUrlOptions): Promise<PresignedUploadUrl>;
|
|
48
|
+
presignedUploadPolicy(options: PresignedUploadPolicyOptions): Promise<PresignedUploadPolicy>;
|
|
49
|
+
checksum(path: string, options?: ChecksumOptions): Promise<string>;
|
|
50
|
+
mimeType(path: string, _options?: MimeTypeOptions): Promise<string>;
|
|
51
|
+
lastModified(path: string): Promise<number>;
|
|
52
|
+
fileSize(path: string): Promise<number>;
|
|
53
|
+
}
|