@seedvault/server 0.1.2 → 0.1.4
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/dist/index.html +721 -381
- package/dist/server.js +74 -5
- package/package.json +1 -1
package/dist/index.html
CHANGED
|
@@ -1,383 +1,723 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
2
|
<html lang="en">
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<title>Seedvault</title>
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;1,400&display=swap"
|
|
11
|
+
rel="stylesheet" />
|
|
12
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.2/src/regular/style.css" />
|
|
13
|
+
<style>
|
|
14
|
+
* {
|
|
15
|
+
box-sizing: border-box;
|
|
16
|
+
margin: 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
html {
|
|
20
|
+
color-scheme: light dark;
|
|
21
|
+
font-family: ui-monospace, monospace;
|
|
22
|
+
font-size: 12px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@media (prefers-color-scheme: dark) {
|
|
26
|
+
html {
|
|
27
|
+
background: rgb(18, 18, 18);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#nav {
|
|
31
|
+
color: rgb(204, 204, 204);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#content {
|
|
35
|
+
color: rgb(230, 230, 230);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
html,
|
|
40
|
+
body {
|
|
41
|
+
height: 100%;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
body {
|
|
45
|
+
padding: 12px;
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-direction: column;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.top {
|
|
51
|
+
display: flex;
|
|
52
|
+
gap: 8px;
|
|
53
|
+
margin-bottom: 12px;
|
|
54
|
+
flex-wrap: wrap;
|
|
55
|
+
flex-shrink: 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
input,
|
|
59
|
+
select,
|
|
60
|
+
button {
|
|
61
|
+
font: inherit;
|
|
62
|
+
padding: 4px 8px;
|
|
63
|
+
background: transparent;
|
|
64
|
+
color: inherit;
|
|
65
|
+
border: 1px solid color-mix(in srgb, currentColor 25%, transparent);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#token {
|
|
69
|
+
min-width: 260px;
|
|
70
|
+
flex: 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
button {
|
|
74
|
+
cursor: pointer;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.grid {
|
|
78
|
+
display: flex;
|
|
79
|
+
gap: 0;
|
|
80
|
+
flex: 1;
|
|
81
|
+
min-height: 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.grid>.panel:first-child {
|
|
85
|
+
flex-shrink: 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.grid>.panel:last-child {
|
|
89
|
+
flex: 1;
|
|
90
|
+
min-width: 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.divider {
|
|
94
|
+
width: 5px;
|
|
95
|
+
cursor: col-resize;
|
|
96
|
+
background: transparent;
|
|
97
|
+
flex-shrink: 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.divider:hover,
|
|
101
|
+
.divider.dragging {
|
|
102
|
+
background: color-mix(in srgb, currentColor 20%, transparent);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@media (max-width: 600px) {
|
|
106
|
+
.grid {
|
|
107
|
+
flex-direction: column;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.grid>.panel:first-child {
|
|
111
|
+
width: auto;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.divider {
|
|
115
|
+
display: none;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.panel {
|
|
120
|
+
border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
|
|
121
|
+
display: flex;
|
|
122
|
+
flex-direction: column;
|
|
123
|
+
min-height: 0;
|
|
124
|
+
overflow: hidden;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.panel-head {
|
|
128
|
+
padding: 4px 8px;
|
|
129
|
+
border-bottom: 1px solid color-mix(in srgb, currentColor 20%, transparent);
|
|
130
|
+
font-weight: bold;
|
|
131
|
+
font-size: 11px;
|
|
132
|
+
text-transform: uppercase;
|
|
133
|
+
letter-spacing: 0.05em;
|
|
134
|
+
flex-shrink: 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#nav-body:focus {
|
|
138
|
+
outline: none;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.panel-body {
|
|
142
|
+
flex: 1;
|
|
143
|
+
overflow: auto;
|
|
144
|
+
scrollbar-width: thin;
|
|
145
|
+
scrollbar-color: color-mix(in srgb, currentColor 20%, transparent) transparent;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.panel-body::-webkit-scrollbar,
|
|
149
|
+
#content::-webkit-scrollbar {
|
|
150
|
+
width: 6px;
|
|
151
|
+
height: 6px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.panel-body::-webkit-scrollbar-track,
|
|
155
|
+
#content::-webkit-scrollbar-track {
|
|
156
|
+
background: transparent;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.panel-body::-webkit-scrollbar-thumb,
|
|
160
|
+
#content::-webkit-scrollbar-thumb {
|
|
161
|
+
background: color-mix(in srgb, currentColor 20%, transparent);
|
|
162
|
+
border-radius: 3px;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.panel-body::-webkit-scrollbar-thumb:hover,
|
|
166
|
+
#content::-webkit-scrollbar-thumb:hover {
|
|
167
|
+
background: color-mix(in srgb, currentColor 30%, transparent);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.tree {
|
|
171
|
+
list-style: none;
|
|
172
|
+
padding: 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.tree ul {
|
|
176
|
+
list-style: none;
|
|
177
|
+
padding: 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.tree-row {
|
|
181
|
+
display: block;
|
|
182
|
+
width: 100%;
|
|
183
|
+
text-align: left;
|
|
184
|
+
border: none;
|
|
185
|
+
padding: 4px 8px;
|
|
186
|
+
white-space: nowrap;
|
|
187
|
+
overflow: hidden;
|
|
188
|
+
text-overflow: ellipsis;
|
|
189
|
+
cursor: pointer;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.tree-row:hover {
|
|
193
|
+
background: color-mix(in srgb, currentColor 8%, transparent);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.tree-row.active {
|
|
197
|
+
background: CanvasText;
|
|
198
|
+
color: Canvas;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.tree-row .arrow {
|
|
202
|
+
display: inline-block;
|
|
203
|
+
width: 1em;
|
|
204
|
+
text-align: center;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.tree-row .ph {
|
|
208
|
+
display: inline-block;
|
|
209
|
+
font-size: 14px;
|
|
210
|
+
margin-left: 12px;
|
|
211
|
+
margin-right: 2px;
|
|
212
|
+
vertical-align: -0.15em;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.tree ul.collapsed {
|
|
216
|
+
display: none;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#content {
|
|
220
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
221
|
+
font-size: 16px;
|
|
222
|
+
padding: 24px 32px;
|
|
223
|
+
overflow: auto;
|
|
224
|
+
line-height: 1.6;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#content h1 {
|
|
228
|
+
font-size: 1.75em;
|
|
229
|
+
font-weight: 700;
|
|
230
|
+
margin: 0 0 0.5em;
|
|
231
|
+
line-height: 1.3;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
#content h2 {
|
|
235
|
+
font-size: 1.4em;
|
|
236
|
+
font-weight: 600;
|
|
237
|
+
margin: 1em 0 0.5em;
|
|
238
|
+
line-height: 1.3;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
#content h3 {
|
|
242
|
+
font-size: 1.2em;
|
|
243
|
+
font-weight: 600;
|
|
244
|
+
margin: 1em 0 0.5em;
|
|
245
|
+
line-height: 1.3;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#content h4,
|
|
249
|
+
#content h5,
|
|
250
|
+
#content h6 {
|
|
251
|
+
font-size: 1.05em;
|
|
252
|
+
font-weight: 600;
|
|
253
|
+
margin: 0.75em 0 0.5em;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#content p {
|
|
257
|
+
margin: 0 0 0.75em;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
#content img {
|
|
261
|
+
max-width: 100%;
|
|
262
|
+
height: auto;
|
|
263
|
+
display: block;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
#content ul,
|
|
267
|
+
#content ol {
|
|
268
|
+
margin: 0 0 0.75em;
|
|
269
|
+
padding-left: 1.5em;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
#content pre,
|
|
273
|
+
#content code {
|
|
274
|
+
font-family: ui-monospace, monospace;
|
|
275
|
+
font-size: 0.9em;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
#content pre {
|
|
279
|
+
background: color-mix(in srgb, currentColor 8%, transparent);
|
|
280
|
+
padding: 10px;
|
|
281
|
+
border-radius: 4px;
|
|
282
|
+
overflow-x: auto;
|
|
283
|
+
margin: 0.5em 0;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#content code {
|
|
287
|
+
padding: 0.15em 0.3em;
|
|
288
|
+
border-radius: 3px;
|
|
289
|
+
background: color-mix(in srgb, currentColor 8%, transparent);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
#content pre code {
|
|
293
|
+
padding: 0;
|
|
294
|
+
background: none;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
#content blockquote {
|
|
298
|
+
border-left: 4px solid color-mix(in srgb, currentColor 30%, transparent);
|
|
299
|
+
margin: 0.5em 0;
|
|
300
|
+
padding-left: 1em;
|
|
301
|
+
color: color-mix(in srgb, currentColor 80%, transparent);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
#content a {
|
|
305
|
+
color: inherit;
|
|
306
|
+
text-decoration: underline;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#content hr {
|
|
310
|
+
border: none;
|
|
311
|
+
border-top: 1px solid color-mix(in srgb, currentColor 25%, transparent);
|
|
312
|
+
margin: 1.5em 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
#content table {
|
|
316
|
+
border-collapse: collapse;
|
|
317
|
+
margin: 0.5em 0;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
#content th,
|
|
321
|
+
#content td {
|
|
322
|
+
border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
|
|
323
|
+
padding: 6px 10px;
|
|
324
|
+
text-align: left;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
#status {
|
|
328
|
+
margin-top: 8px;
|
|
329
|
+
font-size: 11px;
|
|
330
|
+
opacity: 0.6;
|
|
331
|
+
flex-shrink: 0;
|
|
332
|
+
}
|
|
333
|
+
</style>
|
|
334
|
+
</head>
|
|
335
|
+
|
|
336
|
+
<body>
|
|
337
|
+
<div class="top">
|
|
338
|
+
<input id="token" type="password" placeholder="API key ("sv_...")" />
|
|
339
|
+
<button id="connect">Load</button>
|
|
340
|
+
</div>
|
|
341
|
+
<div class="grid">
|
|
342
|
+
<section id="nav" class="panel" style="width:220px">
|
|
343
|
+
<div class="panel-head">Files</div>
|
|
344
|
+
<div class="panel-body" id="nav-body" tabindex="0">
|
|
345
|
+
<ul id="files" class="tree"></ul>
|
|
346
|
+
</div>
|
|
347
|
+
</section>
|
|
348
|
+
<div id="divider" class="divider"></div>
|
|
349
|
+
<section class="panel">
|
|
350
|
+
<div class="panel-head">Content</div>
|
|
351
|
+
<div class="panel-body">
|
|
352
|
+
<div id="content"></div>
|
|
353
|
+
</div>
|
|
354
|
+
</section>
|
|
355
|
+
</div>
|
|
356
|
+
<div id="status"></div>
|
|
357
|
+
<script type="module">
|
|
358
|
+
const { marked } = await import("https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js");
|
|
359
|
+
const matter = (await import("https://cdn.jsdelivr.net/npm/gray-matter@4.0.3/+esm")).default;
|
|
360
|
+
const DOMPurify = (await import("https://cdn.jsdelivr.net/npm/dompurify@3.0.9/+esm")).default;
|
|
361
|
+
|
|
362
|
+
function renderMarkdown(raw) {
|
|
363
|
+
if (typeof raw !== "string") return "";
|
|
364
|
+
try {
|
|
365
|
+
const { content } = matter(raw);
|
|
366
|
+
const html = marked.parse(content);
|
|
367
|
+
return DOMPurify.sanitize(html, { USE_PROFILES: { html: true } });
|
|
368
|
+
} catch {
|
|
369
|
+
const escaped = raw
|
|
370
|
+
.replace(/&/g, "&")
|
|
371
|
+
.replace(/</g, "<")
|
|
372
|
+
.replace(/>/g, ">")
|
|
373
|
+
.replace(/"/g, """);
|
|
374
|
+
return "<pre>" + escaped + "</pre>";
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const $ = (id) => document.getElementById(id);
|
|
379
|
+
const tokenEl = $("token");
|
|
380
|
+
const filesEl = $("files");
|
|
381
|
+
const contentEl = $("content");
|
|
382
|
+
const statusEl = $("status");
|
|
383
|
+
|
|
384
|
+
let token = localStorage.getItem("sv-token") || "";
|
|
385
|
+
tokenEl.value = token;
|
|
386
|
+
|
|
387
|
+
function status(msg) { statusEl.textContent = msg; }
|
|
388
|
+
|
|
389
|
+
async function api(url, opts = {}) {
|
|
390
|
+
const res = await fetch(url, {
|
|
391
|
+
...opts,
|
|
392
|
+
headers: {
|
|
393
|
+
...(token ? { Authorization: "Bearer " + token } : {}),
|
|
394
|
+
...(opts.headers || {}),
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
if (!res.ok) {
|
|
398
|
+
const body = await res.json().catch(() => ({}));
|
|
399
|
+
throw new Error((body.error || "Request failed") + " (" + res.status + ")");
|
|
400
|
+
}
|
|
401
|
+
return res;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const savedContributor = localStorage.getItem("sv-contributor") || "";
|
|
405
|
+
const savedFile = localStorage.getItem("sv-file") || "";
|
|
406
|
+
|
|
407
|
+
function buildTree(fileEntries) {
|
|
408
|
+
const root = {};
|
|
409
|
+
for (const f of fileEntries) {
|
|
410
|
+
const parts = f.path.split("/");
|
|
411
|
+
let node = root;
|
|
412
|
+
for (const part of parts) {
|
|
413
|
+
if (!node[part]) node[part] = {};
|
|
414
|
+
node = node[part];
|
|
415
|
+
}
|
|
416
|
+
node.__file = f;
|
|
417
|
+
}
|
|
418
|
+
return root;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function getNewestCtime(node) {
|
|
422
|
+
if (node.__file) return node.__file.originCtime || node.__file.serverCreatedAt || "";
|
|
423
|
+
let newest = "";
|
|
424
|
+
for (const key of Object.keys(node)) {
|
|
425
|
+
if (key === "__file") continue;
|
|
426
|
+
const t = getNewestCtime(node[key]);
|
|
427
|
+
if (t > newest) newest = t;
|
|
428
|
+
}
|
|
429
|
+
return newest;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function countFiles(node) {
|
|
433
|
+
let count = 0;
|
|
434
|
+
for (const key of Object.keys(node)) {
|
|
435
|
+
if (key === "__file") { count++; continue; }
|
|
436
|
+
count += countFiles(node[key]);
|
|
437
|
+
}
|
|
438
|
+
return count;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function renderTree(node, parentUl, username, depth) {
|
|
442
|
+
const keys = Object.keys(node).filter((k) => k !== "__file").sort((a, b) => {
|
|
443
|
+
const aDir = Object.keys(node[a]).some((k) => k !== "__file");
|
|
444
|
+
const bDir = Object.keys(node[b]).some((k) => k !== "__file");
|
|
445
|
+
if (aDir !== bDir) return aDir ? -1 : 1;
|
|
446
|
+
const aTime = getNewestCtime(node[a]);
|
|
447
|
+
const bTime = getNewestCtime(node[b]);
|
|
448
|
+
if (aTime !== bTime) return bTime.localeCompare(aTime);
|
|
449
|
+
return a.localeCompare(b);
|
|
450
|
+
});
|
|
451
|
+
for (const key of keys) {
|
|
452
|
+
const child = node[key];
|
|
453
|
+
const isFile = child.__file && Object.keys(child).length === 1;
|
|
454
|
+
const li = document.createElement("li");
|
|
455
|
+
const row = document.createElement("div");
|
|
456
|
+
row.className = "tree-row";
|
|
457
|
+
row.style.paddingLeft = (depth * 12 + 8) + "px";
|
|
458
|
+
|
|
459
|
+
if (isFile) {
|
|
460
|
+
row.innerHTML = '<span class="arrow"> </span><i class="ph ph-file-text"></i> ' + key;
|
|
461
|
+
row.dataset.path = child.__file.path;
|
|
462
|
+
row.dataset.contributor = username;
|
|
463
|
+
row.onclick = () => loadContent(username, child.__file.path, row);
|
|
464
|
+
} else {
|
|
465
|
+
const sub = document.createElement("ul");
|
|
466
|
+
const fileCount = countFiles(child);
|
|
467
|
+
row.innerHTML = '<span class="arrow">▼</span><i class="ph ph-folder"></i> ' + key + ' <span style="opacity:0.5">(' + fileCount + ')</span>';
|
|
468
|
+
row.onclick = () => {
|
|
469
|
+
sub.classList.toggle("collapsed");
|
|
470
|
+
row.querySelector(".arrow").innerHTML = sub.classList.contains("collapsed") ? "▶" : "▼";
|
|
471
|
+
};
|
|
472
|
+
renderTree(child, sub, username, depth + 1);
|
|
473
|
+
li.appendChild(row);
|
|
474
|
+
li.appendChild(sub);
|
|
475
|
+
parentUl.appendChild(li);
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
li.appendChild(row);
|
|
479
|
+
parentUl.appendChild(li);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function markActiveRow() {
|
|
484
|
+
filesEl.querySelectorAll(".tree-row").forEach((r) => r.classList.remove("active"));
|
|
485
|
+
const activePath = localStorage.getItem("sv-file");
|
|
486
|
+
const activeContributor = localStorage.getItem("sv-contributor");
|
|
487
|
+
if (!activePath || !activeContributor) return;
|
|
488
|
+
const active = filesEl.querySelector(
|
|
489
|
+
'.tree-row[data-contributor="' + CSS.escape(activeContributor) + '"][data-path="' + CSS.escape(activePath) + '"]'
|
|
490
|
+
);
|
|
491
|
+
if (active) {
|
|
492
|
+
active.classList.add("active");
|
|
493
|
+
active.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function getVisibleTreeRows() {
|
|
498
|
+
const rows = [];
|
|
499
|
+
function walk(ul) {
|
|
500
|
+
if (!ul || ul.classList.contains("collapsed")) return;
|
|
501
|
+
for (const li of ul.children) {
|
|
502
|
+
const row = li.querySelector(":scope > .tree-row");
|
|
503
|
+
if (row && row.dataset.path) rows.push(row);
|
|
504
|
+
walk(li.querySelector(":scope > ul"));
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
walk(filesEl);
|
|
508
|
+
return rows;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function handleNavKeydown(e) {
|
|
512
|
+
if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return;
|
|
513
|
+
if (["INPUT", "TEXTAREA"].includes(document.activeElement?.tagName)) return;
|
|
514
|
+
const rows = getVisibleTreeRows();
|
|
515
|
+
if (rows.length === 0) return;
|
|
516
|
+
const active = filesEl.querySelector(".tree-row.active");
|
|
517
|
+
let idx = active ? rows.indexOf(active) : -1;
|
|
518
|
+
if (e.key === "ArrowDown") {
|
|
519
|
+
idx = idx < rows.length - 1 ? idx + 1 : idx;
|
|
520
|
+
} else {
|
|
521
|
+
idx = idx > 0 ? idx - 1 : 0;
|
|
522
|
+
}
|
|
523
|
+
e.preventDefault();
|
|
524
|
+
rows[idx].click();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function getLoadedContributorList(username) {
|
|
528
|
+
return filesEl.querySelector(
|
|
529
|
+
'ul[data-contributor="' + CSS.escape(username) + '"][data-loaded="true"]'
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async function loadContributorFiles(username, sub, opts = {}) {
|
|
534
|
+
const silent = !!opts.silent;
|
|
535
|
+
sub.innerHTML = "";
|
|
536
|
+
if (!silent) status("Loading files...");
|
|
537
|
+
const { files } = await (await api("/v1/files?prefix=" + encodeURIComponent(username + "/"))).json();
|
|
538
|
+
const prefix = username + "/";
|
|
539
|
+
const tree = buildTree(files.map((f) => ({
|
|
540
|
+
...f,
|
|
541
|
+
path: f.path.startsWith(prefix) ? f.path.slice(prefix.length) : f.path,
|
|
542
|
+
})));
|
|
543
|
+
renderTree(tree, sub, username, 1);
|
|
544
|
+
markActiveRow();
|
|
545
|
+
if (!silent) status(files.length + " file(s)");
|
|
546
|
+
// Update contributor row with file count
|
|
547
|
+
const row = sub.parentElement && sub.parentElement.querySelector(".tree-row");
|
|
548
|
+
if (row) {
|
|
549
|
+
const arrow = row.querySelector(".arrow").outerHTML;
|
|
550
|
+
row.innerHTML = arrow + '<i class="ph ph-user"></i> ' + username + ' <span style="opacity:0.5">(' + files.length + ')</span>';
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async function loadContributors() {
|
|
555
|
+
filesEl.innerHTML = "";
|
|
556
|
+
contentEl.innerHTML = "";
|
|
557
|
+
status("Loading contributors...");
|
|
558
|
+
const { contributors } = await (await api("/v1/contributors")).json();
|
|
559
|
+
|
|
560
|
+
for (const b of contributors) {
|
|
561
|
+
const li = document.createElement("li");
|
|
562
|
+
const row = document.createElement("div");
|
|
563
|
+
row.className = "tree-row";
|
|
564
|
+
row.style.paddingLeft = "8px";
|
|
565
|
+
const sub = document.createElement("ul");
|
|
566
|
+
sub.dataset.contributor = b.username;
|
|
567
|
+
sub.dataset.loaded = "false";
|
|
568
|
+
let loaded = false;
|
|
569
|
+
row.innerHTML = '<span class="arrow">▶</span><i class="ph ph-user"></i> ' + b.username;
|
|
570
|
+
row.onclick = async () => {
|
|
571
|
+
const collapsed = sub.classList.contains("collapsed") || !sub.hasChildNodes();
|
|
572
|
+
if (collapsed) {
|
|
573
|
+
sub.classList.remove("collapsed");
|
|
574
|
+
row.querySelector(".arrow").innerHTML = "▼";
|
|
575
|
+
if (!loaded) {
|
|
576
|
+
loaded = true;
|
|
577
|
+
sub.dataset.loaded = "true";
|
|
578
|
+
await loadContributorFiles(b.username, sub);
|
|
579
|
+
}
|
|
580
|
+
} else {
|
|
581
|
+
sub.classList.add("collapsed");
|
|
582
|
+
row.querySelector(".arrow").innerHTML = "▶";
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
li.appendChild(row);
|
|
586
|
+
li.appendChild(sub);
|
|
587
|
+
filesEl.appendChild(li);
|
|
588
|
+
|
|
589
|
+
if (b.username === savedContributor) {
|
|
590
|
+
sub.classList.remove("collapsed");
|
|
591
|
+
row.querySelector(".arrow").innerHTML = "▼";
|
|
592
|
+
loaded = true;
|
|
593
|
+
sub.dataset.loaded = "true";
|
|
594
|
+
await loadContributorFiles(b.username, sub);
|
|
595
|
+
if (savedFile) {
|
|
596
|
+
const match = filesEl.querySelector('[data-path="' + CSS.escape(savedFile) + '"]');
|
|
597
|
+
if (match) await loadContent(b.username, savedFile, match);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
status(contributors.length + " contributor(s)");
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function loadContent(username, path, row) {
|
|
605
|
+
filesEl.querySelectorAll(".tree-row").forEach((r) => r.classList.remove("active"));
|
|
606
|
+
row.classList.add("active");
|
|
607
|
+
row.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
608
|
+
localStorage.setItem("sv-contributor", username);
|
|
609
|
+
localStorage.setItem("sv-file", path);
|
|
610
|
+
status("Loading " + path + "...");
|
|
611
|
+
const res = await api("/v1/sh", {
|
|
612
|
+
method: "POST",
|
|
613
|
+
headers: { "Content-Type": "application/json" },
|
|
614
|
+
body: JSON.stringify({ cmd: 'cat "' + username + "/" + path + '"' }),
|
|
615
|
+
});
|
|
616
|
+
const text = await res.text();
|
|
617
|
+
contentEl.innerHTML = renderMarkdown(text);
|
|
618
|
+
contentEl.parentElement.scrollTop = 0;
|
|
619
|
+
status(path);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
$("connect").onclick = () => {
|
|
623
|
+
token = tokenEl.value.trim();
|
|
624
|
+
localStorage.setItem("sv-token", token);
|
|
625
|
+
loadContributors().then(() => connectSSE()).catch((e) => status(e.message));
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
// --- SSE real-time updates ---
|
|
629
|
+
let evtSource = null;
|
|
630
|
+
const contributorReloadTimers = new Map();
|
|
631
|
+
|
|
632
|
+
function scheduleContributorReload(username) {
|
|
633
|
+
if (!getLoadedContributorList(username)) return;
|
|
634
|
+
|
|
635
|
+
const existing = contributorReloadTimers.get(username);
|
|
636
|
+
if (existing) clearTimeout(existing);
|
|
637
|
+
|
|
638
|
+
const timer = setTimeout(() => {
|
|
639
|
+
contributorReloadTimers.delete(username);
|
|
640
|
+
const targetUl = getLoadedContributorList(username);
|
|
641
|
+
if (!targetUl) return;
|
|
642
|
+
loadContributorFiles(username, targetUl, { silent: true }).catch((e) => status(e.message));
|
|
643
|
+
}, 100);
|
|
644
|
+
|
|
645
|
+
contributorReloadTimers.set(username, timer);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function connectSSE() {
|
|
649
|
+
if (evtSource) evtSource.close();
|
|
650
|
+
if (!token) return;
|
|
651
|
+
|
|
652
|
+
evtSource = new EventSource("/v1/events?token=" + encodeURIComponent(token));
|
|
653
|
+
|
|
654
|
+
evtSource.addEventListener("file_updated", (e) => {
|
|
655
|
+
const { contributor, path } = JSON.parse(e.data);
|
|
656
|
+
scheduleContributorReload(contributor);
|
|
657
|
+
// If this file is currently open, reload its content
|
|
658
|
+
if (localStorage.getItem("sv-file") === path && localStorage.getItem("sv-contributor") === contributor) {
|
|
659
|
+
api("/v1/sh", {
|
|
660
|
+
method: "POST",
|
|
661
|
+
headers: { "Content-Type": "application/json" },
|
|
662
|
+
body: JSON.stringify({ cmd: 'cat "' + contributor + "/" + path + '"' }),
|
|
663
|
+
})
|
|
664
|
+
.then((res) => res.text())
|
|
665
|
+
.then((text) => { contentEl.innerHTML = renderMarkdown(text); });
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
evtSource.addEventListener("file_deleted", (e) => {
|
|
670
|
+
const { contributor, path } = JSON.parse(e.data);
|
|
671
|
+
scheduleContributorReload(contributor);
|
|
672
|
+
// If this file was being viewed, clear the content
|
|
673
|
+
if (localStorage.getItem("sv-file") === path && localStorage.getItem("sv-contributor") === contributor) {
|
|
674
|
+
contentEl.innerHTML = "";
|
|
675
|
+
status("File deleted: " + path);
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
evtSource.onerror = () => {
|
|
680
|
+
// EventSource auto-reconnects
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (token) loadContributors().then(() => connectSSE()).catch((e) => status(e.message));
|
|
685
|
+
|
|
686
|
+
// Nav keyboard: up/down to move selection
|
|
687
|
+
document.addEventListener("keydown", (e) => {
|
|
688
|
+
if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return;
|
|
689
|
+
if (["INPUT", "TEXTAREA"].includes(document.activeElement?.tagName)) return;
|
|
690
|
+
if (!$("nav-body").contains(document.activeElement) && document.activeElement !== $("nav-body")) return;
|
|
691
|
+
handleNavKeydown(e);
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
// Focus nav when clicking in it
|
|
695
|
+
$("nav").addEventListener("mousedown", (e) => {
|
|
696
|
+
if (e.target.closest("#nav-body")) $("nav-body").focus();
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
// Divider drag
|
|
700
|
+
const divider = $("divider");
|
|
701
|
+
const nav = $("nav");
|
|
702
|
+
const savedNav = localStorage.getItem("sv-nav-w");
|
|
703
|
+
if (savedNav) nav.style.width = savedNav + "px";
|
|
704
|
+
divider.addEventListener("mousedown", (e) => {
|
|
705
|
+
e.preventDefault();
|
|
706
|
+
divider.classList.add("dragging");
|
|
707
|
+
const onMove = (e) => {
|
|
708
|
+
const w = e.clientX - nav.getBoundingClientRect().left;
|
|
709
|
+
if (w > 80 && w < window.innerWidth - 120) nav.style.width = w + "px";
|
|
710
|
+
};
|
|
711
|
+
const onUp = () => {
|
|
712
|
+
divider.classList.remove("dragging");
|
|
713
|
+
localStorage.setItem("sv-nav-w", nav.offsetWidth);
|
|
714
|
+
document.removeEventListener("mousemove", onMove);
|
|
715
|
+
document.removeEventListener("mouseup", onUp);
|
|
716
|
+
};
|
|
717
|
+
document.addEventListener("mousemove", onMove);
|
|
718
|
+
document.addEventListener("mouseup", onUp);
|
|
719
|
+
});
|
|
720
|
+
</script>
|
|
721
|
+
</body>
|
|
722
|
+
|
|
723
|
+
</html>
|
package/dist/server.js
CHANGED
|
@@ -40,6 +40,17 @@ function initDb(dbPath) {
|
|
|
40
40
|
used_at TEXT,
|
|
41
41
|
used_by TEXT REFERENCES contributors(username)
|
|
42
42
|
);
|
|
43
|
+
|
|
44
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
45
|
+
contributor TEXT NOT NULL,
|
|
46
|
+
path TEXT NOT NULL,
|
|
47
|
+
origin_ctime TEXT NOT NULL,
|
|
48
|
+
origin_mtime TEXT NOT NULL,
|
|
49
|
+
server_created_at TEXT NOT NULL,
|
|
50
|
+
server_modified_at TEXT NOT NULL,
|
|
51
|
+
PRIMARY KEY (contributor, path),
|
|
52
|
+
FOREIGN KEY (contributor) REFERENCES contributors(username)
|
|
53
|
+
);
|
|
43
54
|
`);
|
|
44
55
|
return db;
|
|
45
56
|
}
|
|
@@ -98,6 +109,34 @@ function getInvite(id) {
|
|
|
98
109
|
function markInviteUsed(id, usedBy) {
|
|
99
110
|
getDb().prepare("UPDATE invites SET used_at = ?, used_by = ? WHERE id = ?").run(new Date().toISOString(), usedBy, id);
|
|
100
111
|
}
|
|
112
|
+
function upsertFileMetadata(contributor, path, originCtime, originMtime) {
|
|
113
|
+
const now = new Date().toISOString();
|
|
114
|
+
getDb().prepare(`INSERT INTO files (contributor, path, origin_ctime, origin_mtime, server_created_at, server_modified_at)
|
|
115
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
116
|
+
ON CONFLICT (contributor, path) DO UPDATE SET
|
|
117
|
+
origin_mtime = excluded.origin_mtime,
|
|
118
|
+
server_modified_at = excluded.server_modified_at`).run(contributor, path, originCtime, originMtime, now, now);
|
|
119
|
+
return getFileMetadata(contributor, path);
|
|
120
|
+
}
|
|
121
|
+
function getFileMetadata(contributor, path) {
|
|
122
|
+
return getDb().prepare("SELECT * FROM files WHERE contributor = ? AND path = ?").get(contributor, path);
|
|
123
|
+
}
|
|
124
|
+
function listFileMetadata(contributor, prefix) {
|
|
125
|
+
const map = new Map;
|
|
126
|
+
let rows;
|
|
127
|
+
if (prefix) {
|
|
128
|
+
rows = getDb().prepare("SELECT * FROM files WHERE contributor = ? AND path LIKE ?").all(contributor, prefix + "%");
|
|
129
|
+
} else {
|
|
130
|
+
rows = getDb().prepare("SELECT * FROM files WHERE contributor = ?").all(contributor);
|
|
131
|
+
}
|
|
132
|
+
for (const row of rows) {
|
|
133
|
+
map.set(row.path, row);
|
|
134
|
+
}
|
|
135
|
+
return map;
|
|
136
|
+
}
|
|
137
|
+
function deleteFileMetadata(contributor, path) {
|
|
138
|
+
getDb().prepare("DELETE FROM files WHERE contributor = ? AND path = ?").run(contributor, path);
|
|
139
|
+
}
|
|
101
140
|
|
|
102
141
|
// ../node_modules/.bun/hono@4.11.9/node_modules/hono/dist/compose.js
|
|
103
142
|
var compose = (middleware, onError, onNotFound) => {
|
|
@@ -2128,8 +2167,12 @@ function createApp(storageRoot) {
|
|
|
2128
2167
|
return c.json({ error: pathError }, 400);
|
|
2129
2168
|
}
|
|
2130
2169
|
const content = await c.req.text();
|
|
2170
|
+
const now = new Date().toISOString();
|
|
2171
|
+
const originCtime = c.req.header("X-Origin-Ctime") || now;
|
|
2172
|
+
const originMtime = c.req.header("X-Origin-Mtime") || now;
|
|
2131
2173
|
try {
|
|
2132
2174
|
const result = await writeFileAtomic(storageRoot, parsed.username, parsed.filePath, content);
|
|
2175
|
+
const meta = upsertFileMetadata(parsed.username, parsed.filePath, originCtime, originMtime);
|
|
2133
2176
|
broadcast("file_updated", {
|
|
2134
2177
|
contributor: parsed.username,
|
|
2135
2178
|
path: result.path,
|
|
@@ -2137,7 +2180,13 @@ function createApp(storageRoot) {
|
|
|
2137
2180
|
modifiedAt: result.modifiedAt
|
|
2138
2181
|
});
|
|
2139
2182
|
triggerUpdate();
|
|
2140
|
-
return c.json(
|
|
2183
|
+
return c.json({
|
|
2184
|
+
...result,
|
|
2185
|
+
originCtime: meta.origin_ctime,
|
|
2186
|
+
originMtime: meta.origin_mtime,
|
|
2187
|
+
serverCreatedAt: meta.server_created_at,
|
|
2188
|
+
serverModifiedAt: meta.server_modified_at
|
|
2189
|
+
});
|
|
2141
2190
|
} catch (e) {
|
|
2142
2191
|
if (e instanceof FileTooLargeError) {
|
|
2143
2192
|
return c.json({ error: e.message }, 413);
|
|
@@ -2160,6 +2209,7 @@ function createApp(storageRoot) {
|
|
|
2160
2209
|
}
|
|
2161
2210
|
try {
|
|
2162
2211
|
await deleteFile(storageRoot, parsed.username, parsed.filePath);
|
|
2212
|
+
deleteFileMetadata(parsed.username, parsed.filePath);
|
|
2163
2213
|
broadcast("file_deleted", {
|
|
2164
2214
|
contributor: parsed.username,
|
|
2165
2215
|
path: parsed.filePath
|
|
@@ -2185,15 +2235,24 @@ function createApp(storageRoot) {
|
|
|
2185
2235
|
return c.json({ error: "Contributor not found" }, 404);
|
|
2186
2236
|
}
|
|
2187
2237
|
const files = await listFiles(storageRoot, username, subPrefix);
|
|
2238
|
+
const metaMap = listFileMetadata(username, subPrefix);
|
|
2188
2239
|
return c.json({
|
|
2189
|
-
files: files.map((f) =>
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2240
|
+
files: files.map((f) => {
|
|
2241
|
+
const meta = metaMap.get(f.path);
|
|
2242
|
+
return {
|
|
2243
|
+
...f,
|
|
2244
|
+
path: `${username}/${f.path}`,
|
|
2245
|
+
originCtime: meta?.origin_ctime,
|
|
2246
|
+
originMtime: meta?.origin_mtime,
|
|
2247
|
+
serverCreatedAt: meta?.server_created_at,
|
|
2248
|
+
serverModifiedAt: meta?.server_modified_at
|
|
2249
|
+
};
|
|
2250
|
+
})
|
|
2193
2251
|
});
|
|
2194
2252
|
});
|
|
2195
2253
|
authed.get("/v1/events", (c) => {
|
|
2196
2254
|
let ctrl;
|
|
2255
|
+
let heartbeat;
|
|
2197
2256
|
const stream = new ReadableStream({
|
|
2198
2257
|
start(controller) {
|
|
2199
2258
|
ctrl = controller;
|
|
@@ -2203,8 +2262,18 @@ data: {}
|
|
|
2203
2262
|
|
|
2204
2263
|
`;
|
|
2205
2264
|
controller.enqueue(new TextEncoder().encode(msg));
|
|
2265
|
+
heartbeat = setInterval(() => {
|
|
2266
|
+
try {
|
|
2267
|
+
controller.enqueue(new TextEncoder().encode(`:keepalive
|
|
2268
|
+
|
|
2269
|
+
`));
|
|
2270
|
+
} catch {
|
|
2271
|
+
clearInterval(heartbeat);
|
|
2272
|
+
}
|
|
2273
|
+
}, 30000);
|
|
2206
2274
|
},
|
|
2207
2275
|
cancel() {
|
|
2276
|
+
clearInterval(heartbeat);
|
|
2208
2277
|
removeClient(ctrl);
|
|
2209
2278
|
}
|
|
2210
2279
|
});
|