fastify-txstate 3.6.6 → 3.6.8
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/lib/filestorage.d.ts +31 -0
- package/lib/filestorage.js +95 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/postformdata.d.ts +15 -0
- package/lib/postformdata.js +92 -0
- package/lib-esm/index.js +4 -0
- package/package.json +3 -2
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type Readable } from 'stream';
|
|
2
|
+
export interface FileHandler {
|
|
3
|
+
init: () => Promise<void>;
|
|
4
|
+
put: (stream: Readable) => Promise<{
|
|
5
|
+
checksum: string;
|
|
6
|
+
size: number;
|
|
7
|
+
}>;
|
|
8
|
+
get: (checksum: string) => Readable;
|
|
9
|
+
remove: (checksum: string) => Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export declare class FileSystemHandler implements FileHandler {
|
|
12
|
+
#private;
|
|
13
|
+
options: {
|
|
14
|
+
tmpdir: string;
|
|
15
|
+
permdir: string;
|
|
16
|
+
};
|
|
17
|
+
constructor(options?: {
|
|
18
|
+
tmpdir?: string;
|
|
19
|
+
permdir?: string;
|
|
20
|
+
});
|
|
21
|
+
init(): Promise<void>;
|
|
22
|
+
get(checksum: string): import("fs").ReadStream;
|
|
23
|
+
exists(checksum: string): Promise<boolean>;
|
|
24
|
+
fileSize(checksum: string): Promise<number>;
|
|
25
|
+
put(stream: Readable): Promise<{
|
|
26
|
+
checksum: string;
|
|
27
|
+
size: number;
|
|
28
|
+
}>;
|
|
29
|
+
remove(checksum: string): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
export declare const fileHandler: FileSystemHandler;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fileHandler = exports.FileSystemHandler = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const promises_1 = require("fs/promises");
|
|
7
|
+
const path_1 = require("path");
|
|
8
|
+
const promises_2 = require("stream/promises");
|
|
9
|
+
const txstate_utils_1 = require("txstate-utils");
|
|
10
|
+
class FileSystemHandler {
|
|
11
|
+
options;
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.options = {
|
|
14
|
+
tmpdir: options.tmpdir ?? '/files/tmp/',
|
|
15
|
+
permdir: options.permdir ?? '/files/storage/'
|
|
16
|
+
};
|
|
17
|
+
if (!this.options.tmpdir.endsWith('/'))
|
|
18
|
+
this.options.tmpdir += '/';
|
|
19
|
+
if (!this.options.permdir.endsWith('/'))
|
|
20
|
+
this.options.permdir += '/';
|
|
21
|
+
}
|
|
22
|
+
#getTmpLocation() {
|
|
23
|
+
return `${this.options.tmpdir}${(0, txstate_utils_1.randomid)(12)}`;
|
|
24
|
+
}
|
|
25
|
+
#getFileLocation(checksum) {
|
|
26
|
+
return `${this.options.permdir}${checksum.slice(0, 1)}/${checksum.slice(1, 2)}/${checksum.slice(2)}`;
|
|
27
|
+
}
|
|
28
|
+
async #moveToPerm(tmp, checksum) {
|
|
29
|
+
const checksumpath = this.#getFileLocation(checksum);
|
|
30
|
+
await (0, promises_1.mkdir)((0, path_1.dirname)(checksumpath), { recursive: true });
|
|
31
|
+
await (0, promises_1.rename)(tmp, checksumpath);
|
|
32
|
+
}
|
|
33
|
+
async init() {
|
|
34
|
+
await (0, promises_1.mkdir)(this.options.tmpdir, { recursive: true });
|
|
35
|
+
await (0, promises_1.mkdir)(this.options.permdir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
get(checksum) {
|
|
38
|
+
const filepath = this.#getFileLocation(checksum);
|
|
39
|
+
const stream = (0, fs_1.createReadStream)(filepath);
|
|
40
|
+
return stream;
|
|
41
|
+
}
|
|
42
|
+
async exists(checksum) {
|
|
43
|
+
const filepath = this.#getFileLocation(checksum);
|
|
44
|
+
return (await (0, txstate_utils_1.rescue)((0, promises_1.access)(filepath, promises_1.constants.R_OK), false)) ?? true;
|
|
45
|
+
}
|
|
46
|
+
async fileSize(checksum) {
|
|
47
|
+
const filepath = this.#getFileLocation(checksum);
|
|
48
|
+
const info = await (0, promises_1.stat)(filepath);
|
|
49
|
+
return info.size;
|
|
50
|
+
}
|
|
51
|
+
async put(stream) {
|
|
52
|
+
const tmp = this.#getTmpLocation();
|
|
53
|
+
const hash = (0, crypto_1.createHash)('sha256');
|
|
54
|
+
let size = 0;
|
|
55
|
+
stream.on('data', (data) => { hash.update(data); size += data.length; });
|
|
56
|
+
try {
|
|
57
|
+
const out = (0, fs_1.createWriteStream)(tmp);
|
|
58
|
+
const flushedPromise = new Promise((resolve, reject) => {
|
|
59
|
+
out.on('close', resolve);
|
|
60
|
+
out.on('error', reject);
|
|
61
|
+
});
|
|
62
|
+
await (0, promises_2.pipeline)(stream, out);
|
|
63
|
+
await flushedPromise;
|
|
64
|
+
const checksum = hash.digest('base64url');
|
|
65
|
+
const rereadhash = (0, crypto_1.createHash)('sha256');
|
|
66
|
+
const read = (0, fs_1.createReadStream)(tmp);
|
|
67
|
+
for await (const chunk of read) {
|
|
68
|
+
rereadhash.update(chunk);
|
|
69
|
+
}
|
|
70
|
+
const rereadsum = rereadhash.digest('base64url');
|
|
71
|
+
if (rereadsum !== checksum)
|
|
72
|
+
throw new Error('File did not write to disk correctly. Please try uploading again.');
|
|
73
|
+
await this.#moveToPerm(tmp, checksum);
|
|
74
|
+
return { checksum, size };
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
await (0, txstate_utils_1.rescue)((0, promises_1.unlink)(tmp));
|
|
78
|
+
throw e;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async remove(checksum) {
|
|
82
|
+
const filepath = this.#getFileLocation(checksum);
|
|
83
|
+
try {
|
|
84
|
+
await (0, promises_1.unlink)(filepath);
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
if (e.code === 'ENOENT')
|
|
88
|
+
console.warn('Tried to delete file with checksum', checksum, 'but it did not exist.');
|
|
89
|
+
else
|
|
90
|
+
console.warn(e);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
exports.FileSystemHandler = FileSystemHandler;
|
|
95
|
+
exports.fileHandler = new FileSystemHandler();
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -417,4 +417,6 @@ this is log into this application and use dev tools to pull your token from the
|
|
|
417
417
|
exports.default = Server;
|
|
418
418
|
__exportStar(require("./analytics"), exports);
|
|
419
419
|
__exportStar(require("./error"), exports);
|
|
420
|
+
__exportStar(require("./filestorage"), exports);
|
|
420
421
|
__exportStar(require("./unified-auth"), exports);
|
|
422
|
+
__exportStar(require("./postformdata"), exports);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import { ReadableStream } from 'node:stream/web';
|
|
3
|
+
export interface FormDataTextField {
|
|
4
|
+
name: string;
|
|
5
|
+
value: string;
|
|
6
|
+
}
|
|
7
|
+
export interface FormDataFileField {
|
|
8
|
+
name: string;
|
|
9
|
+
value: ReadableStream | Readable;
|
|
10
|
+
filename?: string;
|
|
11
|
+
filetype?: string;
|
|
12
|
+
filesize?: number;
|
|
13
|
+
}
|
|
14
|
+
export type FormDataField = FormDataTextField | FormDataFileField;
|
|
15
|
+
export declare function postFormData(url: string, fields: FormDataField[], headers?: Record<string, any>): Promise<Response>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.postFormData = postFormData;
|
|
4
|
+
const node_stream_1 = require("node:stream");
|
|
5
|
+
const web_1 = require("node:stream/web");
|
|
6
|
+
async function postFormData(url, fields, headers = {}) {
|
|
7
|
+
const encoder = new TextEncoder();
|
|
8
|
+
const boundary = `${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`;
|
|
9
|
+
const footer = `--${boundary}--\r\n`;
|
|
10
|
+
const chunks = fields.map(field => new FormDataChunk(boundary, encoder, field));
|
|
11
|
+
const totalSize = chunks.some(chunk => chunk.contentsize == null) ? undefined : chunks.reduce((sum, chunk) => sum + chunk.extrasize + chunk.contentsize, 0);
|
|
12
|
+
headers = {
|
|
13
|
+
...headers,
|
|
14
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`
|
|
15
|
+
};
|
|
16
|
+
if (totalSize) {
|
|
17
|
+
headers['Content-Length'] = totalSize.toString();
|
|
18
|
+
}
|
|
19
|
+
let i = 0;
|
|
20
|
+
let part = 'header';
|
|
21
|
+
const stream = new web_1.ReadableStream({
|
|
22
|
+
async pull(controller) {
|
|
23
|
+
if (i === chunks.length) {
|
|
24
|
+
controller.enqueue(encoder.encode(footer));
|
|
25
|
+
controller.close();
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
const chunk = chunks[i];
|
|
29
|
+
if (part === 'header') {
|
|
30
|
+
controller.enqueue(encoder.encode(chunk.header));
|
|
31
|
+
part = 'content';
|
|
32
|
+
}
|
|
33
|
+
else if (part === 'content') {
|
|
34
|
+
const { value, done } = await chunk.contentReader.read();
|
|
35
|
+
if (done)
|
|
36
|
+
part = 'footer';
|
|
37
|
+
else
|
|
38
|
+
controller.enqueue(value);
|
|
39
|
+
}
|
|
40
|
+
else if (part === 'footer') {
|
|
41
|
+
controller.enqueue(encoder.encode(chunk.footer));
|
|
42
|
+
i++;
|
|
43
|
+
part = 'header';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
cancel() {
|
|
48
|
+
for (const chunk of chunks) {
|
|
49
|
+
if (chunk.contentReader.cancel) {
|
|
50
|
+
chunk.contentReader.cancel();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return await fetch(url, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers,
|
|
58
|
+
duplex: 'half',
|
|
59
|
+
body: stream
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function isFileField(field) {
|
|
63
|
+
return 'filename' in field || 'filetype' in field || 'filesize' in field || (typeof field.value === 'object' && 'getReader' in field.value);
|
|
64
|
+
}
|
|
65
|
+
class FormDataChunk {
|
|
66
|
+
header;
|
|
67
|
+
footer;
|
|
68
|
+
extrasize;
|
|
69
|
+
contentsize;
|
|
70
|
+
contentReader;
|
|
71
|
+
constructor(boundary, encoder, field) {
|
|
72
|
+
this.header = `--${boundary}\r\nContent-Disposition: form-data; name="${field.name}"`;
|
|
73
|
+
this.footer = '\r\n';
|
|
74
|
+
if (isFileField(field)) {
|
|
75
|
+
this.header += `; filename="${field.filename ?? field.name}"\r\nContent-Type: ${field.filetype ?? 'application/octet-stream'}`;
|
|
76
|
+
this.contentsize = field.filesize;
|
|
77
|
+
this.contentReader = (field.value instanceof node_stream_1.Readable ? web_1.ReadableStream.from(field.value) : field.value).getReader();
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const encoded = encoder.encode(field.value);
|
|
81
|
+
this.contentsize = encoded.length;
|
|
82
|
+
this.contentReader = new web_1.ReadableStream({
|
|
83
|
+
start: controller => {
|
|
84
|
+
controller.enqueue(encoded);
|
|
85
|
+
controller.close();
|
|
86
|
+
}
|
|
87
|
+
}).getReader();
|
|
88
|
+
}
|
|
89
|
+
this.header += '\r\n\r\n';
|
|
90
|
+
this.extrasize = Buffer.byteLength(this.header) + Buffer.byteLength(this.footer);
|
|
91
|
+
}
|
|
92
|
+
}
|
package/lib-esm/index.js
CHANGED
|
@@ -13,4 +13,8 @@ export const analyticsPlugin = ftxst.analyticsPlugin
|
|
|
13
13
|
export const AnalyticsClient = ftxst.AnalyticsClient
|
|
14
14
|
export const LoggingAnalyticsClient = ftxst.LoggingAnalyticsClient
|
|
15
15
|
export const ElasticAnalyticsClient = ftxst.ElasticAnalyticsClient
|
|
16
|
+
export const postFormData = ftxst.postFormData
|
|
17
|
+
export const readableToWebReadable = ftxst.readableToWebReadable
|
|
18
|
+
export const FileSystemHandler = ftxst.FileSystemHandler
|
|
19
|
+
export const fileHandler = ftxst.fileHandler
|
|
16
20
|
export default ftxst.default
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fastify-txstate",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.8",
|
|
4
4
|
"description": "A small wrapper for fastify providing a set of common conventions & utility functions we use.",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -34,9 +34,10 @@
|
|
|
34
34
|
"ua-parser-js": "^1.0.37"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
+
"@fastify/multipart": "^8.0.0",
|
|
37
38
|
"@types/chai": "^4.2.14",
|
|
38
39
|
"@types/mocha": "^10.0.0",
|
|
39
|
-
"@types/node": "^
|
|
40
|
+
"@types/node": "^24.0.0",
|
|
40
41
|
"axios": "^1.6.8",
|
|
41
42
|
"chai": "^4.2.0",
|
|
42
43
|
"eslint-config-standard-with-typescript": "^43.0.0",
|