@blocklet/uploader-server 0.1.49
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 +13 -0
- package/README.md +101 -0
- package/es/index.d.ts +1 -0
- package/es/index.js +1 -0
- package/es/middlewares/companion.d.ts +6 -0
- package/es/middlewares/companion.js +92 -0
- package/es/middlewares/local-storage.d.ts +18 -0
- package/es/middlewares/local-storage.js +374 -0
- package/es/middlewares/static-resource.d.ts +11 -0
- package/es/middlewares/static-resource.js +107 -0
- package/es/middlewares.d.ts +3 -0
- package/es/middlewares.js +3 -0
- package/es/types.d.ts +0 -0
- package/es/types.js +0 -0
- package/es/utils.d.ts +0 -0
- package/es/utils.js +0 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +16 -0
- package/lib/middlewares/companion.d.ts +6 -0
- package/lib/middlewares/companion.js +104 -0
- package/lib/middlewares/local-storage.d.ts +18 -0
- package/lib/middlewares/local-storage.js +423 -0
- package/lib/middlewares/static-resource.d.ts +11 -0
- package/lib/middlewares/static-resource.js +133 -0
- package/lib/middlewares.d.ts +3 -0
- package/lib/middlewares.js +38 -0
- package/lib/types.d.ts +0 -0
- package/lib/types.js +1 -0
- package/lib/utils.d.ts +0 -0
- package/lib/utils.js +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const { existsSync } = require("fs");
|
|
2
|
+
const { join } = require("path");
|
|
3
|
+
const config = require("@blocklet/sdk/lib/config");
|
|
4
|
+
const { getResources } = require("@blocklet/sdk/lib/component");
|
|
5
|
+
const logger = console;
|
|
6
|
+
const ImgResourceType = "imgpack";
|
|
7
|
+
const ImageBinDid = "z8ia1mAXo8ZE7ytGF36L5uBf9kD2kenhqFGp9";
|
|
8
|
+
let skipRunningCheck = false;
|
|
9
|
+
let mediaKitInfo = null;
|
|
10
|
+
let resourceTypes = [
|
|
11
|
+
{
|
|
12
|
+
type: ImgResourceType,
|
|
13
|
+
did: ImageBinDid,
|
|
14
|
+
folder: ""
|
|
15
|
+
// default is root, can be set to 'public' or 'assets' or any other folder
|
|
16
|
+
}
|
|
17
|
+
];
|
|
18
|
+
let canUseResources = [];
|
|
19
|
+
export const mappingResource = async () => {
|
|
20
|
+
mediaKitInfo = config.components.find((item) => item.did === ImageBinDid);
|
|
21
|
+
if (mediaKitInfo) {
|
|
22
|
+
mediaKitInfo.uploadsDir = config.env.dataDir.replace(/\/[^/]*$/, "/image-bin/uploads");
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const resources = getResources({
|
|
26
|
+
types: resourceTypes,
|
|
27
|
+
skipRunningCheck
|
|
28
|
+
});
|
|
29
|
+
canUseResources = resources.map((resource) => {
|
|
30
|
+
const originDir = resource.path;
|
|
31
|
+
const resourceType = resourceTypes.find(({ type }) => originDir.endsWith(type));
|
|
32
|
+
if (!existsSync(originDir) || !resourceType) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const { folder = "" } = resourceType;
|
|
36
|
+
return { originDir, dir: join(originDir, folder || ""), blockletInfo: resource };
|
|
37
|
+
}).filter(Boolean);
|
|
38
|
+
logger.info(
|
|
39
|
+
"Mapping can use resources count: ",
|
|
40
|
+
canUseResources.length
|
|
41
|
+
// canUseResources
|
|
42
|
+
);
|
|
43
|
+
return canUseResources;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error(error);
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
};
|
|
49
|
+
const { events, Events } = config;
|
|
50
|
+
events.on(Events.componentAdded, () => mappingResource());
|
|
51
|
+
events.on(Events.componentRemoved, () => mappingResource());
|
|
52
|
+
events.on(Events.componentStarted, () => mappingResource());
|
|
53
|
+
events.on(Events.componentStopped, () => mappingResource());
|
|
54
|
+
events.on(Events.componentUpdated, () => mappingResource());
|
|
55
|
+
export const initStaticResourceMiddleware = ({
|
|
56
|
+
options,
|
|
57
|
+
resourceTypes: _resourceTypes = resourceTypes,
|
|
58
|
+
express,
|
|
59
|
+
skipRunningCheck: _skipRunningCheck
|
|
60
|
+
} = {}) => {
|
|
61
|
+
skipRunningCheck = !!_skipRunningCheck;
|
|
62
|
+
if (_resourceTypes?.length > 0) {
|
|
63
|
+
resourceTypes = _resourceTypes.map((item) => {
|
|
64
|
+
if (typeof item === "string") {
|
|
65
|
+
return {
|
|
66
|
+
type: item,
|
|
67
|
+
did: ImageBinDid,
|
|
68
|
+
// not set did, default is ImageBinDid
|
|
69
|
+
folder: ""
|
|
70
|
+
// not set folder, default is root
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return item;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
mappingResource();
|
|
77
|
+
return (req, res, next) => {
|
|
78
|
+
const urlPath = new URL(`http://localhost${req.url}`).pathname;
|
|
79
|
+
const matchCanUseResourceItem = canUseResources.find((item) => existsSync(join(item.dir, urlPath)));
|
|
80
|
+
if (matchCanUseResourceItem) {
|
|
81
|
+
express.static(matchCanUseResourceItem.dir, {
|
|
82
|
+
maxAge: "365d",
|
|
83
|
+
immutable: true,
|
|
84
|
+
index: false,
|
|
85
|
+
// fallthrough: false,
|
|
86
|
+
...options
|
|
87
|
+
})(req, res, next);
|
|
88
|
+
} else {
|
|
89
|
+
res.status(404).end();
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
export const getCanUseResources = () => canUseResources;
|
|
94
|
+
export const initProxyToMediaKitUploadsMiddleware = ({ options, express } = {}) => {
|
|
95
|
+
return (req, res, next) => {
|
|
96
|
+
if (!mediaKitInfo?.uploadsDir) {
|
|
97
|
+
return next();
|
|
98
|
+
}
|
|
99
|
+
return express.static(mediaKitInfo.uploadsDir, {
|
|
100
|
+
maxAge: "365d",
|
|
101
|
+
immutable: true,
|
|
102
|
+
index: false,
|
|
103
|
+
// fallthrough: false,
|
|
104
|
+
...options
|
|
105
|
+
})(req, res, next);
|
|
106
|
+
};
|
|
107
|
+
};
|
package/es/types.d.ts
ADDED
|
File without changes
|
package/es/types.js
ADDED
|
File without changes
|
package/es/utils.d.ts
ADDED
|
File without changes
|
package/es/utils.js
ADDED
|
File without changes
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './middlewares';
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
var _middlewares = require("./middlewares");
|
|
7
|
+
Object.keys(_middlewares).forEach(function (key) {
|
|
8
|
+
if (key === "default" || key === "__esModule") return;
|
|
9
|
+
if (key in exports && exports[key] === _middlewares[key]) return;
|
|
10
|
+
Object.defineProperty(exports, key, {
|
|
11
|
+
enumerable: true,
|
|
12
|
+
get: function () {
|
|
13
|
+
return _middlewares[key];
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.initCompanion = initCompanion;
|
|
7
|
+
exports.proxyImageDownload = proxyImageDownload;
|
|
8
|
+
const companion = require("@uppy/companion");
|
|
9
|
+
const bodyParser = require("body-parser");
|
|
10
|
+
const session = require("express-session");
|
|
11
|
+
const axios = require("axios");
|
|
12
|
+
const crypto = require("crypto");
|
|
13
|
+
const secret = crypto.randomBytes(32).toString("hex");
|
|
14
|
+
function initCompanion({
|
|
15
|
+
path,
|
|
16
|
+
express,
|
|
17
|
+
providerOptions,
|
|
18
|
+
...restProps
|
|
19
|
+
}) {
|
|
20
|
+
const app = express();
|
|
21
|
+
app.use(bodyParser.json());
|
|
22
|
+
app.use(session({
|
|
23
|
+
secret
|
|
24
|
+
}));
|
|
25
|
+
app.use("/proxy", proxyImageDownload);
|
|
26
|
+
let dynamicProviderOptions = providerOptions;
|
|
27
|
+
const companionOptions = {
|
|
28
|
+
secret,
|
|
29
|
+
providerOptions,
|
|
30
|
+
// unused
|
|
31
|
+
server: {
|
|
32
|
+
protocol: "https",
|
|
33
|
+
host: "UNUSED_HOST",
|
|
34
|
+
// unused
|
|
35
|
+
path: "UNUSED_PATH"
|
|
36
|
+
// unused
|
|
37
|
+
},
|
|
38
|
+
filePath: path,
|
|
39
|
+
streamingUpload: true,
|
|
40
|
+
metrics: false,
|
|
41
|
+
...restProps
|
|
42
|
+
};
|
|
43
|
+
const newCompanion = companion.app(companionOptions);
|
|
44
|
+
const {
|
|
45
|
+
app: companionApp
|
|
46
|
+
} = newCompanion;
|
|
47
|
+
app.all("*", (req, res, next) => {
|
|
48
|
+
let hackerCompanion = {};
|
|
49
|
+
Object.defineProperty(req, "companion", {
|
|
50
|
+
get() {
|
|
51
|
+
return hackerCompanion;
|
|
52
|
+
},
|
|
53
|
+
set(value) {
|
|
54
|
+
hackerCompanion = value;
|
|
55
|
+
hackerCompanion.options.providerOptions = dynamicProviderOptions;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
next();
|
|
59
|
+
}, companionApp);
|
|
60
|
+
newCompanion.handle = app;
|
|
61
|
+
newCompanion.setProviderOptions = options => {
|
|
62
|
+
dynamicProviderOptions = options;
|
|
63
|
+
};
|
|
64
|
+
return newCompanion;
|
|
65
|
+
}
|
|
66
|
+
async function proxyImageDownload(req, res, next) {
|
|
67
|
+
let {
|
|
68
|
+
url,
|
|
69
|
+
responseType = "stream"
|
|
70
|
+
} = {
|
|
71
|
+
...req.query,
|
|
72
|
+
...req.body
|
|
73
|
+
};
|
|
74
|
+
if (url) {
|
|
75
|
+
url = encodeURI(url);
|
|
76
|
+
try {
|
|
77
|
+
const {
|
|
78
|
+
headers,
|
|
79
|
+
data,
|
|
80
|
+
status
|
|
81
|
+
} = await axios.get(url, {
|
|
82
|
+
responseType
|
|
83
|
+
});
|
|
84
|
+
if (data && status >= 200 && status < 302) {
|
|
85
|
+
res.setHeader("Content-Type", headers["content-type"]);
|
|
86
|
+
try {} catch (error) {}
|
|
87
|
+
if (responseType === "stream") {
|
|
88
|
+
data.pipe(res);
|
|
89
|
+
} else if (responseType === "arraybuffer") {
|
|
90
|
+
res.end(data?.toString?.("binary"), "binary");
|
|
91
|
+
} else {
|
|
92
|
+
res.send(data);
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
throw new Error("download image error");
|
|
96
|
+
}
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error("Proxy url failed: ", err);
|
|
99
|
+
res.status(500).send("Proxy url failed");
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
res.status(500).send('Parameter "url" is required');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ServerOptions } from '@tus/server';
|
|
2
|
+
export declare function initLocalStorageServer({ path: _path, onUploadFinish: _onUploadFinish, onUploadCreate: _onUploadCreate, express, expiredUploadTime, // default 3 days expire
|
|
3
|
+
...restProps }: ServerOptions & {
|
|
4
|
+
path: string;
|
|
5
|
+
onUploadFinish?: Function;
|
|
6
|
+
onUploadCreate?: Function;
|
|
7
|
+
express: Function;
|
|
8
|
+
expiredUploadTime?: Number;
|
|
9
|
+
}): any;
|
|
10
|
+
export declare const getFileName: (req: any) => any;
|
|
11
|
+
export declare function getFileNameParam(req: any, res: any, { isRequired }?: {
|
|
12
|
+
isRequired: boolean;
|
|
13
|
+
}): any;
|
|
14
|
+
export declare function getLocalStorageFile({ server }: any): (req: any, res: any, next: any) => Promise<void>;
|
|
15
|
+
export declare function setHeaders(req: any, res: any, next?: Function): void;
|
|
16
|
+
export declare function fileExistBeforeUpload(req: any, res: any, next?: Function): Promise<void>;
|
|
17
|
+
export declare function getMetaDataByFilePath(filePath: string): Promise<any>;
|
|
18
|
+
export declare function joinUrl(...args: string[]): any;
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.fileExistBeforeUpload = fileExistBeforeUpload;
|
|
7
|
+
exports.getFileName = void 0;
|
|
8
|
+
exports.getFileNameParam = getFileNameParam;
|
|
9
|
+
exports.getLocalStorageFile = getLocalStorageFile;
|
|
10
|
+
exports.getMetaDataByFilePath = getMetaDataByFilePath;
|
|
11
|
+
exports.initLocalStorageServer = initLocalStorageServer;
|
|
12
|
+
exports.joinUrl = joinUrl;
|
|
13
|
+
exports.setHeaders = setHeaders;
|
|
14
|
+
const {
|
|
15
|
+
Server,
|
|
16
|
+
EVENTS
|
|
17
|
+
} = require("@tus/server");
|
|
18
|
+
const {
|
|
19
|
+
FileStore
|
|
20
|
+
} = require("@tus/file-store");
|
|
21
|
+
const cron = require("@abtnode/cron");
|
|
22
|
+
const fs = require("fs").promises;
|
|
23
|
+
const path = require("path");
|
|
24
|
+
const crypto = require("crypto");
|
|
25
|
+
const mime = require("mime-types");
|
|
26
|
+
const joinUrlLib = require("url-join");
|
|
27
|
+
const {
|
|
28
|
+
default: queue
|
|
29
|
+
} = require("p-queue");
|
|
30
|
+
const validFilePathInDirPath = (dirPath, filePath) => {
|
|
31
|
+
const fileName = path.basename(filePath);
|
|
32
|
+
if (!filePath.startsWith(dirPath) || path.join(dirPath, fileName) !== filePath) {
|
|
33
|
+
console.error("Invalid file path: ", filePath);
|
|
34
|
+
throw new Error("Invalid file path");
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
};
|
|
38
|
+
function initLocalStorageServer({
|
|
39
|
+
path: _path,
|
|
40
|
+
onUploadFinish: _onUploadFinish,
|
|
41
|
+
onUploadCreate: _onUploadCreate,
|
|
42
|
+
express,
|
|
43
|
+
expiredUploadTime = 1e3 * 60 * 60 * 24 * 3,
|
|
44
|
+
// default 3 days expire
|
|
45
|
+
...restProps
|
|
46
|
+
}) {
|
|
47
|
+
const app = express();
|
|
48
|
+
const configstore = new RewriteFileConfigstore(_path);
|
|
49
|
+
const datastore = new RewriteFileStore({
|
|
50
|
+
directory: _path,
|
|
51
|
+
expirationPeriodInMilliseconds: expiredUploadTime,
|
|
52
|
+
configstore
|
|
53
|
+
});
|
|
54
|
+
const formatMetadata = uploadMetadata => {
|
|
55
|
+
const cloneUploadMetadata = {
|
|
56
|
+
...uploadMetadata
|
|
57
|
+
};
|
|
58
|
+
if (cloneUploadMetadata.metadata?.name?.indexOf("/") > -1 && cloneUploadMetadata.metadata?.relativePath?.indexOf("/") > -1) {
|
|
59
|
+
cloneUploadMetadata.metadata.name = cloneUploadMetadata.metadata.name.split("/").pop();
|
|
60
|
+
cloneUploadMetadata.metadata.filename = cloneUploadMetadata.metadata.name;
|
|
61
|
+
}
|
|
62
|
+
if (cloneUploadMetadata.id && !cloneUploadMetadata.runtime) {
|
|
63
|
+
const {
|
|
64
|
+
id,
|
|
65
|
+
metadata,
|
|
66
|
+
size
|
|
67
|
+
} = cloneUploadMetadata;
|
|
68
|
+
cloneUploadMetadata.runtime = {
|
|
69
|
+
relativePath: metadata?.relativePath,
|
|
70
|
+
absolutePath: path.join(_path, id),
|
|
71
|
+
size,
|
|
72
|
+
hashFileName: id,
|
|
73
|
+
originFileName: metadata?.filename,
|
|
74
|
+
type: metadata?.type,
|
|
75
|
+
fileType: metadata?.filetype
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return cloneUploadMetadata;
|
|
79
|
+
};
|
|
80
|
+
const rewriteMetaDataFile = async uploadMetadata => {
|
|
81
|
+
uploadMetadata = formatMetadata(uploadMetadata);
|
|
82
|
+
const {
|
|
83
|
+
id
|
|
84
|
+
} = uploadMetadata;
|
|
85
|
+
if (!id) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const oldMetadata = formatMetadata(await configstore.get(id));
|
|
89
|
+
if (JSON.stringify(oldMetadata) !== JSON.stringify(uploadMetadata)) {
|
|
90
|
+
await configstore.set(id, uploadMetadata);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const onUploadCreate = async (req, res, uploadMetadata) => {
|
|
94
|
+
uploadMetadata = formatMetadata(uploadMetadata);
|
|
95
|
+
await rewriteMetaDataFile(uploadMetadata);
|
|
96
|
+
if (uploadMetadata.offset === 0 && uploadMetadata.size === 0) {
|
|
97
|
+
res.status(200);
|
|
98
|
+
res.setHeader("Location", joinUrl(req.headers["x-uploader-base-url"], uploadMetadata.id));
|
|
99
|
+
res.setHeader("Upload-Offset", 0);
|
|
100
|
+
res.setHeader("Upload-Length", 0);
|
|
101
|
+
res.setHeader("x-uploader-file-exist", true);
|
|
102
|
+
}
|
|
103
|
+
if (_onUploadCreate) {
|
|
104
|
+
const result = await _onUploadCreate(req, res, uploadMetadata);
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
return res;
|
|
108
|
+
};
|
|
109
|
+
const onUploadFinish = async (req, res, uploadMetadata) => {
|
|
110
|
+
res.setHeader("x-uploader-file-exist", true);
|
|
111
|
+
uploadMetadata = formatMetadata(uploadMetadata);
|
|
112
|
+
await rewriteMetaDataFile(uploadMetadata);
|
|
113
|
+
if (_onUploadFinish) {
|
|
114
|
+
try {
|
|
115
|
+
const result = await _onUploadFinish(req, res, uploadMetadata);
|
|
116
|
+
return result;
|
|
117
|
+
} catch (err) {
|
|
118
|
+
console.error("@blocklet/uploader: onUploadFinish error: ", err);
|
|
119
|
+
newServer.delete(uploadMetadata.id);
|
|
120
|
+
res.setHeader("x-uploader-file-exist", false);
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return res;
|
|
125
|
+
};
|
|
126
|
+
const newServer = new Server({
|
|
127
|
+
path: "/",
|
|
128
|
+
// UNUSED
|
|
129
|
+
relativeLocation: true,
|
|
130
|
+
// respectForwardedHeaders: true,
|
|
131
|
+
namingFunction: req => {
|
|
132
|
+
const fileName = getFileName(req);
|
|
133
|
+
const filePath = path.join(_path, fileName);
|
|
134
|
+
validFilePathInDirPath(_path, filePath);
|
|
135
|
+
return fileName;
|
|
136
|
+
},
|
|
137
|
+
datastore,
|
|
138
|
+
onUploadFinish: async (req, res, uploadMetadata) => {
|
|
139
|
+
uploadMetadata = formatMetadata(uploadMetadata);
|
|
140
|
+
const result = await onUploadFinish(req, res, uploadMetadata);
|
|
141
|
+
if (result && !result.send) {
|
|
142
|
+
const body = typeof result === "string" ? result : JSON.stringify(result);
|
|
143
|
+
throw {
|
|
144
|
+
body,
|
|
145
|
+
status_code: 200
|
|
146
|
+
};
|
|
147
|
+
} else {
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
onUploadCreate,
|
|
152
|
+
...restProps
|
|
153
|
+
});
|
|
154
|
+
app.use((req, res, next) => {
|
|
155
|
+
req.uploaderProps = {
|
|
156
|
+
server: newServer,
|
|
157
|
+
onUploadFinish,
|
|
158
|
+
onUploadCreate
|
|
159
|
+
};
|
|
160
|
+
next();
|
|
161
|
+
});
|
|
162
|
+
cron.init({
|
|
163
|
+
context: {},
|
|
164
|
+
jobs: [{
|
|
165
|
+
name: "auto-cleanup-expired-uploads",
|
|
166
|
+
time: "0 0 * * * *",
|
|
167
|
+
// each hour
|
|
168
|
+
fn: () => {
|
|
169
|
+
try {
|
|
170
|
+
newServer.cleanUpExpiredUploads().then(count => {
|
|
171
|
+
console.info(`@blocklet/uploader: cleanup expired uploads done: ${count}`);
|
|
172
|
+
}).catch(err => {
|
|
173
|
+
console.error(`@blocklet/uploader: cleanup expired uploads error`, err);
|
|
174
|
+
});
|
|
175
|
+
} catch (err) {
|
|
176
|
+
console.error(`@blocklet/uploader: cleanup expired uploads error`, err);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
options: {
|
|
180
|
+
runOnInit: false
|
|
181
|
+
}
|
|
182
|
+
}],
|
|
183
|
+
onError: err => {
|
|
184
|
+
console.error("@blocklet/uploader: cleanup job failed", err);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
newServer.delete = async key => {
|
|
188
|
+
try {
|
|
189
|
+
await configstore.delete(key);
|
|
190
|
+
await configstore.delete(key, false);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
console.error("@blocklet/uploader: delete error: ", err);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
newServer.on(EVENTS.POST_RECEIVE, async (req, res, uploadMetadata) => {
|
|
196
|
+
uploadMetadata = formatMetadata(uploadMetadata);
|
|
197
|
+
await rewriteMetaDataFile(uploadMetadata);
|
|
198
|
+
});
|
|
199
|
+
app.all("*", setHeaders, fileExistBeforeUpload, newServer.handle.bind(newServer));
|
|
200
|
+
newServer.handle = app;
|
|
201
|
+
return newServer;
|
|
202
|
+
}
|
|
203
|
+
const getFileName = req => {
|
|
204
|
+
const ext = req.headers["x-uploader-file-ext"];
|
|
205
|
+
const randomName = `${crypto.randomBytes(16).toString("hex")}${ext ? `.${ext}` : ""}`;
|
|
206
|
+
return req.headers["x-uploader-file-name"] || randomName;
|
|
207
|
+
};
|
|
208
|
+
exports.getFileName = getFileName;
|
|
209
|
+
function getFileNameParam(req, res, {
|
|
210
|
+
isRequired = true
|
|
211
|
+
} = {}) {
|
|
212
|
+
let {
|
|
213
|
+
fileName
|
|
214
|
+
} = req.params;
|
|
215
|
+
if (!fileName) {
|
|
216
|
+
fileName = req.originalUrl.replace(req.baseUrl, "");
|
|
217
|
+
}
|
|
218
|
+
if (!fileName && isRequired) {
|
|
219
|
+
res.status(400).json({
|
|
220
|
+
error: 'Parameter "fileName" is required'
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
return fileName;
|
|
225
|
+
}
|
|
226
|
+
function getLocalStorageFile({
|
|
227
|
+
server
|
|
228
|
+
}) {
|
|
229
|
+
return async (req, res, next) => {
|
|
230
|
+
const fileName = getFileNameParam(req, res);
|
|
231
|
+
const filePath = path.join(server.datastore.directory, fileName);
|
|
232
|
+
validFilePathInDirPath(server.datastore.directory, filePath);
|
|
233
|
+
const fileExists = await fs.stat(filePath).catch(() => false);
|
|
234
|
+
if (!fileExists) {
|
|
235
|
+
res.status(404).json({
|
|
236
|
+
error: "file not found"
|
|
237
|
+
});
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
setHeaders(req, res);
|
|
241
|
+
const file = await fs.readFile(filePath);
|
|
242
|
+
res.send(file);
|
|
243
|
+
next?.();
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function setHeaders(req, res, next) {
|
|
247
|
+
let {
|
|
248
|
+
method
|
|
249
|
+
} = req;
|
|
250
|
+
method = method.toUpperCase();
|
|
251
|
+
const fileName = getFileNameParam(req, res, {
|
|
252
|
+
isRequired: false
|
|
253
|
+
});
|
|
254
|
+
if (req.headers["x-uploader-endpoint-url"]) {
|
|
255
|
+
const query = new URL(req.headers["x-uploader-endpoint-url"]).searchParams;
|
|
256
|
+
req.query = {
|
|
257
|
+
...Object.fromEntries(query),
|
|
258
|
+
// query params convert to object
|
|
259
|
+
...req.query
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
if (method === "POST" && req.headers["x-uploader-base-url"]) {
|
|
263
|
+
req.baseUrl = req.headers["x-uploader-base-url"];
|
|
264
|
+
}
|
|
265
|
+
if (method === "GET" && fileName) {
|
|
266
|
+
const contentType = mime.lookup(fileName);
|
|
267
|
+
if (contentType) {
|
|
268
|
+
res.setHeader("Content-Type", contentType);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
next?.();
|
|
272
|
+
}
|
|
273
|
+
async function fileExistBeforeUpload(req, res, next) {
|
|
274
|
+
let {
|
|
275
|
+
method,
|
|
276
|
+
uploaderProps
|
|
277
|
+
} = req;
|
|
278
|
+
method = method.toUpperCase();
|
|
279
|
+
if (["PATCH", "POST"].includes(method)) {
|
|
280
|
+
const _path = uploaderProps.server.datastore.directory;
|
|
281
|
+
const fileName = getFileName(req);
|
|
282
|
+
const filePath = path.join(_path, fileName);
|
|
283
|
+
validFilePathInDirPath(_path, filePath);
|
|
284
|
+
const isExist = await fs.stat(filePath).catch(() => false);
|
|
285
|
+
if (isExist) {
|
|
286
|
+
const metaData = await getMetaDataByFilePath(filePath);
|
|
287
|
+
if (isExist?.size >= 0 && isExist?.size === metaData?.size) {
|
|
288
|
+
const prepareUpload = method === "POST";
|
|
289
|
+
if (prepareUpload) {
|
|
290
|
+
res.status(200);
|
|
291
|
+
res.setHeader("Location", joinUrl(req.headers["x-uploader-base-url"], fileName));
|
|
292
|
+
res.setHeader("Upload-Offset", +metaData.offset);
|
|
293
|
+
res.setHeader("Upload-Length", +metaData.size);
|
|
294
|
+
}
|
|
295
|
+
if (req.headers["x-uploader-metadata"]) {
|
|
296
|
+
try {
|
|
297
|
+
const realMetaData = JSON.parse(req.headers["x-uploader-metadata"], (key, value) => {
|
|
298
|
+
if (typeof value === "string") {
|
|
299
|
+
return decodeURIComponent(value);
|
|
300
|
+
}
|
|
301
|
+
return value;
|
|
302
|
+
});
|
|
303
|
+
metaData.metadata = {
|
|
304
|
+
...metaData.metadata,
|
|
305
|
+
...realMetaData
|
|
306
|
+
};
|
|
307
|
+
} catch (err) {
|
|
308
|
+
console.error("@blocklet/uploader: parse metadata error: ", err);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const uploadResult = await uploaderProps.onUploadFinish(req, res, metaData);
|
|
312
|
+
res.json(uploadResult);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
next?.();
|
|
318
|
+
}
|
|
319
|
+
async function getMetaDataByFilePath(filePath) {
|
|
320
|
+
const metaDataPath = `${filePath}.json`;
|
|
321
|
+
const isExist = await fs.stat(filePath).catch(() => false);
|
|
322
|
+
if (isExist) {
|
|
323
|
+
try {
|
|
324
|
+
const metaData = await fs.readFile(metaDataPath, "utf-8");
|
|
325
|
+
const metaDataJson = JSON.parse(metaData);
|
|
326
|
+
return metaDataJson;
|
|
327
|
+
} catch (err) {
|
|
328
|
+
console.error("@blocklet/uploader: getMetaDataByPath error: ", err);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
function joinUrl(...args) {
|
|
334
|
+
const realArgs = args.filter(Boolean).map(item => {
|
|
335
|
+
if (item === "/") {
|
|
336
|
+
return "";
|
|
337
|
+
}
|
|
338
|
+
return item;
|
|
339
|
+
});
|
|
340
|
+
return joinUrlLib(...realArgs);
|
|
341
|
+
}
|
|
342
|
+
class RewriteFileConfigstore {
|
|
343
|
+
directory;
|
|
344
|
+
queue;
|
|
345
|
+
constructor(path2) {
|
|
346
|
+
this.directory = path2;
|
|
347
|
+
this.queue = new queue({
|
|
348
|
+
concurrency: 1
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
async get(key) {
|
|
352
|
+
try {
|
|
353
|
+
const buffer = await this.queue.add(() => fs.readFile(this.resolve(key), "utf8"));
|
|
354
|
+
const metadata = JSON.parse(buffer);
|
|
355
|
+
if (metadata.offset !== metadata.size) {
|
|
356
|
+
const info = await fs.stat(this.resolve(key, false)).catch(() => false);
|
|
357
|
+
if (info?.size !== metadata?.offset) {
|
|
358
|
+
metadata.offset = info.size;
|
|
359
|
+
this.set(key, metadata);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return metadata;
|
|
363
|
+
} catch {
|
|
364
|
+
return void 0;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async set(key, value) {
|
|
368
|
+
if (value?.runtime) {
|
|
369
|
+
delete value.runtime;
|
|
370
|
+
}
|
|
371
|
+
if (value?.metadata?.runtime) {
|
|
372
|
+
delete value.metadata.runtime;
|
|
373
|
+
}
|
|
374
|
+
await this.queue.add(() => fs.writeFile(this.resolve(key), JSON.stringify(value)));
|
|
375
|
+
}
|
|
376
|
+
async safeDeleteFile(filePath) {
|
|
377
|
+
validFilePathInDirPath(this.directory, filePath);
|
|
378
|
+
try {
|
|
379
|
+
const isExist = await fs.stat(filePath).catch(() => false);
|
|
380
|
+
if (isExist) {
|
|
381
|
+
await fs.rm(filePath);
|
|
382
|
+
} else {
|
|
383
|
+
console.log("Can not remove file, the file not exist: ", filePath);
|
|
384
|
+
}
|
|
385
|
+
} catch (err) {
|
|
386
|
+
console.error("@blocklet/uploader: safeDeleteFile error: ", err);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async delete(key, isMetadata = true) {
|
|
390
|
+
try {
|
|
391
|
+
await this.queue.add(() => this.safeDeleteFile(this.resolve(key, isMetadata)));
|
|
392
|
+
} catch (err) {
|
|
393
|
+
console.error("@blocklet/uploader: delete error: ", err);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
async list() {
|
|
397
|
+
return this.queue.add(async () => {
|
|
398
|
+
const files = await fs.readdir(this.directory, {
|
|
399
|
+
withFileTypes: true
|
|
400
|
+
});
|
|
401
|
+
const promises = files.filter(file => file.isFile() && file.name.endsWith(".json")).map(file => {
|
|
402
|
+
return file.name.replace(".json", "");
|
|
403
|
+
});
|
|
404
|
+
return Promise.all(promises);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
resolve(key, isMetadata = true) {
|
|
408
|
+
let fileKey = key;
|
|
409
|
+
if (isMetadata) {
|
|
410
|
+
fileKey = `${key}.json`;
|
|
411
|
+
}
|
|
412
|
+
return path.join(this.directory, fileKey);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
class RewriteFileStore extends FileStore {
|
|
416
|
+
constructor(options) {
|
|
417
|
+
super(options);
|
|
418
|
+
}
|
|
419
|
+
async remove(key) {
|
|
420
|
+
this.configstore.delete(key);
|
|
421
|
+
this.configstore.delete(key, false);
|
|
422
|
+
}
|
|
423
|
+
}
|