magpie-page-token-widget 0.1.1 → 0.2.0
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 +137 -14
- package/lib/atomic.mjs +66 -0
- package/lib/collector.mjs +431 -123
- package/lib/forward.mjs +40 -19
- package/lib/normalize.mjs +66 -12
- package/lib/server.mjs +343 -58
- package/lib/state.mjs +197 -10
- package/lib/tailnet.mjs +140 -11
- package/package.json +1 -1
- package/scriptable/SetupUsageWidget.js +58 -58
- package/scriptable/UsageWidget.js +11 -2
- package/ui/app.js +292 -28
- package/ui/index.html +42 -1
- package/ui/style.css +81 -0
package/README.md
CHANGED
|
@@ -6,14 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
> 数据来自你自己的 Magpie,脚本里不含任何密钥。组件显示的仍是 Magpie 的口径。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
插件页长这样:上面是扫码配对,下面依次是状态、数据更新、网络连接、设备管理、显示哪些订阅、高级设置。
|
|
10
10
|
|
|
11
11
|
## 它做什么
|
|
12
12
|
|
|
13
13
|
- 插件页给出一个二维码。用 iPhone 的相机扫一下,Safari 打开配对页,下载组件脚本,用 Scriptable 打开它,长按桌面加组件就行了。
|
|
14
14
|
- 组件里显示每家订阅的剩余额度、5 小时与周额度窗口的百分比、重置倒计时、今日 Token、七日趋势和各家排行。参数可以选订阅(`codex`、`minimax`、`glm`,或 `codex@账号名`),也可以把百分比换成已用(`|used`)。
|
|
15
|
-
-
|
|
16
|
-
- 勾一个厂商=这个厂商的所有账号都显示;只勾一个账号=只有这个账号。勾选框显示的就是手机上会出现的卡片。
|
|
15
|
+
- 插件页是一个配置中心,分五块:**订阅展示**(勾选手机上要出现的卡片)、**数据更新**(多久读一次)、**网络连接**(尾网状态、连接路径、本机网卡)、**设备管理**(已配对的手机,可单独撤销)、**高级设置**(端口、网关等少改的参数)。
|
|
17
16
|
- 点一下手机上的组件,Safari 会打开手机版用量页:每家订阅的剩余额度、今日与近七日的 Token、模型/厂商/客户端排行,和电脑上看到的是同一份数据。
|
|
18
17
|
- 服务只读 Magpie 的本地接口,只在本机打开,手机经 Tailscale 的端口转发进来,令牌存放在 Magpie 自己的目录里。
|
|
19
18
|
|
|
@@ -45,9 +44,56 @@ magpie plugin add ./magpie-page-token-widget
|
|
|
45
44
|
4. 在「文件」App 里打开刚下载的 `UsageWidget.js`,选「共享 → 用 Scriptable 打开」。
|
|
46
45
|
5. 长按 iPhone 桌面空白处,添加 Scriptable 小组件,选择 `UsageWidget`。
|
|
47
46
|
|
|
48
|
-
|
|
47
|
+
配对码只能用一次,而且只活十分钟:十分钟之后它自己就废了,重新生成一个就行;用掉之后旧码立刻作废。每配一台手机,它都会拿到属于自己的那枚只读令牌,服务端只留这枚令牌的哈希,明文只在配对的那一次出现,之后再也拿不出来。
|
|
49
48
|
|
|
50
|
-
|
|
49
|
+
猜配对码是没有便宜的:同一个配对码连续猜错十次就锁五分钟,锁的是这个码本身,不是那台机器。原因是转发过来的手机在服务器看来都是本机的回环地址,按地址锁的话,尾网上任何一个人猜错十次就能把所有人的配对都关上。请求头里的 `X-Forwarded-For` 谁都能写,服务器不看它。
|
|
50
|
+
|
|
51
|
+
服务还有一本总账:五分钟里猜错一百次(不管猜的是哪个码),配对页、组件脚本和配对接口一起先关上,等五分钟满了自动恢复。电脑上点一次「重新生成配对码」会把总账清空,手动马上就能再配。真正让配对码难猜的不是这两道门,而是它有九十六位随机数、只活十分钟、只能用一次。
|
|
52
|
+
|
|
53
|
+
手机脚本把令牌存在 Scriptable 的钥匙串里,同一台 iPhone 上的三个组件共用这一枚令牌。
|
|
54
|
+
|
|
55
|
+
不想扫码也行:插件页的「设备管理」里可以给某台手机单独发一枚令牌,把它和「服务地址」填进 `scriptable/SetupUsageWidget.js`(在 Scriptable 里新建一个脚本粘贴运行)也行。
|
|
56
|
+
|
|
57
|
+
### 旧手机怎么办
|
|
58
|
+
|
|
59
|
+
升级到 0.2 之后,0.1 时代配好的手机**不会失效**:老版本那枚共享令牌会被迁成一个名叫「旧版共享令牌」的设备授权,手机照常用它读数。只有在「设备管理」里把这一条撤销,0.1 时代的手机才会一起失效。新配的手机拿到的都是自己的令牌,和别人无关,撤销哪一台就只停哪一台。
|
|
60
|
+
|
|
61
|
+
## 设备管理
|
|
62
|
+
|
|
63
|
+
「设备管理」里列出所有配过的设备:名字、什么时候配的、最后一次是什么时候、什么时候撤销的。每一条都能改名字,都能单独撤销;撤销一台手机,别的手机照常读数。想加一台新手机,点「给这台手机一个令牌」,把生成的那枚明文令牌填进手机即可(只显示这一次)。
|
|
64
|
+
|
|
65
|
+
## 数据更新
|
|
66
|
+
|
|
67
|
+
服务不会每个手机请求都去读一遍 Magpie。它拿着一份快照,快照一分钟内是新鲜的,直接给;过期了才去读,而且所有同时来的请求共用同一次读取。
|
|
68
|
+
|
|
69
|
+
读的时候最多等两三秒:读回来了就立刻给新的;还没回来就先给旧的,标成「旧数据」,后台继续读。
|
|
70
|
+
|
|
71
|
+
平时多久读一次可以选:
|
|
72
|
+
|
|
73
|
+
| 策略 | 说明 |
|
|
74
|
+
|---|---|
|
|
75
|
+
| 智能更新(默认) | 有人看的时候每分钟一次;十分钟没人看改成五分钟一次;半小时没人看就基本不自己读了,等有人来再说 |
|
|
76
|
+
| 每分钟 | 固定每分钟读一次 |
|
|
77
|
+
| 每两分钟 | 固定每两分钟读一次 |
|
|
78
|
+
| 每五分钟 | 固定每五分钟读一次 |
|
|
79
|
+
|
|
80
|
+
在插件页改一下就立刻生效,不用重启 Magpie。
|
|
81
|
+
|
|
82
|
+
## 网络连接
|
|
83
|
+
|
|
84
|
+
「网络连接」里能看到:Tailscale 装没装、连上没有、本机的尾网地址、MagicDNS 名字、同一尾网里的 iPhone,以及现在这通连接走的是哪条路。
|
|
85
|
+
|
|
86
|
+
连接分三种,看的时候别混:
|
|
87
|
+
|
|
88
|
+
- **直连(Direct)**:Tailscale 在两台机器之间直接加密通信,通常是同一个局域网里自动找到对方地址之后走这条路,最快。
|
|
89
|
+
- **经由尾网中继设备(Peer Relay)**:两台机器之间没有直连路径,借了尾网里第三台设备转发。
|
|
90
|
+
- **经由 Tailscale 中继服务器(DERP)**:连不上也没有中继设备时,流量走 Tailscale 自己的中继服务器兜底,最慢但最稳。
|
|
91
|
+
|
|
92
|
+
读不出来的时候显示「还不知道」,不猜。「诊断连接」按钮会真的 ping 一次那台手机,把结果写出来;平时不会自己 ping,省得一直起 Tailscale 子进程。
|
|
93
|
+
|
|
94
|
+
本机的网卡也在这一块里列着:网卡名、IPv4、IPv6、子网前缀、是 Tailscale 网卡还是 Wi-Fi/有线/虚拟网卡。**这些地址只给电脑上的你看,手机接口拿不到。** 列出它们是为了排障:手机连不上的时候,先看看这台电脑在局域网里的地址,再看尾网地址是不是变了。
|
|
95
|
+
|
|
96
|
+
**默认仍然走 Tailscale**,不会因为这台电脑有个 192.168.x.x 就让手机改走局域网直连。原因是:局域网直连要处理明文 HTTP(或另配 HTTPS)、DHCP 换地址、iOS 的本地网络权限等等一串事情,做不好就是新的安全口子;而且尾网地址在手机和电脑上都是稳的。局域网直连这次没有实现,只做了地址发现。
|
|
51
97
|
|
|
52
98
|
## 手机上的用量页
|
|
53
99
|
|
|
@@ -69,7 +115,7 @@ magpie plugin add ./magpie-page-token-widget
|
|
|
69
115
|
|
|
70
116
|
## 端口
|
|
71
117
|
|
|
72
|
-
服务默认听 8790。端口上冲突时换一个:
|
|
118
|
+
服务默认听 8790,手机那一侧是 8791。端口上冲突时换一个:
|
|
73
119
|
|
|
74
120
|
```bash
|
|
75
121
|
magpie plugin options magpie-page-token-widget '{"port":8890}'
|
|
@@ -79,7 +125,17 @@ magpie plugin options magpie-page-token-widget '{"port":8890}'
|
|
|
79
125
|
|
|
80
126
|
转发指向的是手机那一侧的端口(默认比服务端口大 1,即 8791),控制台留在 127.0.0.1:8790。尾网里的请求落在这条转发上,不会被当成本机的请求,所以手机看不到控制台,也拿不到令牌。
|
|
81
127
|
|
|
82
|
-
|
|
128
|
+
插件会把自己建的转发记在配置文件里(端口和指向哪里)。重启之后它认得自己的转发,不会重复建;老版本留下的转发会被搬到手机那一侧;别人的转发它既不碰也不删。只有插件正常卸载、并且那条转发仍然指向它记下的地方时,它才会把转发撤掉。
|
|
129
|
+
|
|
130
|
+
`tailscale serve --bg --tcp=8790` 是 Tailscale 里的长期设置:它留在那儿,插件被移除或崩溃都不会自己消失。想知道现在有哪些转发,看 `tailscale serve status`。
|
|
131
|
+
|
|
132
|
+
## 兼容性
|
|
133
|
+
|
|
134
|
+
- 需要 Magpie `0.1.3681` 以上(`package.json` 里写着)。
|
|
135
|
+
- 额度读的是 Magpie 已经缓存好的那份(`/v1/magpie/quotas?kept=1`),不额外去问供应商。
|
|
136
|
+
- 今天的 Token 读 `/v1/magpie/usage?period=today`,一次请求就拿到总数、分时、各厂商和各账号。**老版本 Magpie 没有这个接口时会自动退回老办法**(`/v1/magpie/usage/requests?period=today` 加逐账号的请求),组件上的数字照常有。
|
|
137
|
+
- 七日趋势和模型/厂商排行读 `/v1/magpie/usage/requests?period=7d&limit=1`:这个接口才带 `by.model`、`by.agent` 这些分组,换成别的接口会把排行和趋势丢掉,所以保留。
|
|
138
|
+
- 周期参数 `week` 在 Magpie 里不认(会被当成 30 天),七日要用 `7d`。
|
|
83
139
|
|
|
84
140
|
## 卸载
|
|
85
141
|
|
|
@@ -92,15 +148,18 @@ magpie plugin rm magpie-page-token-widget
|
|
|
92
148
|
## 隐私
|
|
93
149
|
|
|
94
150
|
- 服务只读 Magpie 的本地接口(`127.0.0.1:3425`),不写 Magpie 的配置,不碰登录信息。
|
|
95
|
-
- 令牌是本机随机生成的,只读,存在 Magpie
|
|
151
|
+
- 令牌是本机随机生成的,只读,存在 Magpie 的配置目录里;服务端只留哈希。只有本机的插件页和拿着配对码的手机能拿到。
|
|
152
|
+
- 局域网地址只出现在电脑上的控制台里,手机接口不提供。
|
|
96
153
|
- 服务只在本机(127.0.0.1)上监听,手机是经 Tailscale 的转发进来的,没有公网入口,也不需要防火墙规则。
|
|
97
154
|
|
|
98
155
|
## 开发
|
|
99
156
|
|
|
100
157
|
```bash
|
|
101
|
-
|
|
158
|
+
npm test
|
|
102
159
|
```
|
|
103
160
|
|
|
161
|
+
测试里不会碰真的 Tailscale:所有和 Tailscale 命令打交道的测试都用一个自己的假命令(`test/helpers.mjs` 里的 `mockTailscale`),也不会去改真的 `tailscale serve` 设置。Magpie 网关同样是测试自己起的一个。
|
|
162
|
+
|
|
104
163
|
`lib/collector.mjs` 和 `lib/normalize.mjs` 来自 [ScriptableTokenWidgets](https://github.com/liuzq1103/ScriptableTokenWidgets),`lib/vendor/qrcode-generator.cjs` 是 [qrcode-generator](https://github.com/kazuhikoarase/qrcode-generator)。详见 [NOTICE](NOTICE)。
|
|
105
164
|
|
|
106
165
|
---
|
|
@@ -133,7 +192,54 @@ From a checkout: `magpie plugin add ./magpie-page-token-widget`.
|
|
|
133
192
|
4. Open the downloaded `UsageWidget.js` in the Files app and choose "Share → Open with Scriptable".
|
|
134
193
|
5. Long press the home screen, add the Scriptable widget and pick `UsageWidget`.
|
|
135
194
|
|
|
136
|
-
The pairing code works once
|
|
195
|
+
The pairing code works once and lives ten minutes at most: after that it is dead, and a new one replaces it at once. Every phone that pairs gets a read-only token of its own. The service keeps only the hash of that token; the plain text is handed over exactly once, at the moment of pairing, and never again.
|
|
196
|
+
|
|
197
|
+
Guessing a code is not cheap: ten wrong tries at one code lock that code for five minutes. The lock follows the code, not the machine — behind the Tailscale forward every phone arrives as the loopback of this computer, so a lock per address would let anybody on the tailnet shut the pairing of everybody else. An `X-Forwarded-For` header can be written by anybody, so it is not believed.
|
|
198
|
+
|
|
199
|
+
The service keeps one budget for the whole of it: a hundred wrong tries in five minutes, whichever code they were aimed at, and the pairing page, the widget script and the pairing door all close until the five minutes are up. Asking for a new pairing code at the computer empties that budget at once, so a person can open the door again without waiting. What really makes a code hard to guess is neither of those two doors: it has ninety-six bits of randomness, it lives ten minutes and it opens the door once.
|
|
200
|
+
|
|
201
|
+
The script keeps the token in the keychain of Scriptable, and the three widgets of one iPhone share it.
|
|
202
|
+
|
|
203
|
+
Without the code you can also use "Device management": give a phone a token of its own and put it, with the service address, into `scriptable/SetupUsageWidget.js`.
|
|
204
|
+
|
|
205
|
+
### The phones of the older version
|
|
206
|
+
|
|
207
|
+
Updating to 0.2 does not lock out the phones that were paired with 0.1: the shared token of the old version is migrated into a device named "旧版共享令牌" and those phones keep reading with it. Only revoking that entry stops them. Every new phone gets a token of its own, and revoking one phone stops that phone alone.
|
|
208
|
+
|
|
209
|
+
## Device management
|
|
210
|
+
|
|
211
|
+
The list holds every device that was paired: its name, when it was paired, when it was last seen and when it was revoked. Every entry can be renamed and revoked on its own. To add a phone, press "give this phone a token" and put the token that appears once into the phone.
|
|
212
|
+
|
|
213
|
+
## Data update
|
|
214
|
+
|
|
215
|
+
The service does not read Magpie for every request of the phone. It keeps a snapshot that is good for a minute: a fresh snapshot is given right away, an old one triggers a reading, and all the requests that come at that moment share that one reading.
|
|
216
|
+
|
|
217
|
+
A reader waits at most a few seconds: it gets the new numbers when they arrive, and the old ones marked as old when they do not, while the reading continues in the background.
|
|
218
|
+
|
|
219
|
+
How often the service reads on its own:
|
|
220
|
+
|
|
221
|
+
| Strategy | What it does |
|
|
222
|
+
|---|---|
|
|
223
|
+
| Smart (the default) | every minute while somebody is looking, every five minutes after ten quiet minutes, and almost never after half an hour of quiet |
|
|
224
|
+
| Every minute | a fixed rhythm |
|
|
225
|
+
| Every two minutes | a fixed rhythm |
|
|
226
|
+
| Every five minutes | a fixed rhythm |
|
|
227
|
+
|
|
228
|
+
Changing it in the plugin page takes effect at once; Magpie does not have to be restarted.
|
|
229
|
+
|
|
230
|
+
## Network
|
|
231
|
+
|
|
232
|
+
The page shows whether Tailscale is installed and connected, the address of this computer in the tailnet, the MagicDNS name, the iPhones of the tailnet and the way the connection is made. Three ways are told apart:
|
|
233
|
+
|
|
234
|
+
- **Direct**: Tailscale talks to the phone itself, encrypted end to end; on one LAN it usually finds the other side on its own, and that is the fastest way.
|
|
235
|
+
- **Peer relay**: there is no direct path, so another device of the tailnet carries the traffic.
|
|
236
|
+
- **DERP**: Tailscale's own relay server carries the traffic; the slowest way, but it always works.
|
|
237
|
+
|
|
238
|
+
When the way cannot be read, the page says so instead of guessing. The button "diagnose" pings the phone once and writes down the answer; the service never pings on its own.
|
|
239
|
+
|
|
240
|
+
The network cards of this computer are listed as well: name, IPv4, IPv6, the length of the network part, and whether the card is Tailscale, Wi-Fi, wired or virtual. **These addresses are for the computer only; the phone API never gets them.** They are there to help: when the phone cannot be reached, look at the address of this computer in the LAN and at the address of the tailnet.
|
|
241
|
+
|
|
242
|
+
**Tailscale stays the default way in.** The phone does not go around the tailnet just because this computer has a 192.168.x.x: a direct way over the LAN would have to deal with plain HTTP (or with HTTPS of its own), with addresses that move when the router hands out new ones, and with the local network permission of iOS. That is a new hole to look after, and the tailnet address is stable on both sides. The direct way over the LAN is not implemented in this release; only the addresses are discovered.
|
|
137
243
|
|
|
138
244
|
## The page on the phone
|
|
139
245
|
|
|
@@ -155,7 +261,7 @@ The service runs as long as Magpie runs. With Magpie closed, the phone keeps the
|
|
|
155
261
|
|
|
156
262
|
## Port
|
|
157
263
|
|
|
158
|
-
The service listens on 8790 by default. Pick another one when the port is taken:
|
|
264
|
+
The service listens on 8790 by default, the phone side on 8791. Pick another one when the port is taken:
|
|
159
265
|
|
|
160
266
|
```bash
|
|
161
267
|
magpie plugin options magpie-page-token-widget '{"port":8890}'
|
|
@@ -165,7 +271,17 @@ Restart Magpie afterwards. The plugin publishes the port in the tailnet with a f
|
|
|
165
271
|
|
|
166
272
|
The forwarding points at the phone side of the service (by default one port above the service port, so 8791), while the console stays on 127.0.0.1:8790. A request that comes in through the tailnet lands on that phone port and is never taken for a request of this computer, so the phone cannot read the console or the token.
|
|
167
273
|
|
|
168
|
-
|
|
274
|
+
The plugin writes down the forwarding it made (the port and where it points) in its own file. After a restart it recognizes it and does not make it twice; a forwarding of an older version is moved to the phone side; the forwarding of somebody else is neither touched nor removed. It is taken away only when the plugin is uninstalled properly and the forwarding still points where it was written down.
|
|
275
|
+
|
|
276
|
+
`tailscale serve --bg --tcp=8790` is a standing setting of Tailscale: it stays there when the plugin is removed or crashes. `tailscale serve status` shows what is published right now.
|
|
277
|
+
|
|
278
|
+
## Compatibility
|
|
279
|
+
|
|
280
|
+
- Magpie `0.1.3681` or later is needed (`package.json` says so).
|
|
281
|
+
- The quota is read from the report Magpie has already cached (`/v1/magpie/quotas?kept=1`); the vendors are not asked again.
|
|
282
|
+
- Today's tokens come from one request to `/v1/magpie/usage?period=today`, which holds the totals, the hours, the vendors and the accounts. **When an older Magpie does not know that endpoint, the old way is used instead** (`/v1/magpie/usage/requests?period=today` plus one request per account) and the widget keeps its numbers.
|
|
283
|
+
- The trend of the last seven days and the rankings by model and vendor come from `/v1/magpie/usage/requests?period=7d&limit=1`: this is the only endpoint that carries the groups `by.model` and `by.agent`, and another endpoint would lose the rankings and the trend, so it stays.
|
|
284
|
+
- Magpie does not know the period `week` (it is taken for 30 days); seven days is `7d`.
|
|
169
285
|
|
|
170
286
|
## Uninstall
|
|
171
287
|
|
|
@@ -177,10 +293,17 @@ The forwarding of the plugin is taken away with it.
|
|
|
177
293
|
|
|
178
294
|
## Privacy
|
|
179
295
|
|
|
180
|
-
The service only reads the local API of Magpie (`127.0.0.1:3425`).
|
|
296
|
+
- The service only reads the local API of Magpie (`127.0.0.1:3425`). It does not write the configuration of Magpie and does not touch the logins.
|
|
297
|
+
- The tokens are random, read only and stay in the directory of Magpie; the service keeps only their hashes. Only the plugin page of this computer and the phone that carries a pairing code can get one.
|
|
298
|
+
- The addresses of the LAN are only shown in the console of the computer; the phone API does not have them.
|
|
299
|
+
- The service listens on this computer only (127.0.0.1); the phone comes in through the forwarding of Tailscale. There is no public entrance and no firewall rule is needed.
|
|
181
300
|
|
|
182
301
|
## Development
|
|
183
302
|
|
|
184
303
|
```bash
|
|
185
|
-
|
|
304
|
+
npm test
|
|
186
305
|
```
|
|
306
|
+
|
|
307
|
+
The tests never touch the real Tailscale: everything that speaks with the command of Tailscale talks to a fake one instead (`mockTailscale` in `test/helpers.mjs`), and the real `tailscale serve` settings are never changed. The gateway of Magpie is one the tests start themselves.
|
|
308
|
+
|
|
309
|
+
`lib/collector.mjs` and `lib/normalize.mjs` come from [ScriptableTokenWidgets](https://github.com/liuzq1103/ScriptableTokenWidgets), `lib/vendor/qrcode-generator.cjs` is [qrcode-generator](https://github.com/kazuhikoarase/qrcode-generator). See [NOTICE](NOTICE).
|
package/lib/atomic.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
// A file is written somewhere else first and then moved onto its place, so a
|
|
6
|
+
// reader never sees half a file. The name of the temporary file is one of its
|
|
7
|
+
// own for every write, so two writes cannot pull the same file away from
|
|
8
|
+
// under each other.
|
|
9
|
+
//
|
|
10
|
+
// Windows refuses to move a file onto a place that another program happens to
|
|
11
|
+
// hold open (a scanner, a viewer), and it says EPERM for that. The move is
|
|
12
|
+
// therefore tried a few times before the write is given up on.
|
|
13
|
+
const MOVES = 4;
|
|
14
|
+
const PAUSE_MS = 40;
|
|
15
|
+
|
|
16
|
+
const pause = ms => new Promise(resolve => { setTimeout(resolve, ms).unref?.(); });
|
|
17
|
+
|
|
18
|
+
export async function writeAtomic(file, data, { mode = 0o600 } = {}) {
|
|
19
|
+
const temporary = `${file}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
|
|
20
|
+
try {
|
|
21
|
+
await writeFile(temporary, data, { mode });
|
|
22
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
23
|
+
try {
|
|
24
|
+
await rename(temporary, file);
|
|
25
|
+
return;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (attempt >= MOVES || (error?.code !== 'EPERM' && error?.code !== 'EACCES' && error?.code !== 'EBUSY')) {
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
await pause(PAUSE_MS);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
await rm(temporary, { force: true }).catch(() => {});
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// When the program is killed between the write and the move, the temporary
|
|
40
|
+
// file stays behind. Nothing ever reads it, but it is litter, so it is swept
|
|
41
|
+
// away when the service starts.
|
|
42
|
+
export async function sweepTemporaries(file, { olderThanMs = 60 * 60 * 1000 } = {}) {
|
|
43
|
+
const folder = path.dirname(file);
|
|
44
|
+
const name = path.basename(file);
|
|
45
|
+
const prefix = `${name}.`;
|
|
46
|
+
let names = [];
|
|
47
|
+
try {
|
|
48
|
+
names = await readdir(folder);
|
|
49
|
+
} catch {
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
let swept = 0;
|
|
53
|
+
for (const entry of names) {
|
|
54
|
+
if (!entry.startsWith(prefix) || !entry.endsWith('.tmp')) continue;
|
|
55
|
+
const stale = path.join(folder, entry);
|
|
56
|
+
try {
|
|
57
|
+
const info = await stat(stale);
|
|
58
|
+
if (Date.now() - info.mtimeMs < olderThanMs) continue;
|
|
59
|
+
await rm(stale, { force: true });
|
|
60
|
+
swept += 1;
|
|
61
|
+
} catch {
|
|
62
|
+
// A file that cannot be looked at is left where it is.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return swept;
|
|
66
|
+
}
|