chattransfer 2.0.12 → 2.0.13
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/frontend/dist/assets/{browser-Crql_X4e.js → browser-LN9vL0f8.js} +1 -1
- package/frontend/dist/assets/{index-B70USaaW.js → index-BZdEqZiK.js} +1 -1
- package/frontend/dist/index.html +1 -1
- package/package.json +1 -1
- package/server/index.js +41 -5
- package/server/routes/files.js +41 -38
- package/server/routes/index.js +10 -5
- package/server/routes/system.js +93 -81
- package/server/utils/cache-path-validation.js +125 -0
- package/server/routes/setCachePath.js +0 -94
package/frontend/dist/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<link rel="icon" href="/assets/logo-DQUxWwh1.png" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>ChatTransfer</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-BZdEqZiK.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-Bg5fD0DP.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -3,7 +3,7 @@ const net = require('net');
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { getIPAddress, openDefaultBrowser } = require('./tool');
|
|
6
|
-
const { getCachePath, getConfigFile } = require('./config');
|
|
6
|
+
const { getCachePath, getConfigFile, getDefaultCachePath } = require('./config');
|
|
7
7
|
const registerRoutes = require('./routes/index');
|
|
8
8
|
const registerSocket = require('./socket/index');
|
|
9
9
|
const { MessageHistoryService } = require('./services/message-history-service');
|
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
usesIndependentDatabaseFile
|
|
15
15
|
} = require('./utils/runtime-paths');
|
|
16
16
|
const { resolveStartOptions } = require('./utils/runtime-options');
|
|
17
|
+
const { getSafeStartupCachePath } = require('./utils/cache-path-validation');
|
|
17
18
|
|
|
18
19
|
const DEFAULT_PORT = 55556;
|
|
19
20
|
const MAX_RETRY = 3;
|
|
@@ -22,7 +23,27 @@ const pkg = require('../package.json');
|
|
|
22
23
|
|
|
23
24
|
function createApp() {
|
|
24
25
|
const app = require('express')();
|
|
25
|
-
|
|
26
|
+
return app;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createConfiguredApp(cachePath) {
|
|
30
|
+
const app = createApp();
|
|
31
|
+
const safeCachePath = cachePath
|
|
32
|
+
? { path: path.resolve(cachePath), changed: false }
|
|
33
|
+
: getSafeStartupCachePath({
|
|
34
|
+
appRoot: path.resolve(__dirname, '..'),
|
|
35
|
+
configPath: getConfigFile(),
|
|
36
|
+
configuredPath: getCachePath(),
|
|
37
|
+
dataRoot: path.dirname(getConfigFile()),
|
|
38
|
+
defaultPath: getDefaultCachePath()
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (safeCachePath.changed) {
|
|
42
|
+
console.warn(`[Cache] unsafe configured path rejected: ${getCachePath()}`);
|
|
43
|
+
console.warn(`[Cache] fallback to default: ${safeCachePath.path}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
registerRoutes(app, safeCachePath.path);
|
|
26
47
|
return app;
|
|
27
48
|
}
|
|
28
49
|
|
|
@@ -72,7 +93,22 @@ async function start(options = {}) {
|
|
|
72
93
|
return;
|
|
73
94
|
}
|
|
74
95
|
|
|
75
|
-
const
|
|
96
|
+
const configuredCachePath = getCachePath();
|
|
97
|
+
const defaultCachePath = getDefaultCachePath();
|
|
98
|
+
const safeCachePath = getSafeStartupCachePath({
|
|
99
|
+
appRoot: path.resolve(__dirname, '..'),
|
|
100
|
+
configPath: getConfigFile(),
|
|
101
|
+
configuredPath: configuredCachePath,
|
|
102
|
+
dataRoot: path.dirname(getConfigFile()),
|
|
103
|
+
defaultPath: defaultCachePath
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (safeCachePath.changed) {
|
|
107
|
+
console.warn(`[Cache] unsafe configured path rejected: ${configuredCachePath}`);
|
|
108
|
+
console.warn(`[Cache] fallback to default: ${safeCachePath.path}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const uploadsDir = safeCachePath.path;
|
|
76
112
|
await fs.promises.mkdir(uploadsDir, { recursive: true });
|
|
77
113
|
|
|
78
114
|
const historyService = new MessageHistoryService({
|
|
@@ -87,7 +123,7 @@ async function start(options = {}) {
|
|
|
87
123
|
return;
|
|
88
124
|
}
|
|
89
125
|
|
|
90
|
-
const app =
|
|
126
|
+
const app = createConfiguredApp(uploadsDir);
|
|
91
127
|
const server = http.createServer(app);
|
|
92
128
|
|
|
93
129
|
registerSocket(server, historyService);
|
|
@@ -127,6 +163,6 @@ if (require.main === module) {
|
|
|
127
163
|
}
|
|
128
164
|
|
|
129
165
|
module.exports = {
|
|
130
|
-
createApp,
|
|
166
|
+
createApp: createConfiguredApp,
|
|
131
167
|
start
|
|
132
168
|
};
|
package/server/routes/files.js
CHANGED
|
@@ -1,49 +1,52 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const router = express.Router();
|
|
4
3
|
const { getCachePath } = require('../config');
|
|
5
4
|
|
|
6
5
|
function sanitizeFileName(name) {
|
|
7
6
|
return path.basename(name || '').replace(/[\\/:*?"<>|\u0000]/g, '_');
|
|
8
7
|
}
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
9
|
+
module.exports = (configuredUploadsDir = getCachePath()) => {
|
|
10
|
+
const router = express.Router();
|
|
11
|
+
const uploadsDir = path.resolve(configuredUploadsDir);
|
|
12
|
+
|
|
13
|
+
router.post('/uploadFile', async (req, res) => {
|
|
14
|
+
const files = req.files && (req.files.file_data || req.files.fileObj);
|
|
15
|
+
|
|
16
|
+
if (!files) {
|
|
17
|
+
return res.status(400).send({
|
|
18
|
+
code: -1,
|
|
19
|
+
message: 'No file uploaded'
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const fileList = Array.isArray(files) ? files : [files];
|
|
24
|
+
const uploadResults = [];
|
|
25
|
+
|
|
26
|
+
for (const file of fileList) {
|
|
27
|
+
const timestamp = Date.now();
|
|
28
|
+
const safeName = `${timestamp}_${sanitizeFileName(file.name)}`;
|
|
29
|
+
const filePath = path.join(uploadsDir, 'files', safeName);
|
|
30
|
+
|
|
31
|
+
await new Promise((resolve, reject) => {
|
|
32
|
+
file.mv(filePath, (error) => (error ? reject(error) : resolve()));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
uploadResults.push({
|
|
36
|
+
name: safeName,
|
|
37
|
+
mimetype: file.mimetype,
|
|
38
|
+
size: file.size,
|
|
39
|
+
fileUrl: `${req.protocol}://${req.headers.host}/files/${encodeURIComponent(safeName)}`,
|
|
40
|
+
localFilePath: filePath
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
res.send({
|
|
45
|
+
code: 0,
|
|
46
|
+
message: fileList.length > 1 ? 'Files uploaded' : 'File uploaded',
|
|
47
|
+
data: fileList.length > 1 ? uploadResults : uploadResults[0]
|
|
39
48
|
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
res.send({
|
|
43
|
-
code: 0,
|
|
44
|
-
message: fileList.length > 1 ? 'Files uploaded' : 'File uploaded',
|
|
45
|
-
data: fileList.length > 1 ? uploadResults : uploadResults[0]
|
|
46
49
|
});
|
|
47
|
-
});
|
|
48
50
|
|
|
49
|
-
|
|
51
|
+
return router;
|
|
52
|
+
};
|
package/server/routes/index.js
CHANGED
|
@@ -5,10 +5,15 @@ const fs = require('fs');
|
|
|
5
5
|
const fileUpload = require('express-fileupload');
|
|
6
6
|
const { getCachePath } = require('../config');
|
|
7
7
|
|
|
8
|
-
module.exports = function registerRoutes(app) {
|
|
9
|
-
const uploadsDir =
|
|
8
|
+
module.exports = function registerRoutes(app, configuredUploadsDir = getCachePath()) {
|
|
9
|
+
const uploadsDir = path.resolve(configuredUploadsDir);
|
|
10
|
+
const filesDir = path.join(uploadsDir, 'files');
|
|
10
11
|
const tempDir = path.join(uploadsDir, 'tmp');
|
|
11
12
|
|
|
13
|
+
if (!fs.existsSync(filesDir)) {
|
|
14
|
+
fs.mkdirSync(filesDir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
|
|
12
17
|
if (!fs.existsSync(tempDir)) {
|
|
13
18
|
fs.mkdirSync(tempDir, { recursive: true });
|
|
14
19
|
}
|
|
@@ -22,13 +27,13 @@ module.exports = function registerRoutes(app) {
|
|
|
22
27
|
|
|
23
28
|
app.use(express.json());
|
|
24
29
|
|
|
25
|
-
app.use(require('./system'));
|
|
30
|
+
app.use(require('./system')(uploadsDir));
|
|
26
31
|
app.use(require('./session'));
|
|
27
32
|
app.use(require('./qr'));
|
|
28
|
-
app.use(require('./files'));
|
|
33
|
+
app.use(require('./files')(uploadsDir));
|
|
29
34
|
|
|
30
35
|
app.use(express.static(path.join(__dirname, '../../frontend/dist')));
|
|
31
|
-
app.use(express.static(
|
|
36
|
+
app.use('/files', express.static(filesDir));
|
|
32
37
|
|
|
33
38
|
app.get('*', (req, res) => {
|
|
34
39
|
res.sendFile(path.join(__dirname, '../../frontend/dist/index.html'));
|
package/server/routes/system.js
CHANGED
|
@@ -1,107 +1,119 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
3
4
|
const { execFile } = require('child_process');
|
|
4
|
-
const router = express.Router();
|
|
5
5
|
const {
|
|
6
6
|
getCachePath,
|
|
7
7
|
getConfigFile,
|
|
8
8
|
getDefaultCachePath,
|
|
9
|
-
isCustomized,
|
|
10
9
|
setCachePath
|
|
11
10
|
} = require('../config');
|
|
12
11
|
const { getDefaultDataDir } = require('../utils/runtime-paths');
|
|
12
|
+
const { validateCachePath } = require('../utils/cache-path-validation');
|
|
13
13
|
const { trackEvent } = require('../telemetry/compat');
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
res.send({
|
|
19
|
-
code: 0,
|
|
20
|
-
message: 'ok',
|
|
21
|
-
data: {
|
|
22
|
-
uploadsDir,
|
|
23
|
-
defaultDir: getDefaultCachePath(),
|
|
24
|
-
isCustomized: isCustomized(),
|
|
25
|
-
configFile: getConfigFile()
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
router.get('/getVersionInfo', (req, res) => {
|
|
31
|
-
res.send({
|
|
32
|
-
code: 0,
|
|
33
|
-
message: 'ok',
|
|
34
|
-
data: {
|
|
35
|
-
version: require('../../package.json').version,
|
|
36
|
-
deviceId: 'anonymous'
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
router.post('/openCachePath', express.json(), (req, res) => {
|
|
42
|
-
const uploadsDir = getCachePath();
|
|
43
|
-
const respond = (code, message) => res.send({
|
|
44
|
-
code,
|
|
45
|
-
message,
|
|
46
|
-
data: { platform: process.platform, path: uploadsDir }
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
if (process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
|
|
50
|
-
respond(0, '服务器无桌面环境,缓存目录已返回');
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
15
|
+
module.exports = (configuredUploadsDir = getCachePath()) => {
|
|
16
|
+
const router = express.Router();
|
|
17
|
+
const uploadsDir = path.resolve(configuredUploadsDir);
|
|
53
18
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
19
|
+
router.get('/getHomeDir', (req, res) => {
|
|
20
|
+
res.send({
|
|
21
|
+
code: 0,
|
|
22
|
+
message: 'ok',
|
|
23
|
+
data: {
|
|
24
|
+
uploadsDir,
|
|
25
|
+
defaultDir: getDefaultCachePath(),
|
|
26
|
+
isCustomized: uploadsDir !== path.resolve(getDefaultCachePath()),
|
|
27
|
+
configFile: getConfigFile()
|
|
28
|
+
}
|
|
57
29
|
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
router.get('/getVersionInfo', (req, res) => {
|
|
33
|
+
res.send({
|
|
34
|
+
code: 0,
|
|
35
|
+
message: 'ok',
|
|
36
|
+
data: {
|
|
37
|
+
version: require('../../package.json').version,
|
|
38
|
+
deviceId: 'anonymous'
|
|
39
|
+
}
|
|
61
40
|
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
router.post('/openCachePath', express.json(), (req, res) => {
|
|
44
|
+
const respond = (code, message) => res.send({
|
|
45
|
+
code,
|
|
46
|
+
message,
|
|
47
|
+
data: { platform: process.platform, path: uploadsDir }
|
|
65
48
|
});
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
49
|
|
|
69
|
-
|
|
70
|
-
|
|
50
|
+
if (process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
|
|
51
|
+
respond(0, '服务器无桌面环境,缓存目录已返回');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
71
54
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
55
|
+
if (process.platform === 'darwin') {
|
|
56
|
+
execFile('open', [uploadsDir], (error) => {
|
|
57
|
+
respond(error ? -1 : 0, error ? `打开失败:${error.message}` : '打开成功');
|
|
58
|
+
});
|
|
59
|
+
} else if (process.platform === 'win32') {
|
|
60
|
+
execFile('explorer', [uploadsDir], (error) => {
|
|
61
|
+
respond(error ? -1 : 0, error ? `打开失败:${error.message}` : '打开成功');
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
execFile('xdg-open', [uploadsDir], (error) => {
|
|
65
|
+
respond(error ? -1 : 0, error ? `打开失败:${error.message}` : '打开成功');
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
});
|
|
75
69
|
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
router.post('/setCachePath', (req, res) => {
|
|
71
|
+
const inputPath = req.body && req.body.path;
|
|
78
72
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
73
|
+
if (!inputPath || typeof inputPath !== 'string') {
|
|
74
|
+
return res.send({ code: -1, message: 'invalid path' });
|
|
75
|
+
}
|
|
82
76
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
message: 'ok',
|
|
89
|
-
data: { newPath, needsRestart: true }
|
|
77
|
+
const validation = validateCachePath(inputPath, {
|
|
78
|
+
appRoot: path.resolve(__dirname, '..', '..'),
|
|
79
|
+
configPath: getConfigFile(),
|
|
80
|
+
currentPath: uploadsDir,
|
|
81
|
+
dataRoot: getDefaultDataDir()
|
|
90
82
|
});
|
|
91
|
-
} catch (error) {
|
|
92
|
-
res.send({ code: -1, message: error.message });
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
83
|
|
|
96
|
-
|
|
97
|
-
|
|
84
|
+
if (!validation.valid) {
|
|
85
|
+
return res.send({ code: -1, message: 'invalid cache path' });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const newPath = validation.path;
|
|
90
|
+
fs.mkdirSync(newPath, { recursive: true });
|
|
91
|
+
|
|
92
|
+
const testFile = path.join(newPath, '.chattransfer-write-test');
|
|
93
|
+
fs.writeFileSync(testFile, 'ok', 'utf8');
|
|
94
|
+
fs.unlinkSync(testFile);
|
|
95
|
+
|
|
96
|
+
setCachePath(newPath);
|
|
97
|
+
res.send({
|
|
98
|
+
code: 0,
|
|
99
|
+
message: 'ok',
|
|
100
|
+
data: { newPath, needsRestart: true }
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
res.send({ code: -1, message: error.message });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
router.post('/api/track', (req, res) => {
|
|
108
|
+
const { eventType, eventProperties, userId } = req.body || {};
|
|
98
109
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
110
|
+
if (!eventType) {
|
|
111
|
+
return res.status(400).send({ code: -1, message: 'eventType is required' });
|
|
112
|
+
}
|
|
102
113
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
});
|
|
114
|
+
trackEvent(eventType, eventProperties || {}, userId || null);
|
|
115
|
+
res.send({ code: 0, message: 'ok' });
|
|
116
|
+
});
|
|
106
117
|
|
|
107
|
-
|
|
118
|
+
return router;
|
|
119
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
function isWithin(parent, child) {
|
|
6
|
+
const relative = path.relative(path.resolve(parent), path.resolve(child));
|
|
7
|
+
return relative === '' || (
|
|
8
|
+
relative !== '..' &&
|
|
9
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
10
|
+
!path.isAbsolute(relative)
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getSystemRoots() {
|
|
15
|
+
if (process.platform === 'win32') {
|
|
16
|
+
return [
|
|
17
|
+
process.env.SystemRoot,
|
|
18
|
+
process.env.ProgramFiles,
|
|
19
|
+
process.env['ProgramFiles(x86)'],
|
|
20
|
+
process.env.ProgramData
|
|
21
|
+
].filter(Boolean);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return [
|
|
25
|
+
'/bin',
|
|
26
|
+
'/boot',
|
|
27
|
+
'/dev',
|
|
28
|
+
'/etc',
|
|
29
|
+
'/lib',
|
|
30
|
+
'/lib64',
|
|
31
|
+
'/proc',
|
|
32
|
+
'/run',
|
|
33
|
+
'/sbin',
|
|
34
|
+
'/sys',
|
|
35
|
+
'/usr',
|
|
36
|
+
'/var'
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolvePathForValidation(filePath) {
|
|
41
|
+
const resolved = path.resolve(filePath);
|
|
42
|
+
let nearest = resolved;
|
|
43
|
+
|
|
44
|
+
while (!fs.existsSync(nearest)) {
|
|
45
|
+
const parent = path.dirname(nearest);
|
|
46
|
+
if (parent === nearest) break;
|
|
47
|
+
nearest = parent;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const nearestRealPath = fs.realpathSync(nearest);
|
|
51
|
+
if (nearest === resolved) return nearestRealPath;
|
|
52
|
+
|
|
53
|
+
return path.resolve(nearestRealPath, path.relative(nearest, resolved));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function validateCachePath(inputPath, {
|
|
57
|
+
appRoot,
|
|
58
|
+
configPath,
|
|
59
|
+
currentPath,
|
|
60
|
+
dataRoot,
|
|
61
|
+
homeDir = os.homedir()
|
|
62
|
+
} = {}) {
|
|
63
|
+
if (typeof inputPath !== 'string' || !inputPath.trim()) {
|
|
64
|
+
return { valid: false, reason: 'empty' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!path.isAbsolute(inputPath.trim())) {
|
|
68
|
+
return { valid: false, reason: 'relative' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const newPath = path.resolve(inputPath.trim());
|
|
72
|
+
const realPath = resolvePathForValidation(newPath);
|
|
73
|
+
const filesystemRoot = path.parse(realPath).root;
|
|
74
|
+
const systemRoots = getSystemRoots().map((item) => resolvePathForValidation(item));
|
|
75
|
+
const protectedPaths = [
|
|
76
|
+
filesystemRoot,
|
|
77
|
+
appRoot,
|
|
78
|
+
configPath && path.dirname(configPath),
|
|
79
|
+
currentPath,
|
|
80
|
+
dataRoot,
|
|
81
|
+
homeDir,
|
|
82
|
+
process.cwd(),
|
|
83
|
+
...systemRoots
|
|
84
|
+
].filter(Boolean).map((item) => path.resolve(item));
|
|
85
|
+
|
|
86
|
+
if (protectedPaths.some((protectedPath) => isWithin(realPath, protectedPath))) {
|
|
87
|
+
return { valid: false, reason: 'protected' };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { valid: true, path: newPath, realPath };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getSafeStartupCachePath({
|
|
94
|
+
appRoot,
|
|
95
|
+
configPath,
|
|
96
|
+
configuredPath,
|
|
97
|
+
dataRoot,
|
|
98
|
+
defaultPath
|
|
99
|
+
}) {
|
|
100
|
+
if (configuredPath === defaultPath) {
|
|
101
|
+
return { path: defaultPath, changed: false };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const validation = validateCachePath(configuredPath, {
|
|
105
|
+
appRoot,
|
|
106
|
+
configPath,
|
|
107
|
+
dataRoot
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (validation.valid) {
|
|
111
|
+
return { path: validation.path, changed: false };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
path: defaultPath,
|
|
116
|
+
changed: true,
|
|
117
|
+
reason: validation.reason
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
getSafeStartupCachePath,
|
|
123
|
+
isWithin,
|
|
124
|
+
validateCachePath
|
|
125
|
+
};
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 设置自定义缓存目录
|
|
3
|
-
* 请求体: { path: string, migrate?: boolean }
|
|
4
|
-
* 生效方式: 修改后需重启进程(express.static / fileUpload 中间件运行时不可热切换)
|
|
5
|
-
* @param {*} app
|
|
6
|
-
* @param {*} uploadsDir 当前生效缓存目录
|
|
7
|
-
*/
|
|
8
|
-
const fs = require('fs');
|
|
9
|
-
const path = require('path');
|
|
10
|
-
const express = require('express');
|
|
11
|
-
const { setCachePath } = require('../config.js');
|
|
12
|
-
const { getAppWritableRoot } = require('../tool.js');
|
|
13
|
-
|
|
14
|
-
module.exports = (app, uploadsDir) => {
|
|
15
|
-
app.post('/setCachePath', express.json(), async (req, res) => {
|
|
16
|
-
try {
|
|
17
|
-
const inputPath = req.body && req.body.path;
|
|
18
|
-
const migrate = req.body ? req.body.migrate !== false : true;
|
|
19
|
-
|
|
20
|
-
// 1. 参数校验
|
|
21
|
-
if (!inputPath || typeof inputPath !== 'string' || !inputPath.trim()) {
|
|
22
|
-
return res.send({ code: -1, message: '请输入缓存目录路径' });
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// 2. 必须绝对路径(先校验原始输入,再 resolve 规范化)
|
|
26
|
-
const trimmed = inputPath.trim();
|
|
27
|
-
if (!path.isAbsolute(trimmed)) {
|
|
28
|
-
return res.send({ code: -1, message: '请输入绝对路径' });
|
|
29
|
-
}
|
|
30
|
-
const newPath = path.resolve(trimmed);
|
|
31
|
-
|
|
32
|
-
// 3. 不能等于当前路径
|
|
33
|
-
if (newPath === path.resolve(uploadsDir)) {
|
|
34
|
-
return res.send({ code: -1, message: '新路径与当前路径相同' });
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// 4. 不能等于应用根目录(避免污染应用文件)
|
|
38
|
-
const appRoot = getAppWritableRoot();
|
|
39
|
-
if (newPath === path.resolve(appRoot)) {
|
|
40
|
-
return res.send({ code: -1, message: '不能使用应用根目录作为缓存目录' });
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// 5. 目录不存在则创建
|
|
44
|
-
try {
|
|
45
|
-
if (!fs.existsSync(newPath)) {
|
|
46
|
-
fs.mkdirSync(newPath, { recursive: true });
|
|
47
|
-
}
|
|
48
|
-
} catch (e) {
|
|
49
|
-
return res.send({ code: -1, message: '目录创建失败:' + e.message });
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// 6. 可写性测试:写临时文件再删
|
|
53
|
-
const testFile = path.join(newPath, '.chattransfer-write-test');
|
|
54
|
-
try {
|
|
55
|
-
fs.writeFileSync(testFile, 'ok', 'utf8');
|
|
56
|
-
fs.unlinkSync(testFile);
|
|
57
|
-
} catch (e) {
|
|
58
|
-
return res.send({ code: -1, message: '路径无效或不可写' });
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// 7. 迁移已有文件(递归复制旧目录内容到新目录)
|
|
62
|
-
if (migrate && fs.existsSync(uploadsDir)) {
|
|
63
|
-
try {
|
|
64
|
-
fs.cpSync(uploadsDir, newPath, { recursive: true });
|
|
65
|
-
// 清理测试遗留(若有)
|
|
66
|
-
if (fs.existsSync(testFile)) fs.unlinkSync(testFile);
|
|
67
|
-
} catch (e) {
|
|
68
|
-
// 迁移失败:清理已复制的内容,回滚,不更新配置
|
|
69
|
-
console.error('[setCachePath] migrate failed:', e.message);
|
|
70
|
-
return res.send({ code: -1, message: '迁移文件失败:' + e.message });
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// 8. 持久化到 config.json
|
|
75
|
-
try {
|
|
76
|
-
setCachePath(newPath);
|
|
77
|
-
} catch (e) {
|
|
78
|
-
return res.send({ code: -1, message: '配置保存失败:' + e.message });
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
res.send({
|
|
82
|
-
code: 0,
|
|
83
|
-
message: '缓存目录设置成功,重启应用后生效',
|
|
84
|
-
data: {
|
|
85
|
-
newPath: newPath,
|
|
86
|
-
needsRestart: true
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
} catch (err) {
|
|
90
|
-
console.log(err);
|
|
91
|
-
res.status(500).send({ code: -1, message: err.message || '服务器内部错误' });
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
}
|