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.
@@ -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
- uploads.set(uploadId, {
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
- uploads.delete(String(body.uploadId));
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
- uploads.delete(uploadId);
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
- uploads.delete(uploadId);
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
  };