@numa-tech/numa 1.18.0 → 1.19.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 +30 -10
- package/THIRD_PARTY_NOTICES.md +50 -0
- package/dist/oss/commands.js +7 -4
- package/dist/oss/commands.js.map +1 -1
- package/dist/oss/local-picker.d.ts +23 -0
- package/dist/oss/local-picker.js +245 -0
- package/dist/oss/local-picker.js.map +1 -0
- package/dist/oss/transfer-paths.d.ts +16 -0
- package/dist/oss/transfer-paths.js +131 -0
- package/dist/oss/transfer-paths.js.map +1 -0
- package/dist/oss/transfer-queue.d.ts +62 -0
- package/dist/oss/transfer-queue.js +349 -0
- package/dist/oss/transfer-queue.js.map +1 -0
- package/dist/oss/transfer-types.d.ts +54 -0
- package/dist/oss/transfer-types.js +2 -0
- package/dist/oss/transfer-types.js.map +1 -0
- package/dist/oss/transfer-worker.d.ts +3 -0
- package/dist/oss/transfer-worker.js +415 -0
- package/dist/oss/transfer-worker.js.map +1 -0
- package/dist/oss/transfers.d.ts +16 -0
- package/dist/oss/transfers.js +24 -0
- package/dist/oss/transfers.js.map +1 -1
- package/dist/oss/web-icons.d.ts +3 -0
- package/dist/oss/web-icons.js +4 -0
- package/dist/oss/web-icons.js.map +1 -0
- package/dist/oss/web-page.js +162 -52
- package/dist/oss/web-page.js.map +1 -1
- package/dist/oss/web.d.ts +6 -0
- package/dist/oss/web.js +214 -75
- package/dist/oss/web.js.map +1 -1
- package/package.json +5 -5
package/dist/oss/web-page.js
CHANGED
|
@@ -1,24 +1,51 @@
|
|
|
1
|
+
import { OSS_REFRESH_ICON, OSS_COPY_ICON } from "./web-icons.js";
|
|
1
2
|
/** Self-contained local assets: no CDN, remote fonts, analytics or credential-bearing markup. */
|
|
2
3
|
export const OSS_WEB_HTML = String.raw `<!doctype html>
|
|
3
4
|
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
4
5
|
<title>Numa · OSS 文件</title><link rel="icon" href="data:,"><link rel="stylesheet" href="/app.css"><script src="/app.js" defer></script></head>
|
|
5
|
-
<body><header class="topbar"><a class="brand" href="/">numa<span> / OSS</span></a><span class="local"><i></i> 本机连接</span><span class="topnote">文件工作区</span></header>
|
|
6
|
-
<div class="authorization"><div class="grant-field"><label for="grant">当前授权</label><select id="grant" aria-label="选择 OSS Grant"><option>正在读取授权…</option></select><button id="grants-refresh" class="quiet icon-button" title="刷新授权列表" aria-label="刷新授权列表"
|
|
6
|
+
<body><header class="topbar"><a class="brand" href="/">numa<span> / OSS</span></a><span class="local"><i></i> 本机连接</span><span class="topnote">文件工作区</span><button id="tasks-toggle" aria-controls="tasks-panel" aria-expanded="false">传输任务 <span id="tasks-count">0</span></button></header>
|
|
7
|
+
<div class="authorization"><div class="grant-field"><label for="grant">当前授权</label><select id="grant" aria-label="选择 OSS Grant"><option>正在读取授权…</option></select><button id="grants-refresh" class="quiet icon-button" title="刷新授权列表" aria-label="刷新授权列表">${OSS_REFRESH_ICON}</button></div>
|
|
7
8
|
<div class="session-controls"><div class="expiry"><span>STS 剩余时间</span><strong id="countdown" aria-live="off">--:--</strong></div><label class="check"><input id="auto-renew" type="checkbox" checked>自动续期</label><button id="renew">立即续期</button></div></div>
|
|
8
9
|
<div id="notice" class="notice" role="status" hidden></div>
|
|
9
10
|
<div class="workspace"><aside class="sidebar"><div class="eyebrow">存储位置</div><button id="root" class="root-button"><span class="folder-icon"></span><span id="bucket">正在连接</span></button><div class="scope-label">授权目录</div><div id="scope" class="scope">—</div><div class="sidebar-bottom"><span id="permission">正在读取权限</span><p>文件操作仅限当前授权范围。<br>关闭 CLI 将断开本机连接。</p></div></aside>
|
|
10
|
-
<main class="files"><div class="page-heading"><div><div class="eyebrow">OBJECT STORAGE</div><h1>文件</h1></div><
|
|
11
|
-
<div class="location"><button id="up" class="quiet" title="返回上级目录" aria-label="返回上级目录">↑</button><nav id="breadcrumbs" aria-label="文件路径"></nav><button id="refresh" class="quiet"
|
|
11
|
+
<main class="files"><div class="page-heading"><div><div class="eyebrow">OBJECT STORAGE</div><h1>文件</h1></div><div class="upload-actions"><button id="upload-folder" disabled>上传文件夹</button><button id="upload" class="primary" disabled>+ 上传文件</button></div></div>
|
|
12
|
+
<div class="location"><button id="up" class="quiet" title="返回上级目录" aria-label="返回上级目录">↑</button><nav id="breadcrumbs" aria-label="文件路径"></nav><button id="refresh" class="quiet icon-button" title="刷新文件列表" aria-label="刷新文件列表">${OSS_REFRESH_ICON}</button></div>
|
|
12
13
|
<div class="toolbar"><input id="search" type="search" placeholder="筛选已加载的文件与目录…" aria-label="筛选已加载的文件与目录"><span id="selection-count"></span><button id="download-selected" disabled>下载所选</button><button id="delete-selected" class="danger" disabled>删除所选</button></div>
|
|
13
|
-
<div class="table-wrap"><table><thead><tr><th class="check-cell"><input id="select-all" type="checkbox" aria-label="
|
|
14
|
+
<div class="table-wrap"><table><thead><tr><th class="check-cell"><input id="select-all" type="checkbox" aria-label="选择已显示的文件与目录"></th><th>名称</th><th class="size-cell">大小</th><th class="date-cell">修改时间</th></tr></thead><tbody id="rows"></tbody></table><div id="empty" class="empty">正在连接当前授权…</div></div>
|
|
14
15
|
<footer class="list-footer"><span id="list-summary">—</span><button id="more" hidden>加载更多</button></footer>
|
|
15
|
-
|
|
16
|
+
|
|
16
17
|
</main><aside class="preview-panel"><div class="preview-heading"><h2>文件预览</h2><button id="preview-close" class="quiet" aria-label="关闭预览">×</button></div><div id="preview" class="preview-body"><div class="preview-empty">选择一个文件<br><small>查看内容与文件信息</small></div></div><div class="preview-actions"><button id="download" disabled>下载文件</button><button id="delete" class="danger" disabled>删除文件</button></div></aside></div>
|
|
17
18
|
<dialog id="delete-dialog"><h2>删除所选文件?</h2><p>此操作会删除远程 OSS 对象,不能在此页面撤销。</p><ul id="delete-list"></ul><p id="delete-target" class="scope"></p><div class="dialog-actions"><button id="delete-cancel">取消</button><button id="delete-confirm" class="destructive">确认删除</button></div></dialog>
|
|
19
|
+
<section id="tasks-panel" class="tasks-panel" aria-labelledby="tasks-heading" hidden><div class="tasks-heading"><h2 id="tasks-heading">传输任务</h2><button id="tasks-close" class="quiet" aria-label="关闭传输任务">×</button></div><p class="tasks-note">由本机 CLI 持续传输,关闭浏览器后仍会继续。CLI 重启后,未完成任务会暂停,需手动恢复。</p><p id="tasks-connection" role="status" hidden></p><div id="tasks-list" class="tasks-list"></div><p id="tasks-empty" class="tasks-note">暂无任务。选择文件或文件夹开始传输。</p></section>
|
|
20
|
+
<template id="copy-icon">${OSS_COPY_ICON}</template>
|
|
18
21
|
</body></html>`;
|
|
19
22
|
export const OSS_WEB_CSS = String.raw `
|
|
20
23
|
.icon-button{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;min-width:34px;min-height:34px}
|
|
21
24
|
:root{font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif;color:#23322e;background:#f5f7f5;font-size:14px;font-synthesis:none}*{box-sizing:border-box}body{margin:0}button,input,select{font:inherit}button,select{cursor:pointer}button{border:1px solid #d8e0db;background:white;color:#344b41;border-radius:6px;padding:8px 12px;white-space:nowrap}button:hover:not(:disabled){background:#eff5f1;border-color:#a5bcae}button:disabled{opacity:.4;cursor:not-allowed}input:focus-visible,button:focus-visible,select:focus-visible{outline:2px solid #16805a;outline-offset:2px}input[type=checkbox]{accent-color:#197854;width:15px;height:15px;cursor:pointer}input[type=search]{border:1px solid #dbe3de;border-radius:6px;padding:9px 12px;min-width:160px;background:#fafcfb;color:#23322e}select{border:0;background:transparent;color:#263d31;max-width:100%;padding:6px 24px 6px 0;font-weight:550}h1,h2,p{margin:0}h1{font-size:27px;letter-spacing:-.7px;font-weight:600}h2{font-size:15px;font-weight:600}.topbar{height:62px;padding:0 26px;background:#20372e;color:#ecf4ed;display:flex;align-items:center;gap:20px}.brand{font-size:24px;letter-spacing:-1px;font-weight:650;color:inherit;text-decoration:none}.brand span{font-size:16px;font-weight:400;letter-spacing:.5px;color:#a9c0b0;margin-left:12px}.local{font-size:11px;color:#cee0d5;display:flex;align-items:center;gap:6px}.local i{height:6px;width:6px;background:#8edaac;border-radius:50%}.topnote{margin-left:auto;color:#aac0b2;font-size:12px;letter-spacing:2px}.authorization{display:flex;align-items:center;justify-content:space-between;gap:24px;background:#fff;border-bottom:1px solid #dbe3de;padding:16px 26px}.grant-field{min-width:0;display:flex;align-items:center;gap:12px;flex:1}.grant-field label{font-size:12px;color:#7a8c80;white-space:nowrap}.session-controls{display:flex;align-items:center;gap:18px;flex-shrink:0}.expiry{display:flex;align-items:center;gap:10px;font-size:11px;color:#718076}.expiry strong{font-family:ui-monospace,SFMono-Regular,monospace;color:#2b674c;font-size:20px;font-weight:500;min-width:62px}.expiry strong.warning{color:#b45309}.expiry strong.expired{color:#bf403d}.check{display:flex;align-items:center;gap:5px;font-size:12px;white-space:nowrap}.quiet{background:transparent;border-color:transparent;color:#718577;padding:6px 8px}.primary{color:white;background:#227b54;border-color:#227b54}.primary:hover:not(:disabled){background:#196140;color:white}.danger{color:#9f4b40}.destructive{color:white;background:#a7473d;border-color:#a7473d}.notice{margin:0;padding:10px 26px;background:#fff4df;border-bottom:1px solid #ead9b8;color:#845422;font-size:13px;line-height:1.6}.notice.success{background:#e9f6ec;color:#266342;border-color:#c9e3d0}.workspace{display:grid;grid-template-columns:205px minmax(420px,1fr) 310px;min-height:calc(100vh - 137px)}.sidebar{border-right:1px solid #dbe3de;padding:30px 16px;display:flex;flex-direction:column;background:#f0f4f0;min-width:0}.eyebrow{font-size:10px;letter-spacing:1.7px;color:#89988d;font-weight:600}.sidebar .eyebrow{padding-left:10px}.root-button{display:flex;align-items:center;gap:10px;margin-top:18px;white-space:normal;text-align:left;overflow-wrap:anywhere;background:#e1ebe2;border:0;color:#24573d;font-size:13px}.folder-icon{display:inline-block;width:16px;height:11px;background:#9fbba1;border-radius:2px;position:relative;flex-shrink:0;margin-top:3px}.folder-icon:before{content:"";position:absolute;left:0;top:-3px;width:7px;height:4px;border-radius:2px 2px 0 0;background:inherit}.scope-label{font-size:11px;color:#86978a;margin:23px 10px 9px}.scope{font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;color:#637c6a;overflow-wrap:anywhere;line-height:1.7}.sidebar>.scope{padding:0 10px}.sidebar-bottom{margin-top:auto;padding:40px 10px 0;color:#607e68;font-size:12px}.sidebar-bottom p{margin-top:12px;line-height:1.8;color:#8b9b8e;font-size:11px}.files{background:white;min-width:0;padding:30px 28px 0;display:flex;flex-direction:column}.page-heading{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px}.page-heading .eyebrow{margin-bottom:8px}.location{display:flex;align-items:center;gap:6px;border-top:1px solid #e8ede9;border-bottom:1px solid #e8ede9;padding:9px 0;min-width:0}.location nav{display:flex;align-items:center;flex-wrap:wrap;flex:1;gap:2px;min-width:0}.location nav button{padding:4px 7px;border:0;font-size:12px;max-width:220px;overflow:hidden;text-overflow:ellipsis}.location nav span{color:#c1ccc4}.toolbar{display:flex;gap:8px;align-items:center;margin:18px 0}.toolbar input[type=search]{flex:1;min-width:0}.toolbar button{font-size:12px;padding:7px 9px}#selection-count{font-size:11px;color:#819286;white-space:nowrap}.table-wrap{overflow-x:auto;min-height:290px}table{width:100%;border-collapse:collapse;table-layout:fixed}th{text-align:left;font-size:11px;color:#8a978e;font-weight:500;border-bottom:1px solid #e3e9e4;padding:11px 9px}td{padding:12px 9px;border-bottom:1px solid #f0f3f0;font-size:12px;color:#79897e}.check-cell{width:30px;padding-left:0}.size-cell{width:82px;text-align:right;font-variant-numeric:tabular-nums}.date-cell{width:145px;text-align:right}tr:hover td{background:#f7faf7}tr.active td{background:#edf5ee}td .entry{border:0;background:transparent;padding:0;display:flex;align-items:center;gap:10px;max-width:100%;color:#354e3e;font-size:13px;text-align:left}td .entry span:last-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.file-icon{display:inline-block;width:13px;height:16px;border:1px solid #a8b8ad;background:#f5f8f5;border-radius:2px;flex-shrink:0}.empty{text-align:center;padding:65px 20px;color:#8b9b8f;line-height:1.9}.list-footer{border-top:1px solid #e9eee9;display:flex;align-items:center;justify-content:space-between;color:#92a095;font-size:11px;padding:13px 0;margin-top:auto;min-height:48px}.preview-panel{border-left:1px solid #dbe3de;display:flex;flex-direction:column;padding:29px 20px 20px;background:#f8faf7;min-width:0}.preview-heading{display:flex;justify-content:space-between;align-items:center}.preview-heading h2{font-size:13px;color:#61776a}.preview-body{padding-top:25px;flex:1;min-width:0}.preview-empty{text-align:center;color:#859889;padding:80px 0;line-height:2}.preview-empty small{color:#a0aca2;font-size:11px}.preview-body h3{font-size:15px;line-height:1.7;overflow-wrap:anywhere;font-weight:500;margin:0 0 18px}.preview-body dl{font-size:11px;display:grid;grid-template-columns:60px minmax(0,1fr);gap:10px;margin:20px 0;color:#91a094}.preview-body dd{margin:0;color:#607968;overflow-wrap:anywhere}.preview-body pre{font:11px/1.8 ui-monospace,SFMono-Regular,monospace;white-space:pre-wrap;overflow-wrap:anywhere;max-height:50vh;overflow:auto;background:#edf2ec;padding:12px;border-radius:5px;color:#48654e}.preview-body img{max-width:100%;max-height:40vh;object-fit:contain;border-radius:4px}.preview-body p{font-size:12px;line-height:1.8;color:#8c9a8f}.preview-actions{display:flex;gap:8px;padding-top:20px}.preview-actions button{flex:1;font-size:12px}.transfer{border-top:1px solid #dce8df;padding:14px 0;font-size:12px}.transfer>div{display:flex;align-items:center;justify-content:space-between}.transfer progress{width:100%;height:5px;accent-color:#227b54;margin:10px 0}.transfer span{font-size:11px;color:#7b9082}dialog{border:1px solid #d4ded6;border-radius:10px;padding:26px;max-width:min(480px,90vw);color:#344c3c;box-shadow:0 20px 80px #182c2530}dialog::backdrop{background:#1c322747}dialog h2{font-size:19px;margin-bottom:15px}dialog p{font-size:13px;line-height:1.8;color:#879489}dialog ul{max-height:180px;overflow:auto;padding-left:20px;font-size:12px;line-height:1.9;overflow-wrap:anywhere}.dialog-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:24px}[hidden]{display:none!important}@media(min-width:1500px){.workspace{grid-template-columns:235px minmax(500px,1fr) 370px}.files{padding-left:38px;padding-right:38px}}@media(max-width:1100px){.workspace{grid-template-columns:160px minmax(360px,1fr) 260px}.files{padding:24px 18px 0}.authorization{flex-wrap:wrap;gap:10px}.grant-field{flex-basis:100%}.session-controls{margin-left:auto}.date-cell{width:110px}.toolbar{flex-wrap:wrap}.toolbar input[type=search]{flex-basis:100%}}@media(max-width:820px){.workspace{grid-template-columns:1fr}.sidebar{display:none}.preview-panel{border-left:0;border-top:1px solid #dbe3de}.preview-empty{padding:20px}.files{min-height:60vh}.authorization{padding:14px 18px}.grant-field{gap:7px;flex-wrap:wrap}.grant-field select{flex:1;min-width:180px}.session-controls{width:100%;justify-content:space-between;gap:8px}.expiry{gap:6px}.topbar{padding:0 18px}.topnote{display:none}.date-cell{width:110px}.preview-body pre{max-height:280px}.preview-actions{max-width:360px}}
|
|
25
|
+
/* Independent scrolling keeps file actions within the current viewport. */
|
|
26
|
+
body{height:100vh;height:100dvh;display:flex;flex-direction:column;overflow:hidden}
|
|
27
|
+
.topbar,.authorization,.notice{flex-shrink:0}.notice{max-height:20vh;overflow:auto}
|
|
28
|
+
.workspace{flex:1;min-height:0;overflow:hidden}.sidebar{min-height:0;overflow:auto}
|
|
29
|
+
.files{min-height:0;overflow:hidden}.files>:not(.table-wrap){flex-shrink:0}
|
|
30
|
+
.table-wrap{flex:1;min-height:0;overflow:auto;overscroll-behavior:contain}
|
|
31
|
+
.table-wrap thead{position:sticky;top:0;background:#fff;z-index:1}
|
|
32
|
+
.list-footer{margin-top:0;flex-shrink:0}.preview-panel{min-height:0;overflow:hidden}
|
|
33
|
+
.preview-heading,.preview-actions{flex-shrink:0}.preview-body{min-height:0;overflow:auto;overscroll-behavior:contain;padding-right:6px}
|
|
34
|
+
.preview-actions{border-top:1px solid #dbe3de;margin-top:16px;padding-top:16px;background:#f8faf7}
|
|
35
|
+
.preview-body h4{font-size:12px;font-weight:600;margin:24px 0 12px;color:#496553}
|
|
36
|
+
.preview-body dl{grid-template-columns:78px minmax(0,1fr)}.preview-body dt{overflow-wrap:anywhere}
|
|
37
|
+
.preview-body .metadata-list{grid-template-columns:minmax(0,1fr);gap:5px}.metadata-list dd{padding-bottom:9px}
|
|
38
|
+
.copy-field{display:flex;align-items:flex-start;gap:6px;min-width:0}.copy-field>span{min-width:0;overflow-wrap:anywhere}
|
|
39
|
+
.copy-button{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;padding:2px;width:24px;height:24px;margin-top:-3px}
|
|
40
|
+
.copy-button svg{width:15px;height:15px}.copy-button.copied{color:#18764e;background:#e1f2e7}
|
|
41
|
+
.copy-field a{color:#197854;text-underline-offset:3px;line-height:1.7}.copy-manual{width:100%;font:inherit}
|
|
42
|
+
.clipboard-fallback{position:fixed;left:0;top:0;opacity:0;pointer-events:none}
|
|
43
|
+
.icon-button{color:#526f5e}.icon-button svg{display:block}.is-loading svg{animation:oss-refresh-spin .8s linear infinite}
|
|
44
|
+
@keyframes oss-refresh-spin{to{transform:rotate(360deg)}}
|
|
45
|
+
@media(prefers-reduced-motion:reduce){.is-loading svg{animation:none}.is-loading{opacity:.55}}
|
|
46
|
+
@media(max-width:820px){.workspace{display:flex;flex-direction:column;overflow:auto}.files{flex:none;height:58dvh;min-height:330px}.preview-panel{flex:none;height:45dvh;min-height:270px}.preview-body pre{max-height:none}}
|
|
47
|
+
|
|
48
|
+
.upload-actions{display:flex;gap:8px}.tasks-panel{position:fixed;right:20px;bottom:20px;top:155px;width:min(480px,calc(100vw - 32px));z-index:5;background:#fff;border:1px solid #cbd9cf;border-radius:10px;box-shadow:0 12px 50px #20372e30;display:flex;flex-direction:column;overflow:hidden}.tasks-heading{display:flex;align-items:center;justify-content:space-between;padding:15px 18px;border-bottom:1px solid #e4ebe5}.tasks-note{font-size:12px;line-height:1.8;color:#718577;padding:12px 18px}.tasks-list{overflow:auto;overscroll-behavior:contain;flex:1;min-height:0}.task-card{padding:15px 18px;border-top:1px solid #e8eee9}.task-card h3{font-size:13px;margin:0 0 7px;overflow-wrap:anywhere;font-weight:550}.task-meta,.task-path,.task-stats{font-size:11px;color:#738a7b;line-height:1.7;overflow-wrap:anywhere}.task-path{font-family:ui-monospace,SFMono-Regular,monospace}.task-card progress{width:100%;height:6px;accent-color:#227b54;margin:10px 0 6px}.task-actions{display:flex;gap:7px;margin-top:10px}.task-actions button{font-size:12px;padding:5px 10px}.task-error,#tasks-connection{font-size:12px;color:#9f4b40;line-height:1.7;overflow-wrap:anywhere}.task-error{margin-top:8px}#tasks-connection{padding:0 18px 10px}#tasks-toggle{font-size:12px;background:#2f4b3e;border-color:#607969;color:#edf5ef}#tasks-count{font-variant-numeric:tabular-nums;margin-left:5px}@media(max-width:820px){.tasks-panel{top:140px;right:16px;bottom:16px}.upload-actions{gap:5px}.upload-actions button{font-size:12px;padding:7px 9px}}
|
|
22
49
|
`;
|
|
23
50
|
export const OSS_WEB_JS = String.raw `(() => {
|
|
24
51
|
'use strict';
|
|
@@ -28,9 +55,31 @@ const supplied = fragment.get('token');
|
|
|
28
55
|
let token = supplied || sessionStorage.getItem('numa-oss-web-token');
|
|
29
56
|
if (supplied) { sessionStorage.setItem('numa-oss-web-token', supplied); history.replaceState(null, '', location.pathname); }
|
|
30
57
|
const state = { id: 0, epoch: 0, dir: '', root: '', rows: [], marker: '', selected: new Set(), file: null,
|
|
31
|
-
session: null, offset: 0, ready: false, loading: false, mutating: false, controller: null, previewController: null,
|
|
58
|
+
session: null, offset: 0, ready: false, loading: false, mutating: false, controller: null, previewController: null, renewalBusy: false, grantsLoading: false };
|
|
32
59
|
const node = (tag, text, className) => { const el = document.createElement(tag); if (text !== undefined) el.textContent = text; if (className) el.className = className; return el; };
|
|
33
60
|
function notice(text, success = false) { $('notice').textContent = text; $('notice').className = 'notice' + (success ? ' success' : ''); $('notice').hidden = !text; }
|
|
61
|
+
function setBusy(id, busy) {
|
|
62
|
+
const button = $(id); button.classList.toggle('is-loading', busy); button.setAttribute('aria-busy', String(busy));
|
|
63
|
+
}
|
|
64
|
+
function copyButton(value, label) {
|
|
65
|
+
const button = node('button', undefined, 'quiet copy-button'); button.title = '复制' + label; button.setAttribute('aria-label', button.title);
|
|
66
|
+
button.append($('copy-icon').content.cloneNode(true));
|
|
67
|
+
button.onclick = async () => {
|
|
68
|
+
try {
|
|
69
|
+
if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(value);
|
|
70
|
+
else {
|
|
71
|
+
const field = node('textarea', undefined, 'clipboard-fallback'); field.value = value; document.body.append(field); field.select();
|
|
72
|
+
try { if (!document.execCommand('copy')) throw new Error('copy unavailable'); } finally { field.remove(); button.focus(); }
|
|
73
|
+
}
|
|
74
|
+
button.classList.add('copied'); button.title = '已复制'; notice(label + '已复制。', true);
|
|
75
|
+
setTimeout(() => { button.classList.remove('copied'); button.title = '复制' + label; }, 1600);
|
|
76
|
+
} catch {
|
|
77
|
+
const field = node('input', undefined, 'copy-manual'); field.value = value; field.readOnly = true;
|
|
78
|
+
button.parentElement.append(field); field.select(); notice('浏览器未允许复制,请复制已选中的内容。');
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
return button;
|
|
82
|
+
}
|
|
34
83
|
function fail(error) { if (error.name !== 'AbortError') notice(error.message || '操作失败,请重试。'); }
|
|
35
84
|
function url(path, params = {}, id = state.id) { return '/api/' + path + '?' + new URLSearchParams({ grantId: String(id), ...params }); }
|
|
36
85
|
async function api(path, params = {}, options = {}, id = state.id) {
|
|
@@ -47,11 +96,14 @@ function remaining() { return state.session ? Math.max(0, Math.ceil((Date.parse(
|
|
|
47
96
|
function controls() {
|
|
48
97
|
const active = state.ready && remaining() > 0 && !state.mutating;
|
|
49
98
|
$('upload').disabled = !active || !state.session?.write;
|
|
99
|
+
$('upload-folder').disabled = $('upload').disabled;
|
|
50
100
|
$('download').disabled = !active || !state.file || !state.session?.read;
|
|
51
101
|
$('delete').disabled = !active || !state.file || !state.session?.delete;
|
|
52
102
|
$('download-selected').disabled = !active || !state.selected.size || !state.session?.read;
|
|
53
|
-
$('delete-selected').disabled = !active || !state.selected.size || !state.session?.delete;
|
|
103
|
+
$('delete-selected').disabled = !active || !state.selected.size || !state.session?.delete || state.rows.some(row => row.directory && state.selected.has(row.key));
|
|
54
104
|
$('grant').disabled = state.mutating;
|
|
105
|
+
$('grants-refresh').disabled = state.mutating || state.grantsLoading;
|
|
106
|
+
$('refresh').disabled = state.mutating || state.loading || !state.session;
|
|
55
107
|
$('renew').disabled = !state.id || state.renewalBusy;
|
|
56
108
|
$('auto-renew').disabled = !state.session || state.renewalBusy;
|
|
57
109
|
$('selection-count').textContent = state.selected.size ? '已选 ' + state.selected.size + ' 项' : '';
|
|
@@ -83,7 +135,7 @@ function renderRows() {
|
|
|
83
135
|
for (const row of rows) {
|
|
84
136
|
const tr = node('tr'); if (state.file?.key === row.key) tr.className = 'active';
|
|
85
137
|
const checkCell = node('td', undefined, 'check-cell');
|
|
86
|
-
|
|
138
|
+
{ const check = document.createElement('input'); check.type = 'checkbox'; check.checked = state.selected.has(row.key); check.setAttribute('aria-label', '选择 ' + row.key); check.onchange = () => { if (check.checked) state.selected.add(row.key); else state.selected.delete(row.key); updateSelection(); controls(); }; checkCell.append(check); }
|
|
87
139
|
tr.append(checkCell);
|
|
88
140
|
const name = node('td'); const button = node('button', undefined, 'entry'); button.title = row.key;
|
|
89
141
|
button.append(node('span', undefined, row.directory ? 'folder-icon' : 'file-icon'), node('span', row.key.slice(state.dir.length) || row.key));
|
|
@@ -94,15 +146,17 @@ function renderRows() {
|
|
|
94
146
|
$('empty').hidden = rows.length > 0; $('empty').textContent = state.loading ? '正在读取文件…' : $('search').value ? '已加载列表中没有匹配项' : '此目录暂无文件';
|
|
95
147
|
$('list-summary').textContent = '已加载 ' + state.rows.length + ' 项' + ($('search').value ? ' · 显示 ' + rows.length + ' 项' : '') + (state.marker ? ' · 还有更多文件' : '');
|
|
96
148
|
$('more').hidden = !state.marker; $('more').disabled = state.loading;
|
|
97
|
-
|
|
149
|
+
updateSelection(); controls();
|
|
98
150
|
}
|
|
151
|
+
function updateSelection() { const rows = visible(), count = rows.filter(row => state.selected.has(row.key)).length; $('select-all').checked = rows.length > 0 && count === rows.length; $('select-all').indeterminate = count > 0 && count < rows.length; }
|
|
99
152
|
function clearPreview() { state.previewController?.abort(); state.file = null; $('preview').replaceChildren(node('div', '选择文件以查看内容与信息', 'preview-empty')); controls(); }
|
|
100
153
|
async function navigate(dir, append = false) {
|
|
154
|
+
if (state.mutating) return;
|
|
101
155
|
if (state.session && remaining() <= 0) { notice('STS 已到期,请点击立即续期后继续。'); return; }
|
|
102
156
|
const epoch = state.epoch, id = state.id; state.controller?.abort(); state.controller = new AbortController();
|
|
103
157
|
const controller = state.controller;
|
|
104
158
|
if (!append) { state.dir = dir; state.rows = []; state.selected.clear(); state.marker = ''; clearPreview(); }
|
|
105
|
-
state.loading = true; renderRows(); breadcrumbs();
|
|
159
|
+
state.loading = true; setBusy('refresh', true); renderRows(); breadcrumbs();
|
|
106
160
|
try {
|
|
107
161
|
const data = await api('list', { prefix: dir, ...(append ? { marker: state.marker } : {}) }, { signal: controller.signal }, id);
|
|
108
162
|
if (epoch !== state.epoch || controller !== state.controller) return;
|
|
@@ -112,7 +166,7 @@ async function navigate(dir, append = false) {
|
|
|
112
166
|
state.rows = [...merged.values()].sort((a,b) => Number(b.directory) - Number(a.directory) || a.key.localeCompare(b.key));
|
|
113
167
|
state.marker = data.listing.truncated ? data.listing.nextMarker || '' : '';
|
|
114
168
|
} catch (error) { if (epoch === state.epoch && controller === state.controller) { fail(error); if (error.code?.includes('session') || error.code?.includes('refresh')) state.ready = false; } }
|
|
115
|
-
finally { if (epoch === state.epoch && controller === state.controller) { state.loading = false; renderRows(); breadcrumbs(); } }
|
|
169
|
+
finally { if (epoch === state.epoch && controller === state.controller) { state.loading = false; setBusy('refresh', false); renderRows(); breadcrumbs(); } }
|
|
116
170
|
}
|
|
117
171
|
async function preview(row) {
|
|
118
172
|
clearPreview(); state.file = row; renderRows();
|
|
@@ -123,21 +177,39 @@ async function preview(row) {
|
|
|
123
177
|
if (epoch !== state.epoch || controller !== state.previewController) return;
|
|
124
178
|
const box = $('preview'); box.replaceChildren(node('h3', row.key.split('/').pop()));
|
|
125
179
|
if (data.kind === 'text') box.append(node('pre', data.text));
|
|
126
|
-
else if (data.kind === 'image') { const img = document.createElement('img'); img.src = data.data; img.alt = row.key; box.append(img); }
|
|
180
|
+
else if (data.kind === 'image') { const img = document.createElement('img'); img.src = data.data; img.alt = row.key; img.onerror = () => img.replaceWith(node('p', '图片无法预览,请下载后查看。')); box.append(img); }
|
|
127
181
|
else box.append(node('p', '此文件暂不支持在线预览,请下载后查看。支持文本与不超过 1 MiB 的常见图片。'));
|
|
128
182
|
if (data.truncated && data.kind === 'text') box.append(node('p', '仅显示前 256 KiB;下载可查看完整内容。'));
|
|
129
|
-
const dl = node('dl');
|
|
130
|
-
for (const [label, value] of [['大小', bytes(
|
|
131
|
-
|
|
183
|
+
const meta = data.metadata, dl = node('dl');
|
|
184
|
+
for (const [label, value] of [['大小', bytes(meta.contentLength)], ['类型', meta.contentType || '—'], ['修改时间', date(meta.lastModified)]]) dl.append(node('dt', label), node('dd', value));
|
|
185
|
+
const path = node('dd', undefined, 'copy-field'); path.append(copyButton(meta.key, '对象路径'), node('span', meta.key));
|
|
186
|
+
dl.append(node('dt', '对象路径'), path);
|
|
187
|
+
if (data.publicUrl) {
|
|
188
|
+
const address = node('dd', undefined, 'copy-field'), link = node('a', '打开链接');
|
|
189
|
+
link.href = data.publicUrl; link.target = '_blank'; link.rel = 'noopener noreferrer'; link.title = '打开文件公网地址';
|
|
190
|
+
address.append(copyButton(data.publicUrl, '公网地址'), link); dl.append(node('dt', '公网地址'), address);
|
|
191
|
+
}
|
|
192
|
+
box.append(dl, node('p', '公网地址不含临时签名;私有对象需授权访问。'));
|
|
193
|
+
const standard = node('dl', undefined, 'metadata-list');
|
|
194
|
+
for (const [label, key] of [['ETag','etag'],['存储类型','storageClass'],['对象类型','objectType'],['版本 ID','versionId'],['CRC64','crc64'],['Cache-Control','cacheControl'],['Content-Disposition','contentDisposition'],['Content-Encoding','contentEncoding'],['Expires','expires'],['服务端加密','serverSideEncryption']]) {
|
|
195
|
+
if (meta[key] !== undefined) standard.append(node('dt', label), node('dd', String(meta[key])));
|
|
196
|
+
}
|
|
197
|
+
box.append(node('h4', 'OSS 系统元数据'), standard.children.length ? standard : node('p', '暂无其他系统元数据'));
|
|
198
|
+
const custom = node('dl', undefined, 'metadata-list');
|
|
199
|
+
for (const [key, value] of Object.entries(meta.metadata || {}).sort(([a], [b]) => a.localeCompare(b))) custom.append(node('dt', key.startsWith('x-oss-meta-') ? key : 'x-oss-meta-' + key), node('dd', value));
|
|
200
|
+
box.append(node('h4', 'OSS 自定义元数据'), custom.children.length ? custom : node('p', '暂无自定义元数据'));
|
|
132
201
|
} catch (error) { if (epoch === state.epoch && controller === state.previewController && error.name !== 'AbortError') { $('preview').replaceChildren(node('h3', row.key.split('/').pop()), node('p', error.message)); } }
|
|
133
202
|
}
|
|
134
203
|
async function selectGrant(id) {
|
|
204
|
+
state.loading = false; setBusy('refresh', false);
|
|
135
205
|
state.epoch++; const epoch = state.epoch; state.id = Number(id); state.ready = false; state.session = null; state.rows = []; state.selected.clear(); state.marker = ''; state.dir = ''; state.root = '';
|
|
136
206
|
state.controller?.abort(); $('search').value = ''; clearPreview(); notice(''); tick(); renderRows(); $('scope').textContent = '正在连接…'; $('bucket').textContent = '正在连接…';
|
|
137
207
|
try { const data = await api('session', {}, {}, state.id); if (epoch !== state.epoch) return; setSession(data.session); await navigate(state.root); }
|
|
138
208
|
catch (error) { if (epoch === state.epoch) { fail(error); $('empty').textContent = '连接失败,请检查授权并重试。'; } }
|
|
139
209
|
}
|
|
140
210
|
async function refreshGrants(initial = false) {
|
|
211
|
+
if (state.grantsLoading) return;
|
|
212
|
+
state.grantsLoading = true; setBusy('grants-refresh', true); controls();
|
|
141
213
|
try {
|
|
142
214
|
const data = await api('grants'); const select = $('grant'); const previous = state.id;
|
|
143
215
|
select.replaceChildren(); for (const grant of data.grants) { const option = node('option', (grant.policyCode || 'Grant') + ' · ' + grant.bucketName + '/' + grant.objectPrefix + ' (#' + grant.grantId + ')'); option.value = String(grant.grantId); select.append(option); }
|
|
@@ -145,6 +217,7 @@ async function refreshGrants(initial = false) {
|
|
|
145
217
|
const id = data.grants.some(g => g.grantId === previous) ? previous : data.grants.some(g => g.grantId === data.selectedGrantId) ? data.selectedGrantId : data.grants[0].grantId;
|
|
146
218
|
select.value = String(id); if (initial || id !== previous) await selectGrant(id);
|
|
147
219
|
} catch (error) { fail(error); }
|
|
220
|
+
finally { state.grantsLoading = false; setBusy('grants-refresh', false); controls(); }
|
|
148
221
|
}
|
|
149
222
|
async function renew(manual = false) {
|
|
150
223
|
if (!state.id || state.renewalBusy) return;
|
|
@@ -154,16 +227,77 @@ async function renew(manual = false) {
|
|
|
154
227
|
catch (error) { if (epoch === state.epoch) { state.ready = false; fail(error); } }
|
|
155
228
|
finally { state.renewalBusy = false; $('renew').textContent = '立即续期'; tick(); }
|
|
156
229
|
}
|
|
230
|
+
async function chooseTransfer(path, body) {
|
|
231
|
+
if (state.mutating || !state.ready || remaining() <= 0) return;
|
|
232
|
+
const id = state.id; state.mutating = true; controls();
|
|
233
|
+
const controller = new AbortController(), timeout = setTimeout(() => controller.abort(), 5 * 60 * 1000);
|
|
234
|
+
notice('请在系统对话框中选择' + (path === 'transfers/download' ? '保存目录' : '上传内容') + '…');
|
|
235
|
+
try {
|
|
236
|
+
const data = await api(path, {}, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal: controller.signal }, id);
|
|
237
|
+
if (data.cancelled) { notice('已取消选择。'); return; }
|
|
238
|
+
showTasks(true); notice('已加入本机传输队列。可在传输任务中查看进度。', true); await pollTransfers();
|
|
239
|
+
} catch (error) { if (error.name === 'AbortError') notice('选择或任务准备超时。请先查看传输任务,确认是否已加入队列。'); else fail(error); }
|
|
240
|
+
finally { clearTimeout(timeout); state.mutating = false; controls(); }
|
|
241
|
+
}
|
|
157
242
|
async function download(keys) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
243
|
+
if (!state.session?.read || !keys.length) return;
|
|
244
|
+
const entries = keys.map(key => ({ key, directory: !!state.rows.find(row => row.key === key)?.directory }));
|
|
245
|
+
await chooseTransfer('transfers/download', { entries });
|
|
246
|
+
}
|
|
247
|
+
const taskCards = new Map(), taskStatuses = new Map(), taskBusy = new Set();
|
|
248
|
+
let pollingTransfers = false, pendingUploadNotice = false;
|
|
249
|
+
const taskLabels = { queued:'排队中', running:'传输中', pausing:'正在暂停', paused:'已暂停', cancelling:'正在取消', cancelled:'已取消', completed:'已完成', failed:'失败' };
|
|
250
|
+
function showTasks(show) { $('tasks-panel').hidden = !show; $('tasks-toggle').setAttribute('aria-expanded', String(show)); }
|
|
251
|
+
function setText(element, value) { if (element.textContent !== value) element.textContent = value; }
|
|
252
|
+
async function taskAction(id, action) {
|
|
253
|
+
if (taskBusy.has(id)) return;
|
|
254
|
+
taskBusy.add(id); const card = taskCards.get(id); if (card) for (const button of card.actions.values()) button.disabled = true;
|
|
255
|
+
try { await api('transfers/' + encodeURIComponent(id) + '/' + action, {}, { method:'POST' }); }
|
|
256
|
+
catch (error) { fail(error); }
|
|
257
|
+
finally { taskBusy.delete(id); await pollTransfers(); }
|
|
258
|
+
}
|
|
259
|
+
function renderTasks(jobs) {
|
|
260
|
+
const present = new Set(jobs.map(job => job.id));
|
|
261
|
+
for (const [id, card] of taskCards) if (!present.has(id)) { card.root.remove(); taskCards.delete(id); taskStatuses.delete(id); }
|
|
262
|
+
for (const job of jobs) {
|
|
263
|
+
let card = taskCards.get(job.id);
|
|
264
|
+
if (!card) {
|
|
265
|
+
const root = node('article', undefined, 'task-card'), title = node('h3'), meta = node('p', undefined, 'task-meta'), path = node('p', undefined, 'task-path'), progress = node('progress'), stats = node('p', undefined, 'task-stats'), error = node('p', undefined, 'task-error'), buttons = node('div', undefined, 'task-actions');
|
|
266
|
+
progress.max = 100; progress.setAttribute('aria-label', '文件传输进度');
|
|
267
|
+
const actions = new Map();
|
|
268
|
+
for (const [action, label] of [['pause','暂停'],['resume','恢复'],['cancel','取消']]) { const button = node('button', label); button.onclick = () => taskAction(job.id, action); actions.set(action, button); buttons.append(button); }
|
|
269
|
+
root.append(title, meta, path, progress, stats, error, buttons); $('tasks-list').append(root);
|
|
270
|
+
card = { root, title, meta, path, progress, stats, error, actions }; taskCards.set(job.id, card);
|
|
271
|
+
}
|
|
272
|
+
const prior = taskStatuses.get(job.id);
|
|
273
|
+
if (prior && prior !== 'completed' && job.status === 'completed' && job.direction === 'upload' && Number(job.grantId) === state.id) pendingUploadNotice = true;
|
|
274
|
+
taskStatuses.set(job.id, job.status);
|
|
275
|
+
setText(card.title, job.key); setText(card.meta, (job.direction === 'upload' ? '上传' : '下载') + ' · Grant #' + job.grantId + ' · ' + (taskLabels[job.status] || '状态未知'));
|
|
276
|
+
setText(card.path, job.localPath); card.path.title = job.localPath;
|
|
277
|
+
card.progress.value = job.size > 0 ? Math.min(100, job.transferred / job.size * 100) : job.status === 'completed' ? 100 : 0;
|
|
278
|
+
card.progress.setAttribute('aria-valuetext', bytes(job.transferred) + ' / ' + bytes(job.size));
|
|
279
|
+
setText(card.stats, bytes(job.transferred) + ' / ' + bytes(job.size) + (job.status === 'running' ? ' · ' + bytes(Math.max(0, Number(job.bytesPerSecond) || 0)) + '/s' : ''));
|
|
280
|
+
setText(card.error, job.error || ''); card.error.hidden = !job.error;
|
|
281
|
+
const available = { pause: ['queued','running'].includes(job.status), resume: ['paused','failed'].includes(job.status), cancel: ['queued','running','paused','failed'].includes(job.status) };
|
|
282
|
+
for (const [action, button] of card.actions) { button.hidden = !available[action]; button.disabled = taskBusy.has(job.id); }
|
|
162
283
|
}
|
|
284
|
+
$('tasks-empty').hidden = jobs.length > 0;
|
|
285
|
+
setText($('tasks-count'), String(jobs.length));
|
|
286
|
+
$('tasks-toggle').title = jobs.filter(job => ['queued','running','pausing','cancelling'].includes(job.status)).length + ' 个进行中的任务';
|
|
287
|
+
if (pendingUploadNotice && !state.mutating && !state.loading) { pendingUploadNotice = false; notice('当前授权有上传任务已完成。点击刷新文件列表查看新文件;当前预览已保留。', true); }
|
|
288
|
+
}
|
|
289
|
+
async function pollTransfers() {
|
|
290
|
+
if (!token || pollingTransfers) return;
|
|
291
|
+
pollingTransfers = true;
|
|
292
|
+
try { const data = await api('transfers'); renderTasks(data.jobs || []); $('tasks-connection').hidden = true; }
|
|
293
|
+
catch (error) { setText($('tasks-connection'), error.message || '暂时无法读取任务状态。'); $('tasks-connection').hidden = false; }
|
|
294
|
+
finally { pollingTransfers = false; }
|
|
163
295
|
}
|
|
164
296
|
let deletion = null;
|
|
165
297
|
function askDelete(keys) {
|
|
166
|
-
if (!keys.length
|
|
298
|
+
if (!keys.length || state.mutating || !state.session?.delete || remaining() <= 0) return;
|
|
299
|
+
if (keys.some(key => state.rows.find(row => row.key === key)?.directory)) { notice('目录仅支持下载。删除前请取消选择目录。'); return; }
|
|
300
|
+
deletion = { keys, id: state.id, dir: state.dir, epoch: state.epoch };
|
|
167
301
|
$('delete-list').replaceChildren(...keys.map(key => node('li', key)));
|
|
168
302
|
$('delete-target').textContent = 'Grant #' + state.id + ' · ' + state.session.bucket;
|
|
169
303
|
$('delete-dialog').showModal();
|
|
@@ -174,32 +308,6 @@ async function deleteFiles() {
|
|
|
174
308
|
catch (error) { notice('已删除 ' + done + ' 个文件,后续操作已停止。' + error.message); }
|
|
175
309
|
finally { state.mutating = false; controls(); if (state.epoch === target.epoch) await navigate(target.dir); }
|
|
176
310
|
}
|
|
177
|
-
function sendFile(file, key, id, index, total) {
|
|
178
|
-
return new Promise((resolve, reject) => {
|
|
179
|
-
const xhr = new XMLHttpRequest(); state.xhr = xhr; xhr.open('PUT', url('object', { key }, id)); xhr.setRequestHeader('Authorization', 'Bearer ' + token); xhr.setRequestHeader('Content-Type', 'application/octet-stream');
|
|
180
|
-
xhr.upload.onprogress = event => { if (event.lengthComputable) $('progress').value = event.loaded / event.total * 100; $('transfer-detail').textContent = (index + 1) + '/' + total + ' · ' + bytes(event.loaded) + ' / ' + bytes(file.size); };
|
|
181
|
-
xhr.upload.onload = () => { $('transfer-detail').textContent = '上传数据已发送,正在等待 OSS 确认…'; };
|
|
182
|
-
xhr.onload = () => { let data; try { data = JSON.parse(xhr.responseText); } catch {} if (xhr.status >= 200 && xhr.status < 300 && data?.ok) resolve(); else reject(new Error(data?.error?.message || '上传失败,请刷新目录检查结果。')); };
|
|
183
|
-
xhr.onerror = () => reject(new Error('上传连接中断,请刷新目录检查结果。'));
|
|
184
|
-
xhr.onabort = () => reject(new Error('上传已取消;若请求已提交,请刷新目录核对远程结果。'));
|
|
185
|
-
xhr.send(file);
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
async function upload(files) {
|
|
189
|
-
if (!files.length) return;
|
|
190
|
-
const id = state.id, dir = state.dir, epoch = state.epoch; state.mutating = true; state.cancelUpload = false; controls(); $('transfer').hidden = false; let done = 0;
|
|
191
|
-
try {
|
|
192
|
-
for (let i = 0; i < files.length; i++) {
|
|
193
|
-
if (state.cancelUpload) break;
|
|
194
|
-
const file = files[i]; if (file.size > 5 * 1024 ** 3) throw new Error('单文件最大支持 5 GiB。');
|
|
195
|
-
if (!file.name.trim() || /[\\/]/.test(file.name)) throw new Error('文件名称无效。');
|
|
196
|
-
$('transfer-title').textContent = '上传 ' + file.name; $('progress').value = 0;
|
|
197
|
-
await sendFile(file, dir + file.name, id, i, files.length); done++;
|
|
198
|
-
}
|
|
199
|
-
notice('已上传 ' + done + ' 个文件。', true);
|
|
200
|
-
} catch (error) { notice('已上传 ' + done + ' 个文件。' + error.message); }
|
|
201
|
-
finally { state.xhr = null; state.mutating = false; $('transfer').hidden = true; $('file-input').value = ''; controls(); if (state.epoch === epoch) await navigate(dir); }
|
|
202
|
-
}
|
|
203
311
|
$('grant').onchange = () => selectGrant($('grant').value);
|
|
204
312
|
$('grants-refresh').onclick = () => { if (!state.mutating) refreshGrants(); };
|
|
205
313
|
$('renew').onclick = () => renew(true);
|
|
@@ -214,7 +322,7 @@ $('up').onclick = () => { const parts = state.dir.slice(state.root.length).split
|
|
|
214
322
|
$('refresh').onclick = () => { notice(''); navigate(state.dir); };
|
|
215
323
|
$('search').oninput = renderRows;
|
|
216
324
|
$('more').onclick = () => navigate(state.dir, true);
|
|
217
|
-
$('select-all').onchange = () => { for (const row of visible()
|
|
325
|
+
$('select-all').onchange = () => { for (const row of visible()) { if ($('select-all').checked) state.selected.add(row.key); else state.selected.delete(row.key); } renderRows(); };
|
|
218
326
|
$('preview-close').onclick = () => { clearPreview(); renderRows(); };
|
|
219
327
|
$('download').onclick = () => { if (state.file) download([state.file.key]); };
|
|
220
328
|
$('download-selected').onclick = () => download([...state.selected]);
|
|
@@ -222,12 +330,14 @@ $('delete').onclick = () => { if (state.file) askDelete([state.file.key]); };
|
|
|
222
330
|
$('delete-selected').onclick = () => askDelete([...state.selected]);
|
|
223
331
|
$('delete-cancel').onclick = () => { deletion = null; $('delete-dialog').close(); };
|
|
224
332
|
$('delete-confirm').onclick = deleteFiles;
|
|
225
|
-
$('upload').onclick = () =>
|
|
226
|
-
$('
|
|
227
|
-
$('
|
|
333
|
+
$('upload').onclick = () => { if (state.session?.write) chooseTransfer('transfers/upload', { prefix: state.dir, folder: false }); };
|
|
334
|
+
$('upload-folder').onclick = () => { if (state.session?.write) chooseTransfer('transfers/upload', { prefix: state.dir, folder: true }); };
|
|
335
|
+
$('tasks-toggle').onclick = () => showTasks($('tasks-panel').hidden);
|
|
336
|
+
$('tasks-close').onclick = () => { showTasks(false); $('tasks-toggle').focus(); };
|
|
337
|
+
setInterval(pollTransfers, 1000);
|
|
228
338
|
setInterval(tick, 1000);
|
|
229
339
|
setInterval(() => { if (state.id && !state.loading) renew(false); }, 15000);
|
|
230
340
|
if (!token) { notice('请通过 numa oss web 启动,并使用 CLI 显示的完整链接打开页面。'); $('empty').textContent = '等待本机会话'; }
|
|
231
|
-
else refreshGrants(true);
|
|
341
|
+
else { refreshGrants(true); pollTransfers(); }
|
|
232
342
|
})();`;
|
|
233
343
|
//# sourceMappingURL=web-page.js.map
|
package/dist/oss/web-page.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-page.js","sourceRoot":"","sources":["../../src/oss/web-page.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,GAAG,CAAA
|
|
1
|
+
{"version":3,"file":"web-page.js","sourceRoot":"","sources":["../../src/oss/web-page.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEjE,iGAAiG;AACjG,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,GAAG,CAAA;;;;4PAIsN,gBAAgB;;;;;mOAKzC,gBAAgB;;;;;;;;2BAQxN,aAAa;eACzB,CAAC;AAEhB,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BpC,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAoS9B,CAAC"}
|
package/dist/oss/web.d.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { OssClient } from "./client.js";
|
|
2
2
|
import { OssTransferClient } from "./transfers.js";
|
|
3
3
|
import type { AccessGrant } from "./schemas.js";
|
|
4
|
+
import { type LocalPicker } from "./local-picker.js";
|
|
5
|
+
import type { TransferWorker } from "./transfer-types.js";
|
|
4
6
|
export interface OssWebOptions {
|
|
5
7
|
grantId: number;
|
|
6
8
|
port?: number;
|
|
7
9
|
openBrowser?: boolean;
|
|
8
10
|
client?: Pick<OssClient, "listGrants" | "issueSession">;
|
|
9
11
|
transfer?: OssTransferClient;
|
|
12
|
+
picker?: LocalPicker;
|
|
13
|
+
taskDirectory?: string;
|
|
14
|
+
taskNamespace?: string;
|
|
15
|
+
taskWorker?: TransferWorker;
|
|
10
16
|
now?: () => number;
|
|
11
17
|
}
|
|
12
18
|
export interface OssWebHandle {
|