dsh-mobile-plugin 0.1.76 → 0.1.78
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/README.md +10 -1
- package/lib/impl.js +12 -1
- package/lib/mobile/history-derive.js +77 -42
- package/lib/mobile/host-bridge/event-synthesis.js +55 -12
- package/lib/mobile/pairing.js +211 -0
- package/lib/mobile/sessions.js +1 -8
- package/lib/mobile/uploads.js +48 -6
- package/lib/panel/index.html +8 -6
- package/lib/relay/lan-network.cjs +44 -1
- package/lib/relay/lan-proxy.js +43 -11
- package/lib/relay/protocol.js +5 -4
- package/lib/routes/control-plane.js +38 -0
- package/lib/routes/mobile-api.js +28 -5
- package/package.json +1 -1
package/lib/mobile/uploads.js
CHANGED
|
@@ -44,9 +44,43 @@ const STAGING_TTL_MS = 24 * 60 * 60 * 1000;
|
|
|
44
44
|
/** 分片必须是标准 base64:Node 的 Buffer.from 会静默丢弃非法字符,光靠解码判不出坏片。 */
|
|
45
45
|
const BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
46
46
|
|
|
47
|
-
/** 进行中的上传:uploadId → { name, stagingPath, received, bytes }。 */
|
|
47
|
+
/** 进行中的上传:uploadId → { name, stagingPath, received, bytes, expiresAt, expiryTimer }。 */
|
|
48
48
|
const uploads = new Map();
|
|
49
49
|
|
|
50
|
+
function removeUpload(uploadId, job) {
|
|
51
|
+
if (uploads.get(uploadId) !== job) return false;
|
|
52
|
+
uploads.delete(uploadId);
|
|
53
|
+
if (job.expiryTimer) clearTimeout(job.expiryTimer);
|
|
54
|
+
job.expiryTimer = null;
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function expireUpload(uploadId, job) {
|
|
59
|
+
if (uploads.get(uploadId) !== job) return;
|
|
60
|
+
const remaining = job.expiresAt - Date.now();
|
|
61
|
+
if (remaining > 0) {
|
|
62
|
+
job.expiryTimer = setTimeout(() => { void expireUpload(uploadId, job); }, remaining);
|
|
63
|
+
job.expiryTimer.unref?.();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!removeUpload(uploadId, job)) return;
|
|
67
|
+
try { await fsp.unlink(job.stagingPath); } catch { /* 暂存文件可能已由磁盘清扫移除 */ }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function scheduleUploadExpiry(uploadId, job) {
|
|
71
|
+
if (job.expiryTimer) clearTimeout(job.expiryTimer);
|
|
72
|
+
job.expiryTimer = setTimeout(() => { void expireUpload(uploadId, job); }, Math.max(0, job.expiresAt - Date.now()));
|
|
73
|
+
job.expiryTimer.unref?.();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 清理过期上传状态及其暂存文件;传入 now 便于测试 TTL 边界。 */
|
|
77
|
+
async function sweepExpiredUploads(now = Date.now()) {
|
|
78
|
+
await Promise.all([...uploads].map(async ([uploadId, job]) => {
|
|
79
|
+
if (job.expiresAt > now || !removeUpload(uploadId, job)) return;
|
|
80
|
+
try { await fsp.unlink(job.stagingPath); } catch { /* 暂存文件可能已由磁盘清扫移除 */ }
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
|
|
50
84
|
/** 收件根目录。每次取用都重算,好让测试用环境变量分派。 */
|
|
51
85
|
function inboxRoot() {
|
|
52
86
|
const override = process.env.DSH_MOBILE_INBOX;
|
|
@@ -129,6 +163,7 @@ async function handleUploadBegin(_ctx, req, res) {
|
|
|
129
163
|
return fail(res, 500, 'inbox-unavailable', `收件目录不可用:${error.message}`);
|
|
130
164
|
}
|
|
131
165
|
void sweepStaging(stagingDir);
|
|
166
|
+
void sweepExpiredUploads();
|
|
132
167
|
|
|
133
168
|
const uploadId = crypto.randomUUID();
|
|
134
169
|
const stagingPath = path.join(stagingDir, `${uploadId}.part`);
|
|
@@ -137,12 +172,16 @@ async function handleUploadBegin(_ctx, req, res) {
|
|
|
137
172
|
} catch (error) {
|
|
138
173
|
return fail(res, 500, 'inbox-unavailable', `无法建立暂存文件:${error.message}`);
|
|
139
174
|
}
|
|
140
|
-
|
|
175
|
+
const job = {
|
|
141
176
|
name: sanitizeName(body.name),
|
|
142
177
|
stagingPath,
|
|
143
178
|
received: 0,
|
|
144
179
|
bytes: 0,
|
|
145
|
-
|
|
180
|
+
expiresAt: Date.now() + STAGING_TTL_MS,
|
|
181
|
+
expiryTimer: null,
|
|
182
|
+
};
|
|
183
|
+
uploads.set(uploadId, job);
|
|
184
|
+
scheduleUploadExpiry(uploadId, job);
|
|
146
185
|
sendJson(res, 200, { ok: true, uploadId, chunkSize: CHUNK_BYTES });
|
|
147
186
|
}
|
|
148
187
|
|
|
@@ -169,11 +208,13 @@ async function handleUploadChunk(_ctx, req, res) {
|
|
|
169
208
|
try {
|
|
170
209
|
await fsp.appendFile(job.stagingPath, chunk);
|
|
171
210
|
} catch (error) {
|
|
172
|
-
|
|
211
|
+
removeUpload(String(body.uploadId), job);
|
|
173
212
|
return fail(res, 500, 'inbox-unavailable', `写入暂存文件失败:${error.message}`);
|
|
174
213
|
}
|
|
175
214
|
job.received += 1;
|
|
176
215
|
job.bytes += chunk.length;
|
|
216
|
+
job.expiresAt = Date.now() + STAGING_TTL_MS;
|
|
217
|
+
scheduleUploadExpiry(String(body.uploadId), job);
|
|
177
218
|
sendJson(res, 200, { ok: true, received: job.received });
|
|
178
219
|
}
|
|
179
220
|
|
|
@@ -183,7 +224,7 @@ async function handleUploadFinish(_ctx, req, res) {
|
|
|
183
224
|
const uploadId = String((body && body.uploadId) || '');
|
|
184
225
|
const job = uploads.get(uploadId);
|
|
185
226
|
if (!job) return fail(res, 404, 'unknown-upload', '上传会话不存在或已结束');
|
|
186
|
-
|
|
227
|
+
removeUpload(uploadId, job);
|
|
187
228
|
|
|
188
229
|
const dir = path.join(inboxRoot(), today());
|
|
189
230
|
try {
|
|
@@ -203,7 +244,7 @@ async function handleUploadAbort(_ctx, req, res) {
|
|
|
203
244
|
const uploadId = String((body && body.uploadId) || '');
|
|
204
245
|
const job = uploads.get(uploadId);
|
|
205
246
|
if (job) {
|
|
206
|
-
|
|
247
|
+
removeUpload(uploadId, job);
|
|
207
248
|
try { await fsp.unlink(job.stagingPath); } catch { /* 已经不在了 */ }
|
|
208
249
|
}
|
|
209
250
|
sendJson(res, 200, { ok: true });
|
|
@@ -216,5 +257,6 @@ module.exports = {
|
|
|
216
257
|
handleUploadAbort,
|
|
217
258
|
sanitizeName,
|
|
218
259
|
inboxRoot,
|
|
260
|
+
sweepExpiredUploads,
|
|
219
261
|
CHUNK_BYTES,
|
|
220
262
|
};
|