@opengis/fastify-table 2.2.12 → 2.3.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/helper.d.ts.map +1 -1
- package/dist/helper.js +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/server/plugins/file/getFileSize.d.ts +2 -0
- package/dist/server/plugins/file/getFileSize.d.ts.map +1 -0
- package/dist/server/plugins/file/getFileSize.js +12 -0
- package/dist/server/plugins/file/providers/s3/funcs/getFileSize.d.ts +3 -0
- package/dist/server/plugins/file/providers/s3/funcs/getFileSize.d.ts.map +1 -0
- package/dist/server/plugins/file/providers/s3/funcs/getFileSize.js +30 -0
- package/dist/server/plugins/file/providers/s3/funcs/uploadFile.d.ts.map +1 -1
- package/dist/server/plugins/file/providers/s3/funcs/uploadFile.js +1 -0
- package/dist/server/plugins/file/providers/s3/index.d.ts +1 -0
- package/dist/server/plugins/file/providers/s3/index.d.ts.map +1 -1
- package/dist/server/plugins/file/providers/s3/index.js +2 -0
- package/dist/server/plugins/file/uploadFile.d.ts +1 -1
- package/dist/server/plugins/file/uploadFile.d.ts.map +1 -1
- package/dist/server/plugins/upload/getUploadStatus.d.ts.map +1 -1
- package/dist/server/plugins/upload/getUploadStatus.js +5 -0
- package/dist/server/plugins/upload/index.d.ts +3 -0
- package/dist/server/plugins/upload/index.d.ts.map +1 -1
- package/dist/server/plugins/upload/index.js +10 -2
- package/dist/server/plugins/upload/s3.d.ts +83 -0
- package/dist/server/plugins/upload/s3.d.ts.map +1 -0
- package/dist/server/plugins/upload/s3.js +387 -0
- package/dist/server/plugins/upload/startUpload.d.ts +1 -1
- package/dist/server/plugins/upload/startUpload.d.ts.map +1 -1
- package/dist/server/plugins/upload/startUpload.js +19 -4
- package/dist/server/plugins/upload/uploadChunk.d.ts.map +1 -1
- package/dist/server/plugins/upload/uploadChunk.js +36 -2
- package/package.json +1 -1
package/dist/helper.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../helper.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../helper.ts"],"names":[],"mappings":"AAEA,OAAO,MAAM,MAAM,aAAa,CAAC;AAGjC,OAAO,EAAE,SAAS,EAAkB,MAAM,YAAY,CAAC;AAEvD,QAAA,MAAQ,MAAM,KAAoB,CAAC;AAInC,wBAAsB,KAAK,kBAI1B;AAGD,wBAAsB,QAAQ,kBAW7B;AAED,wBAAsB,KAAK,iBA0B1B;AAED,OAAO,EAEL,MAAM,EACN,MAAM,EACN,SAAS,GACV,CAAC"}
|
package/dist/helper.js
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AA6FA,iBAAe,MAAM,CAAC,OAAO,EAAE,GAAG,iBAiLjC;;AACD,wBAEG"}
|
package/dist/index.js
CHANGED
|
@@ -34,6 +34,7 @@ import metricPlugin from "./server/plugins/metric/index.js";
|
|
|
34
34
|
import redisPlugin from "./server/plugins/redis/index.js";
|
|
35
35
|
import loggerPlugin from "./server/plugins/logger/index.js";
|
|
36
36
|
import authPlugin from "./server/plugins/auth/index.js";
|
|
37
|
+
import chunkedUploadPlugin from "./server/plugins/upload/index.js";
|
|
37
38
|
// utils
|
|
38
39
|
import execMigrations from "./server/plugins/migration/exec.migrations.js";
|
|
39
40
|
import pgClients from "./server/plugins/pg/pgClients.js";
|
|
@@ -113,6 +114,7 @@ async function plugin(fastify) {
|
|
|
113
114
|
execMigrations(path.join(cwd, "server/migrations"), pgClients.client, true).catch((err) => console.warn(err.toString()));
|
|
114
115
|
// plugins / utils / funcs
|
|
115
116
|
authPlugin(fastify); // fastify-auth api + hooks integrated to core
|
|
117
|
+
chunkedUploadPlugin(fastify);
|
|
116
118
|
policyPlugin(fastify);
|
|
117
119
|
metricPlugin();
|
|
118
120
|
redisPlugin(fastify);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getFileSize.d.ts","sourceRoot":"","sources":["../../../../server/plugins/file/getFileSize.ts"],"names":[],"mappings":"AAGA,wBAA8B,WAAW,CACvC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,gBAclC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import providers from "./providers/index.js";
|
|
3
|
+
export default async function getFileSize(filePath, options = {}) {
|
|
4
|
+
const filename = path.basename(filePath);
|
|
5
|
+
// prefix
|
|
6
|
+
const prefix = (options.prefix === "date"
|
|
7
|
+
? new Date().toISOString().split("T")[0]
|
|
8
|
+
: null) || (options.prefix === "3s" ? filename.substring(0, 3) : "");
|
|
9
|
+
const relativePath = path.join(path.dirname(filePath), prefix, filename);
|
|
10
|
+
const fp = providers(options);
|
|
11
|
+
return fp.getFileSize(relativePath, options);
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getFileSize.d.ts","sourceRoot":"","sources":["../../../../../../../server/plugins/file/providers/s3/funcs/getFileSize.ts"],"names":[],"mappings":"AAYA,QAAA,MAAM,WAAW,GACd,aAAa,GAAG,MACV,IAAI,GAAG,EAAE,UAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,iBAuBhD,CAAC;AAEJ,eAAe,WAAW,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { HeadObjectCommand } from "@aws-sdk/client-s3";
|
|
2
|
+
import s3Client from "../client.js";
|
|
3
|
+
import config from "../../../../../../config.js";
|
|
4
|
+
import getPath from "../../../utils/getPath.js";
|
|
5
|
+
import getS3FilePath from "./utils/getS3FilePath.js";
|
|
6
|
+
// if not found on s3 => fs
|
|
7
|
+
import fs from "../../fs.js";
|
|
8
|
+
const { getFileSize: getFileSizeFs } = fs();
|
|
9
|
+
const getFileSize = (s3Settings) => async (fp, options = {}) => {
|
|
10
|
+
const filepath = getS3FilePath(fp, s3Settings);
|
|
11
|
+
const bucketParams = {
|
|
12
|
+
Bucket: s3Settings?.containerName || config.s3?.containerName || "work",
|
|
13
|
+
Key: filepath[0] === "/" ? filepath?.slice(1) : filepath,
|
|
14
|
+
};
|
|
15
|
+
try {
|
|
16
|
+
const data = await s3Client.send(new HeadObjectCommand(bucketParams));
|
|
17
|
+
return data.ContentLength;
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
if (options.fallback === false) {
|
|
21
|
+
throw new Error("file not found");
|
|
22
|
+
}
|
|
23
|
+
const filepath1 = getPath(fp);
|
|
24
|
+
if (!filepath1) {
|
|
25
|
+
throw new Error("file not found");
|
|
26
|
+
}
|
|
27
|
+
return getFileSizeFs(filepath1);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
export default getFileSize;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uploadFile.d.ts","sourceRoot":"","sources":["../../../../../../../server/plugins/file/providers/s3/funcs/uploadFile.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"uploadFile.d.ts","sourceRoot":"","sources":["../../../../../../../server/plugins/file/providers/s3/funcs/uploadFile.ts"],"names":[],"mappings":"AAcA,QAAA,MAAM,UAAU,GAAI,aAAa,GAAG,MAAY,IAAI,GAAG,EAAE,MAAM,GAAG,+EA6BjE,CAAC;AAEF,eAAe,UAAU,CAAC"}
|
|
@@ -6,6 +6,7 @@ import getDataSize from "../../utils/getDataSize.js";
|
|
|
6
6
|
import dataTypes from "../../utils/handlers/dataTypes.js";
|
|
7
7
|
import getMimeType from "../../mime/index.js";
|
|
8
8
|
import getS3FilePath from "./utils/getS3FilePath.js";
|
|
9
|
+
// chunks not supported for s3
|
|
9
10
|
const uploadFile = (s3Settings) => async (fp, data) => {
|
|
10
11
|
const filepath = getS3FilePath(fp, s3Settings);
|
|
11
12
|
const validData = await getValidData({ data, types: [dataTypes.stream] });
|
|
@@ -3,5 +3,6 @@ export default function s3Storage(opt: any): {
|
|
|
3
3
|
downloadFile: (fp: string, options?: Record<string, any>) => Promise<any>;
|
|
4
4
|
uploadFile: (fp: any, data: any) => Promise<import("@aws-sdk/client-s3").CompleteMultipartUploadCommandOutput>;
|
|
5
5
|
fileExists: (fp: any, options?: Record<string, any>) => Promise<any>;
|
|
6
|
+
getFileSize: (fp: any, options?: Record<string, any>) => Promise<any>;
|
|
6
7
|
};
|
|
7
8
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../server/plugins/file/providers/s3/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../server/plugins/file/providers/s3/index.ts"],"names":[],"mappings":"AAKA,MAAM,CAAC,OAAO,UAAU,SAAS,CAAC,GAAG,EAAE,GAAG;;;;;;EAQzC"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import downloadFile from "./funcs/downloadFile.js";
|
|
2
2
|
import fileExists from "./funcs/fileExists.js";
|
|
3
|
+
import getFileSize from "./funcs/getFileSize.js";
|
|
3
4
|
import uploadFile from "./funcs/uploadFile.js";
|
|
4
5
|
export default function s3Storage(opt) {
|
|
5
6
|
return {
|
|
@@ -7,5 +8,6 @@ export default function s3Storage(opt) {
|
|
|
7
8
|
downloadFile: downloadFile(opt),
|
|
8
9
|
uploadFile: uploadFile(opt),
|
|
9
10
|
fileExists: fileExists(opt),
|
|
11
|
+
getFileSize: getFileSize(opt),
|
|
10
12
|
};
|
|
11
13
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
declare function uploadFile(filePath: string, data: any, options?: Record<string,
|
|
1
|
+
declare function uploadFile(filePath: string, data: any, options?: Record<string, any>): Promise<string>;
|
|
2
2
|
export default uploadFile;
|
|
3
3
|
//# sourceMappingURL=uploadFile.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uploadFile.d.ts","sourceRoot":"","sources":["../../../../server/plugins/file/uploadFile.ts"],"names":[],"mappings":"AAGA,iBAAe,UAAU,CACvB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,GAAG,EACT,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"uploadFile.d.ts","sourceRoot":"","sources":["../../../../server/plugins/file/uploadFile.ts"],"names":[],"mappings":"AAGA,iBAAe,UAAU,CACvB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,GAAG,EACT,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,mBAgBlC;AAED,eAAe,UAAU,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"getUploadStatus.d.ts","sourceRoot":"","sources":["../../../../server/plugins/upload/getUploadStatus.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"getUploadStatus.d.ts","sourceRoot":"","sources":["../../../../server/plugins/upload/getUploadStatus.ts"],"names":[],"mappings":"AAeA,wBAA8B,eAAe,CAAC,EAC5C,IAAI,EACJ,EAAE,GACH,EAAE;IACD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;CACZ,gBAmCA"}
|
|
@@ -2,10 +2,15 @@ import path from "node:path";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { prefix, metaDir, uploadChunkDirectory, fetchTimeoutMs, } from "./index.js";
|
|
5
|
+
import config from "../../../config.js";
|
|
5
6
|
import isFileExists from "../file/isFileExists.js";
|
|
7
|
+
import { getUploadStatus as getUploadStatusS3 } from "./s3.js";
|
|
6
8
|
export default async function getUploadStatus({ host, id, }) {
|
|
7
9
|
// return local file metadata
|
|
8
10
|
if (!host) {
|
|
11
|
+
if (config.s3) {
|
|
12
|
+
return getUploadStatusS3({ id });
|
|
13
|
+
}
|
|
9
14
|
const metaExists = existsSync(path.join(metaDir, `${id}.json`));
|
|
10
15
|
// check file upload status: finished/inprogress
|
|
11
16
|
const meta = metaExists
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { FastifyInstance } from "fastify";
|
|
1
2
|
export declare const fetchTimeoutMs: number;
|
|
2
3
|
export declare const prefix = "file/upload2";
|
|
3
4
|
export declare const uploadChunkDirectory = "/files/uploads";
|
|
4
5
|
export declare const fileDir: string;
|
|
5
6
|
export declare const metaDir: string;
|
|
7
|
+
declare function chunkedUploadPlugin(app: FastifyInstance): void;
|
|
8
|
+
export default chunkedUploadPlugin;
|
|
6
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../server/plugins/upload/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../server/plugins/upload/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAW1C,eAAO,MAAM,cAAc,QAE1B,CAAC;AAEF,eAAO,MAAM,MAAM,iBAAiB,CAAC;AAErC,eAAO,MAAM,oBAAoB,mBAAmB,CAAC;AAErD,eAAO,MAAM,OAAO,QAEE,CAAC;AAEvB,eAAO,MAAM,OAAO,QAA4B,CAAC;AAKjD,iBAAS,mBAAmB,CAAC,GAAG,EAAE,eAAe,QAIhD;AAED,eAAe,mBAAmB,CAAC"}
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import config from "../../../config.js";
|
|
3
3
|
import getFolder from "../crud/funcs/utils/getFolder.js";
|
|
4
|
-
config.chunkSize = +(config.chunkSize ||
|
|
4
|
+
config.chunkSize = +(config.chunkSize || 5242880); // 5 MB per chunk by default
|
|
5
5
|
const rootDir = getFolder(config, "local");
|
|
6
|
-
export const fetchTimeoutMs = +(config.fetchTimeoutMs || 5000);
|
|
6
|
+
export const fetchTimeoutMs = +(config.fetchTimeoutMs || (process.platform === "win32" ? 50000 : 5000));
|
|
7
7
|
export const prefix = "file/upload2";
|
|
8
8
|
export const uploadChunkDirectory = "/files/uploads";
|
|
9
9
|
export const fileDir = path
|
|
10
10
|
.join(rootDir, uploadChunkDirectory)
|
|
11
11
|
.replace(/\\/g, "/");
|
|
12
12
|
export const metaDir = path.join(fileDir, "tmp");
|
|
13
|
+
import { cleanupNonRedisUploads } from "./s3.js";
|
|
14
|
+
// w/ out redis - chunked uploads are non-resumable in order to avoid unclosed multiparts (non-discoverable via s3 commands with current version of minio)
|
|
15
|
+
function chunkedUploadPlugin(app) {
|
|
16
|
+
app.addHook("onClose", async () => {
|
|
17
|
+
await cleanupNonRedisUploads();
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export default chunkedUploadPlugin;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export declare function cleanupNonRedisUploads(s3?: any): Promise<void>;
|
|
2
|
+
export declare function saveUploadMeta(id: string, meta: any, ttl?: number): Promise<void>;
|
|
3
|
+
export declare function refreshUploadTTL(id: string, ttl?: number, s3?: any): Promise<void>;
|
|
4
|
+
export declare function deleteUploadMeta(id: string, key?: string): Promise<void>;
|
|
5
|
+
export declare function getUploadMeta(id: string): Promise<any>;
|
|
6
|
+
export declare function getResumableUploadId(key: string): Promise<any>;
|
|
7
|
+
export declare function cleanupStaleUploads(s3?: any): Promise<void>;
|
|
8
|
+
export declare function findUpload({ id }: {
|
|
9
|
+
id: string;
|
|
10
|
+
}, s3?: any): Promise<{
|
|
11
|
+
UploadId: string | undefined;
|
|
12
|
+
Key: any;
|
|
13
|
+
Bucket: any;
|
|
14
|
+
fileSize: any;
|
|
15
|
+
ContentType: any;
|
|
16
|
+
uploaded: any;
|
|
17
|
+
parts: any;
|
|
18
|
+
}>;
|
|
19
|
+
export declare function startUpload({ Bucket, Key, ContentType, fileSize, }: {
|
|
20
|
+
Bucket: string;
|
|
21
|
+
Key: string;
|
|
22
|
+
ContentType: string;
|
|
23
|
+
fileSize: number;
|
|
24
|
+
}, s3?: any): Promise<{
|
|
25
|
+
Key: string;
|
|
26
|
+
Bucket: any;
|
|
27
|
+
UploadId: any;
|
|
28
|
+
fileSize: any;
|
|
29
|
+
extension: any;
|
|
30
|
+
ContentType: any;
|
|
31
|
+
cache: boolean;
|
|
32
|
+
} | {
|
|
33
|
+
UploadId: any;
|
|
34
|
+
Key?: undefined;
|
|
35
|
+
Bucket?: undefined;
|
|
36
|
+
fileSize?: undefined;
|
|
37
|
+
extension?: undefined;
|
|
38
|
+
ContentType?: undefined;
|
|
39
|
+
cache?: undefined;
|
|
40
|
+
}>;
|
|
41
|
+
export declare function uploadChunk({ id, chunk, partNumber, }: {
|
|
42
|
+
id: string;
|
|
43
|
+
chunk: any;
|
|
44
|
+
partNumber: number;
|
|
45
|
+
}, s3?: any): Promise<{
|
|
46
|
+
UploadId: string | undefined;
|
|
47
|
+
Key: any;
|
|
48
|
+
Bucket: any;
|
|
49
|
+
fileSize: any;
|
|
50
|
+
ContentType: any;
|
|
51
|
+
uploaded: any;
|
|
52
|
+
parts: any;
|
|
53
|
+
}>;
|
|
54
|
+
export declare function finishUpload({ id, }: {
|
|
55
|
+
id: string;
|
|
56
|
+
}, s3?: any): Promise<{
|
|
57
|
+
uploaded: any;
|
|
58
|
+
}>;
|
|
59
|
+
export declare function abortUpload({ id, }: {
|
|
60
|
+
id: string;
|
|
61
|
+
}, s3?: any): Promise<{
|
|
62
|
+
aborted: boolean;
|
|
63
|
+
}>;
|
|
64
|
+
export declare function getUploadStatus({ id, Key, Bucket, }: {
|
|
65
|
+
id: string;
|
|
66
|
+
Key?: string;
|
|
67
|
+
Bucket?: string;
|
|
68
|
+
}, s3?: any): Promise<{
|
|
69
|
+
exists: boolean;
|
|
70
|
+
uploaded: any;
|
|
71
|
+
finished?: undefined;
|
|
72
|
+
id?: undefined;
|
|
73
|
+
size?: undefined;
|
|
74
|
+
parts?: undefined;
|
|
75
|
+
} | {
|
|
76
|
+
exists: boolean;
|
|
77
|
+
finished: boolean;
|
|
78
|
+
uploaded: any;
|
|
79
|
+
id: string;
|
|
80
|
+
size: number | undefined;
|
|
81
|
+
parts: any;
|
|
82
|
+
}>;
|
|
83
|
+
//# sourceMappingURL=s3.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../../../../server/plugins/upload/s3.ts"],"names":[],"mappings":"AAgGA,wBAAsB,sBAAsB,CAAC,EAAE,MAAW,iBAuCzD;AAED,wBAAsB,cAAc,CAClC,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,GAAG,EACT,GAAG,SAAqB,iBAazB;AAED,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,MAAM,EACV,GAAG,SAAqB,EACxB,EAAE,MAAW,iBAqBd;AAED,wBAAsB,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,iBA0B9D;AAED,wBAAsB,aAAa,CAAC,EAAE,EAAE,MAAM,gBAQ7C;AAED,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,MAAM,gBAMrD;AAED,wBAAsB,mBAAmB,CAAC,EAAE,MAAW,iBAwEtD;AAED,wBAAsB,UAAU,CAAC,EAAE,EAAE,EAAE,EAAE;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,EAAE,MAAW;;;;;;;;GAqCrE;AAED,wBAAsB,WAAW,CAC/B,EACE,MAAM,EACN,GAAG,EACH,WAAW,EACX,QAAQ,GACT,EAAE;IACD,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB,EACD,EAAE,MAAW;;;;;;;;;;;;;;;;GAqDd;AAED,wBAAsB,WAAW,CAC/B,EACE,EAAE,EACF,KAAK,EACL,UAAU,GACX,EAAE;IACD,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,GAAG,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;CACpB,EACD,EAAE,MAAW;;;;;;;;GAyBd;AAED,wBAAsB,YAAY,CAChC,EACE,EAAE,GACH,EAAE;IACD,EAAE,EAAE,MAAM,CAAC;CACZ,EACD,EAAE,MAAW;;GAoCd;AAED,wBAAsB,WAAW,CAC/B,EACE,EAAE,GACH,EAAE;IACD,EAAE,EAAE,MAAM,CAAC;CACZ,EACD,EAAE,MAAW;;GAuBd;AAED,wBAAsB,eAAe,CACnC,EACE,EAAE,EACF,GAAG,EACH,MAAM,GACP,EAAE;IACD,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,EACD,EAAE,MAAW;;;;;;;;;;;;;;GAmEd"}
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, HeadObjectCommand, ListPartsCommand, AbortMultipartUploadCommand, } from "@aws-sdk/client-s3";
|
|
3
|
+
import config from "../../../config.js";
|
|
4
|
+
import s3Client from "../file/providers/s3/client.js";
|
|
5
|
+
import rclient from "../redis/client.js";
|
|
6
|
+
import logger from "../logger/getLogger.js";
|
|
7
|
+
const uploads = new Map();
|
|
8
|
+
const resumableUploads = new Map();
|
|
9
|
+
// non-redis upload timeout handlers
|
|
10
|
+
const uploadTimeouts = new Map();
|
|
11
|
+
const UPLOAD_TTL_SECONDS = 60 * 60;
|
|
12
|
+
// expire gracefully to give cleanup worker time to abort stale uploads
|
|
13
|
+
const CLEANUP_GRACE_SECONDS = 60 * 60 * 24;
|
|
14
|
+
const REDIS_TTL = UPLOAD_TTL_SECONDS + CLEANUP_GRACE_SECONDS;
|
|
15
|
+
const uploadMetaKey = (id) => `upload:${id}:meta`;
|
|
16
|
+
const uploadExpireKey = (id) => `upload:${id}:expires`;
|
|
17
|
+
const resumableKey = (key) => `resumable:${key}`;
|
|
18
|
+
function clearUploadTimeout(id) {
|
|
19
|
+
const timeout = uploadTimeouts.get(id);
|
|
20
|
+
if (timeout) {
|
|
21
|
+
clearTimeout(timeout);
|
|
22
|
+
uploadTimeouts.delete(id);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function scheduleNonRedisCleanup(id, s3 = s3Client) {
|
|
26
|
+
if (config.redis)
|
|
27
|
+
return;
|
|
28
|
+
clearUploadTimeout(id);
|
|
29
|
+
const timeout = setTimeout(async () => {
|
|
30
|
+
try {
|
|
31
|
+
const meta = uploads.get(id);
|
|
32
|
+
if (!meta)
|
|
33
|
+
return;
|
|
34
|
+
try {
|
|
35
|
+
await s3.send(new AbortMultipartUploadCommand({
|
|
36
|
+
Bucket: meta.Bucket,
|
|
37
|
+
Key: meta.Key,
|
|
38
|
+
UploadId: id,
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
console.error("AbortMultipartUpload failed", {
|
|
43
|
+
uploadId: id,
|
|
44
|
+
Bucket: meta.Bucket,
|
|
45
|
+
Key: meta.Key,
|
|
46
|
+
error: err?.message,
|
|
47
|
+
});
|
|
48
|
+
logger.file("upload/abort/error", {
|
|
49
|
+
uploadId: id,
|
|
50
|
+
Bucket: meta.Bucket,
|
|
51
|
+
Key: meta.Key,
|
|
52
|
+
error: err?.message,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
uploads.delete(id);
|
|
56
|
+
if (resumableUploads.get(meta.Key) === id) {
|
|
57
|
+
resumableUploads.delete(meta.Key);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
console.error(err);
|
|
62
|
+
logger.error(err);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
clearUploadTimeout(id);
|
|
66
|
+
}
|
|
67
|
+
}, UPLOAD_TTL_SECONDS * 1000);
|
|
68
|
+
// don't keep node process alive
|
|
69
|
+
timeout.unref?.();
|
|
70
|
+
uploadTimeouts.set(id, timeout);
|
|
71
|
+
}
|
|
72
|
+
// used via onClose hook to avoid memory leaks
|
|
73
|
+
export async function cleanupNonRedisUploads(s3 = s3Client) {
|
|
74
|
+
if (config.redis)
|
|
75
|
+
return;
|
|
76
|
+
console.log("cleanupNonRedisUploads start");
|
|
77
|
+
const entries = [...uploads.entries()];
|
|
78
|
+
for (const [id, meta] of entries) {
|
|
79
|
+
try {
|
|
80
|
+
await s3.send(new AbortMultipartUploadCommand({
|
|
81
|
+
Bucket: meta.Bucket,
|
|
82
|
+
Key: meta.Key,
|
|
83
|
+
UploadId: id,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
console.error("AbortMultipartUpload failed", {
|
|
88
|
+
uploadId: id,
|
|
89
|
+
Bucket: meta.Bucket,
|
|
90
|
+
Key: meta.Key,
|
|
91
|
+
error: err?.message,
|
|
92
|
+
});
|
|
93
|
+
logger.file("upload/abort/error", {
|
|
94
|
+
uploadId: id,
|
|
95
|
+
Bucket: meta.Bucket,
|
|
96
|
+
Key: meta.Key,
|
|
97
|
+
error: err?.message,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
clearUploadTimeout(id);
|
|
101
|
+
uploads.delete(id);
|
|
102
|
+
if (resumableUploads.get(meta.Key) === id) {
|
|
103
|
+
resumableUploads.delete(meta.Key);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
console.log("open unfinished multipart uploads closed", entries.length);
|
|
107
|
+
}
|
|
108
|
+
export async function saveUploadMeta(id, meta, ttl = UPLOAD_TTL_SECONDS) {
|
|
109
|
+
if (!config.redis)
|
|
110
|
+
return;
|
|
111
|
+
const expiresAt = Date.now() + UPLOAD_TTL_SECONDS * 1000;
|
|
112
|
+
await Promise.all([
|
|
113
|
+
rclient.set(uploadMetaKey(id), JSON.stringify(meta), "EX", REDIS_TTL),
|
|
114
|
+
rclient.set(uploadExpireKey(id), String(expiresAt), "EX", ttl),
|
|
115
|
+
rclient.set(resumableKey(meta.Key), id, "EX", ttl),
|
|
116
|
+
]);
|
|
117
|
+
}
|
|
118
|
+
export async function refreshUploadTTL(id, ttl = UPLOAD_TTL_SECONDS, s3 = s3Client) {
|
|
119
|
+
if (!config.redis) {
|
|
120
|
+
await scheduleNonRedisCleanup(id, s3);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const meta = await getUploadMeta(id);
|
|
124
|
+
if (!meta)
|
|
125
|
+
return;
|
|
126
|
+
const expiresAt = Date.now() + ttl * 1000;
|
|
127
|
+
await Promise.all([
|
|
128
|
+
rclient.expire(uploadMetaKey(id), ttl),
|
|
129
|
+
rclient.set(uploadExpireKey(id), String(expiresAt), "EX", ttl),
|
|
130
|
+
rclient.expire(resumableKey(meta.Key), ttl),
|
|
131
|
+
]);
|
|
132
|
+
}
|
|
133
|
+
export async function deleteUploadMeta(id, key) {
|
|
134
|
+
clearUploadTimeout(id);
|
|
135
|
+
if (!config.redis) {
|
|
136
|
+
const meta = uploads.get(id);
|
|
137
|
+
uploads.delete(id);
|
|
138
|
+
const redisKey = key || meta?.Key;
|
|
139
|
+
if (redisKey && resumableUploads.get(redisKey) === id) {
|
|
140
|
+
resumableUploads.delete(redisKey);
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const redisKey = key || (await getUploadMeta(id))?.Key;
|
|
145
|
+
const keys = [uploadMetaKey(id), uploadExpireKey(id)];
|
|
146
|
+
if (redisKey) {
|
|
147
|
+
keys.push(resumableKey(redisKey));
|
|
148
|
+
}
|
|
149
|
+
await rclient.del(keys);
|
|
150
|
+
}
|
|
151
|
+
export async function getUploadMeta(id) {
|
|
152
|
+
if (!config.redis) {
|
|
153
|
+
return uploads.get(id);
|
|
154
|
+
}
|
|
155
|
+
const raw = await rclient.get(uploadMetaKey(id));
|
|
156
|
+
return raw ? JSON.parse(raw) : undefined;
|
|
157
|
+
}
|
|
158
|
+
export async function getResumableUploadId(key) {
|
|
159
|
+
if (!config.redis) {
|
|
160
|
+
return resumableUploads.get(key);
|
|
161
|
+
}
|
|
162
|
+
return rclient.get(resumableKey(key));
|
|
163
|
+
}
|
|
164
|
+
export async function cleanupStaleUploads(s3 = s3Client) {
|
|
165
|
+
if (!config.redis) {
|
|
166
|
+
await cleanupNonRedisUploads(s3);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
let cursor = "0";
|
|
170
|
+
do {
|
|
171
|
+
const [nextCursor, keys] = await rclient.scan(cursor, "MATCH", "upload:*:expires", "COUNT", 100);
|
|
172
|
+
cursor = nextCursor;
|
|
173
|
+
for (const expireKey of keys) {
|
|
174
|
+
try {
|
|
175
|
+
const expiresAt = Number(await rclient.get(expireKey));
|
|
176
|
+
if (!expiresAt || expiresAt > Date.now()) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const uploadId = expireKey
|
|
180
|
+
.replace("upload:", "")
|
|
181
|
+
.replace(":expires", "");
|
|
182
|
+
const meta = await getUploadMeta(uploadId);
|
|
183
|
+
if (!meta) {
|
|
184
|
+
await rclient.del(expireKey);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const { Bucket, Key } = meta;
|
|
188
|
+
try {
|
|
189
|
+
await s3.send(new AbortMultipartUploadCommand({
|
|
190
|
+
Bucket,
|
|
191
|
+
Key,
|
|
192
|
+
UploadId: uploadId,
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
console.error("AbortMultipartUpload failed", {
|
|
197
|
+
uploadId,
|
|
198
|
+
Bucket,
|
|
199
|
+
Key,
|
|
200
|
+
error: err?.message,
|
|
201
|
+
});
|
|
202
|
+
logger.file("upload/abort/error", {
|
|
203
|
+
uploadId,
|
|
204
|
+
Bucket,
|
|
205
|
+
Key,
|
|
206
|
+
error: err?.message,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
await deleteUploadMeta(uploadId, Key);
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
console.error(err);
|
|
213
|
+
logger.error(err);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} while (cursor !== "0");
|
|
217
|
+
}
|
|
218
|
+
export async function findUpload({ id }, s3 = s3Client) {
|
|
219
|
+
const cachedInfo = await getUploadMeta(id);
|
|
220
|
+
const { Key, Bucket, fileSize: cacheSize, ContentType: cacheContentType, } = cachedInfo || {};
|
|
221
|
+
const parts = await s3
|
|
222
|
+
.send(new ListPartsCommand({
|
|
223
|
+
Bucket,
|
|
224
|
+
Key,
|
|
225
|
+
UploadId: id,
|
|
226
|
+
}))
|
|
227
|
+
.catch(() => null);
|
|
228
|
+
return {
|
|
229
|
+
UploadId: parts ? id : undefined,
|
|
230
|
+
Key,
|
|
231
|
+
Bucket,
|
|
232
|
+
fileSize: cacheSize,
|
|
233
|
+
ContentType: cacheContentType,
|
|
234
|
+
uploaded: parts?.Parts?.reduce((sum, part) => sum + (part.Size || 0), 0) || 0,
|
|
235
|
+
parts: parts?.Parts?.map((p) => ({
|
|
236
|
+
ETag: p.ETag,
|
|
237
|
+
PartNumber: p.PartNumber,
|
|
238
|
+
})) || [],
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
export async function startUpload({ Bucket, Key, ContentType, fileSize, }, s3 = s3Client) {
|
|
242
|
+
const cachedUploadId = await getResumableUploadId(Key);
|
|
243
|
+
const cachedMeta = cachedUploadId
|
|
244
|
+
? await getUploadMeta(cachedUploadId)
|
|
245
|
+
: undefined;
|
|
246
|
+
if (cachedUploadId && cachedMeta) {
|
|
247
|
+
await refreshUploadTTL(cachedUploadId, UPLOAD_TTL_SECONDS, s3);
|
|
248
|
+
return {
|
|
249
|
+
Key,
|
|
250
|
+
Bucket: cachedMeta.Bucket,
|
|
251
|
+
UploadId: cachedUploadId,
|
|
252
|
+
fileSize: cachedMeta.fileSize,
|
|
253
|
+
extension: cachedMeta.extension,
|
|
254
|
+
ContentType: cachedMeta.ContentType,
|
|
255
|
+
cache: true,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const res = await s3.send(new CreateMultipartUploadCommand({
|
|
259
|
+
Bucket,
|
|
260
|
+
Key,
|
|
261
|
+
ContentType,
|
|
262
|
+
}));
|
|
263
|
+
const extension = path.extname(Key).substring(1);
|
|
264
|
+
const meta = {
|
|
265
|
+
Key,
|
|
266
|
+
Bucket,
|
|
267
|
+
fileSize,
|
|
268
|
+
extension,
|
|
269
|
+
ContentType,
|
|
270
|
+
};
|
|
271
|
+
if (config.redis) {
|
|
272
|
+
await saveUploadMeta(res.UploadId, meta);
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
uploads.set(res.UploadId, meta);
|
|
276
|
+
resumableUploads.set(Key, res.UploadId);
|
|
277
|
+
await scheduleNonRedisCleanup(res.UploadId, s3);
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
UploadId: res.UploadId,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
export async function uploadChunk({ id, chunk, partNumber, }, s3 = s3Client) {
|
|
284
|
+
const upload = await findUpload({ id }, s3);
|
|
285
|
+
if (!upload?.UploadId) {
|
|
286
|
+
throw new Error("upload not found");
|
|
287
|
+
}
|
|
288
|
+
if (upload.parts.some((p) => p.PartNumber === partNumber)) {
|
|
289
|
+
throw new Error(`Part ${partNumber} already uploaded`);
|
|
290
|
+
}
|
|
291
|
+
await s3.send(new UploadPartCommand({
|
|
292
|
+
Bucket: upload.Bucket,
|
|
293
|
+
Key: upload.Key,
|
|
294
|
+
UploadId: upload.UploadId,
|
|
295
|
+
PartNumber: partNumber,
|
|
296
|
+
Body: chunk,
|
|
297
|
+
}));
|
|
298
|
+
await refreshUploadTTL(id, UPLOAD_TTL_SECONDS, s3);
|
|
299
|
+
return upload;
|
|
300
|
+
}
|
|
301
|
+
export async function finishUpload({ id, }, s3 = s3Client) {
|
|
302
|
+
const redisCache = await getUploadMeta(id);
|
|
303
|
+
const { fileSize: cachedSize, Key, Bucket } = redisCache || {};
|
|
304
|
+
const size = cachedSize ? Number(cachedSize) : undefined;
|
|
305
|
+
const upload = await findUpload({ id }, s3);
|
|
306
|
+
if (!upload?.uploaded) {
|
|
307
|
+
throw new Error("File not uploaded");
|
|
308
|
+
}
|
|
309
|
+
if (size && size !== upload.uploaded) {
|
|
310
|
+
throw new Error("File size mismatch");
|
|
311
|
+
}
|
|
312
|
+
await s3.send(new CompleteMultipartUploadCommand({
|
|
313
|
+
Bucket,
|
|
314
|
+
Key,
|
|
315
|
+
UploadId: id,
|
|
316
|
+
MultipartUpload: {
|
|
317
|
+
Parts: upload.parts.sort((a, b) => a.PartNumber - b.PartNumber),
|
|
318
|
+
},
|
|
319
|
+
}));
|
|
320
|
+
await deleteUploadMeta(id, Key);
|
|
321
|
+
return {
|
|
322
|
+
uploaded: upload.uploaded,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
export async function abortUpload({ id, }, s3 = s3Client) {
|
|
326
|
+
const meta = await getUploadMeta(id);
|
|
327
|
+
if (!meta) {
|
|
328
|
+
return {
|
|
329
|
+
aborted: false,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
await s3.send(new AbortMultipartUploadCommand({
|
|
333
|
+
Bucket: meta.Bucket,
|
|
334
|
+
Key: meta.Key,
|
|
335
|
+
UploadId: id,
|
|
336
|
+
}));
|
|
337
|
+
await deleteUploadMeta(id, meta.Key);
|
|
338
|
+
return {
|
|
339
|
+
aborted: true,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
export async function getUploadStatus({ id, Key, Bucket, }, s3 = s3Client) {
|
|
343
|
+
const redisCache = id ? await getUploadMeta(id) : undefined;
|
|
344
|
+
const { fileSize: cachedSize, Key: redisKey, Bucket: redisBucket, } = redisCache || {};
|
|
345
|
+
if (!redisKey && Key && Bucket) {
|
|
346
|
+
const res = await s3
|
|
347
|
+
.send(new HeadObjectCommand({
|
|
348
|
+
Bucket,
|
|
349
|
+
Key,
|
|
350
|
+
}))
|
|
351
|
+
.catch(() => false);
|
|
352
|
+
return {
|
|
353
|
+
exists: !!res,
|
|
354
|
+
uploaded: res?.ContentLength,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
const parts = await s3
|
|
358
|
+
.send(new ListPartsCommand({
|
|
359
|
+
Bucket: redisBucket,
|
|
360
|
+
Key: redisKey,
|
|
361
|
+
UploadId: id,
|
|
362
|
+
}))
|
|
363
|
+
.catch(() => ({
|
|
364
|
+
Parts: [],
|
|
365
|
+
}));
|
|
366
|
+
const res = id && !parts.Parts?.length
|
|
367
|
+
? await s3
|
|
368
|
+
.send(new HeadObjectCommand({
|
|
369
|
+
Bucket: redisBucket,
|
|
370
|
+
Key: redisKey,
|
|
371
|
+
}))
|
|
372
|
+
.catch(() => false)
|
|
373
|
+
: false;
|
|
374
|
+
return {
|
|
375
|
+
exists: !!res || !!parts?.Parts?.length,
|
|
376
|
+
finished: !!res,
|
|
377
|
+
uploaded: res
|
|
378
|
+
? res.ContentLength
|
|
379
|
+
: parts?.Parts?.reduce((acc, curr) => acc + (curr.Size || 0), 0) || 0,
|
|
380
|
+
id,
|
|
381
|
+
size: cachedSize ? Number(cachedSize) : undefined,
|
|
382
|
+
parts: parts?.Parts?.map((p) => ({
|
|
383
|
+
ETag: p.ETag,
|
|
384
|
+
PartNumber: p.PartNumber,
|
|
385
|
+
})) || [],
|
|
386
|
+
};
|
|
387
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"startUpload.d.ts","sourceRoot":"","sources":["../../../../server/plugins/upload/startUpload.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"startUpload.d.ts","sourceRoot":"","sources":["../../../../server/plugins/upload/startUpload.ts"],"names":[],"mappings":"AAiBA,wBAA8B,WAAW,CAAC,EACxC,IAAI,EACJ,EAAE,EACF,QAAQ,EACR,IAAI,EACJ,MAAM,GACP,EAAE;IACD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,gBA+EA"}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import config from "../../../config.js";
|
|
4
5
|
import getUploadStatus from "./getUploadStatus.js";
|
|
5
6
|
import { prefix, fileDir, metaDir, uploadChunkDirectory, fetchTimeoutMs, } from "./index.js";
|
|
7
|
+
import mimes from "../file/providers/mime/mimes.js";
|
|
8
|
+
import { startUpload as startUploadS3 } from "./s3.js";
|
|
6
9
|
export default async function startUpload({ host, id, fileName, size, subdir, }) {
|
|
7
10
|
if (subdir && (typeof subdir !== "string" || subdir.includes(".."))) {
|
|
8
11
|
return {
|
|
@@ -11,11 +14,25 @@ export default async function startUpload({ host, id, fileName, size, subdir, })
|
|
|
11
14
|
};
|
|
12
15
|
}
|
|
13
16
|
if (!host) {
|
|
14
|
-
const extension = path.extname(fileName).substring(1);
|
|
15
17
|
const id1 = id || randomUUID();
|
|
18
|
+
const extension = path.extname(fileName).substring(1);
|
|
19
|
+
const relativeDirpath = path
|
|
20
|
+
.join(uploadChunkDirectory, subdir || "")
|
|
21
|
+
.replace(/\\/g, "/");
|
|
22
|
+
if (config.s3) {
|
|
23
|
+
const filepath = path.posix.join(config.folder || '', relativeDirpath, `${id1}.${extension}`);
|
|
24
|
+
const { UploadId } = await startUploadS3({
|
|
25
|
+
Bucket: config.s3?.containerName || "work",
|
|
26
|
+
Key: filepath[0] === "/" ? filepath?.slice(1) : filepath,
|
|
27
|
+
ContentType: mimes[extension],
|
|
28
|
+
fileSize: size,
|
|
29
|
+
});
|
|
30
|
+
return { id: UploadId, UploadId, provider: "s3" };
|
|
31
|
+
}
|
|
16
32
|
const key = id1.split("-").pop();
|
|
17
33
|
const meta = {
|
|
18
34
|
id: id1,
|
|
35
|
+
UploadId: id1,
|
|
19
36
|
key,
|
|
20
37
|
fileName,
|
|
21
38
|
size,
|
|
@@ -33,9 +50,6 @@ export default async function startUpload({ host, id, fileName, size, subdir, })
|
|
|
33
50
|
return meta;
|
|
34
51
|
}
|
|
35
52
|
// if not started - start new upload
|
|
36
|
-
const relativeDirpath = path
|
|
37
|
-
.join(uploadChunkDirectory, subdir || "")
|
|
38
|
-
.replace(/\\/g, "/");
|
|
39
53
|
await mkdir(path.join(fileDir, "tmp"), { recursive: true });
|
|
40
54
|
// create metadata for resumable upload
|
|
41
55
|
const metaData = {
|
|
@@ -43,6 +57,7 @@ export default async function startUpload({ host, id, fileName, size, subdir, })
|
|
|
43
57
|
subdir,
|
|
44
58
|
metaPath: undefined,
|
|
45
59
|
relativeDirpath,
|
|
60
|
+
provider: "fs"
|
|
46
61
|
};
|
|
47
62
|
await writeFile(path.join(metaDir, `${id1}.json`), JSON.stringify(metaData, null, 2));
|
|
48
63
|
return { ...meta, metaPath: undefined };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uploadChunk.d.ts","sourceRoot":"","sources":["../../../../server/plugins/upload/uploadChunk.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"uploadChunk.d.ts","sourceRoot":"","sources":["../../../../server/plugins/upload/uploadChunk.ts"],"names":[],"mappings":"AAqBA,wBAA8B,WAAW,CAAC,EACxC,IAAI,EACJ,EAAE,EACF,IAAI,EACJ,MAAM,EACN,GAAG,EACH,IAAI,GACL,EAAE;IACD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,GAAG,CAAC;IACV,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd,gBAiIA"}
|
|
@@ -2,10 +2,28 @@ import path from "node:path";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { prefix, metaDir, fetchTimeoutMs, uploadChunkDirectory, } from "./index.js";
|
|
5
|
+
import config from "../../../config.js";
|
|
5
6
|
import uploadFile from "../file/uploadFile.js";
|
|
6
7
|
import applyHook from "../hook/applyHook.js";
|
|
8
|
+
import getFileSize from "../file/getFileSize.js";
|
|
9
|
+
import { finishUpload as finishUploadS3, uploadChunk as uploadChunkS3, getUploadStatus as getUploadStatusS3, findUpload as findUploadS3, } from "./s3.js";
|
|
7
10
|
export default async function uploadChunk({ host, id, body, offset, end, size, }) {
|
|
8
11
|
if (!host) {
|
|
12
|
+
if (config.s3) {
|
|
13
|
+
const partNumber = Math.floor(offset / config.chunkSize) + 1;
|
|
14
|
+
const { fileSize } = await findUploadS3({ id });
|
|
15
|
+
await uploadChunkS3({
|
|
16
|
+
id,
|
|
17
|
+
chunk: body,
|
|
18
|
+
partNumber,
|
|
19
|
+
});
|
|
20
|
+
const currentState = await getUploadStatusS3({ id });
|
|
21
|
+
const finished = currentState.uploaded === fileSize;
|
|
22
|
+
if (finished) {
|
|
23
|
+
return finishUploadS3({ id });
|
|
24
|
+
}
|
|
25
|
+
return currentState;
|
|
26
|
+
}
|
|
9
27
|
const metaExists = existsSync(path.join(metaDir, `${id}.json`));
|
|
10
28
|
if (!metaExists) {
|
|
11
29
|
return { error: "File not found", code: 404 };
|
|
@@ -20,6 +38,17 @@ export default async function uploadChunk({ host, id, body, offset, end, size, }
|
|
|
20
38
|
if (body?.length !== end - offset + 1) {
|
|
21
39
|
return { error: "Chunk size mismatch", code: 400 };
|
|
22
40
|
}
|
|
41
|
+
const relpath = path.posix.join(uploadChunkDirectory, `${id}.${meta.extension}`);
|
|
42
|
+
const filepath = path.posix.join(config.folder, uploadChunkDirectory, `${id}.${meta.extension}`);
|
|
43
|
+
const res = await getFileSize(relpath, { fallback: false }).catch((err) => ({
|
|
44
|
+
error: err.toString(),
|
|
45
|
+
}));
|
|
46
|
+
if (res.error && res.error !== "Error: file not found") {
|
|
47
|
+
throw new Error(res.error);
|
|
48
|
+
}
|
|
49
|
+
if (meta.size > res) {
|
|
50
|
+
throw new Error("uploaded size exceeds file size");
|
|
51
|
+
}
|
|
23
52
|
// append chunk to existing file
|
|
24
53
|
await uploadFile(path
|
|
25
54
|
.join(uploadChunkDirectory, `${id}.${meta.extension}`)
|
|
@@ -47,10 +76,15 @@ export default async function uploadChunk({ host, id, body, offset, end, size, }
|
|
|
47
76
|
if (!end || !size) {
|
|
48
77
|
return { error: "not enough params: offset/end/size", code: 400 };
|
|
49
78
|
}
|
|
50
|
-
const
|
|
79
|
+
const url = `${host}/${prefix}/${id}`;
|
|
80
|
+
const range = `bytes ${offset || 0}-${end}/${size}`;
|
|
81
|
+
if (config.trace || process.env.NODE_ENV === "test") {
|
|
82
|
+
console.log("PATCH request", url, range, body.length);
|
|
83
|
+
}
|
|
84
|
+
const res = await fetch(url, {
|
|
51
85
|
method: "PATCH",
|
|
52
86
|
headers: {
|
|
53
|
-
"Content-Range":
|
|
87
|
+
"Content-Range": range,
|
|
54
88
|
},
|
|
55
89
|
body,
|
|
56
90
|
signal: AbortSignal.timeout(fetchTimeoutMs),
|