@numa-tech/numa 1.20.4 → 1.20.6
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 +20 -1
- package/dist/command-catalog.js +9 -1
- package/dist/command-catalog.js.map +1 -1
- package/dist/mcp/catalog.js +3 -0
- package/dist/mcp/catalog.js.map +1 -1
- package/dist/oss/commands.js +20 -0
- package/dist/oss/commands.js.map +1 -1
- package/dist/oss/download-links.d.ts +28 -0
- package/dist/oss/download-links.js +124 -0
- package/dist/oss/download-links.js.map +1 -0
- package/dist/oss/errors.js +1 -1
- package/dist/oss/errors.js.map +1 -1
- package/dist/oss/schemas.d.ts +15 -0
- package/dist/oss/schemas.js +2 -0
- package/dist/oss/schemas.js.map +1 -1
- package/dist/oss/tools.js +18 -0
- package/dist/oss/tools.js.map +1 -1
- package/dist/oss/transfers.d.ts +2 -0
- package/dist/oss/transfers.js +27 -0
- package/dist/oss/transfers.js.map +1 -1
- package/dist/oss/web-page.js +133 -22
- package/dist/oss/web-page.js.map +1 -1
- package/dist/oss/web-session.d.ts +3 -2
- package/dist/oss/web-session.js +13 -6
- package/dist/oss/web-session.js.map +1 -1
- package/dist/oss/web.js +46 -2
- package/dist/oss/web.js.map +1 -1
- package/dist/tasks/oss-query.d.ts +1 -1
- package/dist/tasks/oss-query.js +69 -18
- package/dist/tasks/oss-query.js.map +1 -1
- package/dist/tasks/runtime.d.ts +2 -2
- package/dist/tasks/runtime.js +1 -1
- package/dist/tasks/runtime.js.map +1 -1
- package/dist/tasks/tools.js +1 -1
- package/dist/tasks/tools.js.map +1 -1
- package/docs/agent-tasks.md +8 -2
- package/package.json +5 -5
- package/skills/numa-oss-access/SKILL.md +17 -5
- package/skills/numa-oss-access/evals/evals.json +36 -0
- package/skills/numa-oss-access/numa-skill.json +1 -1
- package/skills/numa-oss-access/references/download-links.md +42 -0
- package/skills/numa-oss-access/references/search-and-mcp.md +8 -3
package/dist/oss/web-page.js
CHANGED
|
@@ -10,11 +10,11 @@ export const OSS_WEB_HTML = String.raw `<!doctype html>
|
|
|
10
10
|
<div id="workspace" class="workspace"><aside class="sidebar"><div class="sidebar-heading"><h2 class="eyebrow">存储位置</h2><button id="sidebar-toggle" class="quiet panel-toggle" aria-label="折叠存储位置" title="折叠存储位置" aria-expanded="true" aria-controls="sidebar-content">‹</button></div><div id="sidebar-content" class="sidebar-content"><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></div></aside>
|
|
11
11
|
<main class="files"><div class="page-heading"><div><div class="eyebrow">OBJECT STORAGE</div><h1>文件</h1></div><div class="upload-actions"><button id="mkdir" disabled>创建目录…</button><button id="upload-folder" disabled>上传文件夹</button><button id="upload" class="primary" disabled>+ 上传文件</button></div></div>
|
|
12
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>
|
|
13
|
-
<div class="toolbar"><input id="search" type="search" placeholder="筛选已加载的文件与目录…" aria-label="筛选已加载的文件与目录"><span id="selection-count"></span><button id="statistics-selected" disabled>统计所选</button><button id="download-selected" disabled>下载所选</button><button id="delete-selected" class="danger" disabled>删除所选</button></div>
|
|
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>
|
|
13
|
+
<div class="toolbar"><input id="search" type="search" placeholder="筛选已加载的文件与目录…" aria-label="筛选已加载的文件与目录"><span id="selection-count"></span><button id="statistics-selected" disabled>统计所选</button><button id="download-selected" disabled>下载所选</button><button id="links-selected" disabled>临时下载链接</button><button id="delete-selected" class="danger" disabled>删除所选</button></div>
|
|
14
|
+
<div id="files-scroll" 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>
|
|
15
15
|
<footer class="list-footer"><span id="list-summary">—</span><button id="more" hidden>加载更多</button></footer>
|
|
16
16
|
|
|
17
|
-
</main><aside class="preview-panel"><div class="preview-heading"><h2>文件预览</h2><button id="preview-close" class="quiet panel-toggle" aria-label="折叠文件预览" title="折叠文件预览" aria-expanded="true" aria-controls="preview-content">›</button></div><div id="preview-content" class="preview-content"><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></div></aside></div>
|
|
17
|
+
</main><aside class="preview-panel"><div class="preview-heading"><h2>文件预览</h2><button id="preview-close" class="quiet panel-toggle" aria-label="折叠文件预览" title="折叠文件预览" aria-expanded="true" aria-controls="preview-content">›</button></div><div id="preview-content" class="preview-content"><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="file-link" disabled>临时链接</button><button id="delete" class="danger" disabled>删除文件</button></div></div></aside></div>
|
|
18
18
|
<dialog id="delete-dialog"><h2>删除所选项目?</h2><p id="delete-count"></p><p>文件夹包含全部子文件。确认后分页扫描并删除,不预先统计总数;已删除内容不能在此页面撤销。执行期间请勿向目标目录写入新文件。</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
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
20
|
<dialog id="statistics-dialog" aria-labelledby="statistics-title"><h2 id="statistics-title">所选项目统计</h2><p id="statistics-selection"></p><p id="statistics-status" role="status"></p><dl id="statistics-result"></dl><div id="statistics-details" hidden>
|
|
@@ -22,6 +22,7 @@ export const OSS_WEB_HTML = String.raw `<!doctype html>
|
|
|
22
22
|
<div id="statistics-types-group" hidden><table class="statistics-table"><caption>按文件类型</caption><thead><tr><th scope="col">扩展名</th><th scope="col">文件数量</th><th scope="col">总大小</th></tr></thead><tbody id="statistics-types"></tbody></table><p>按最后一个扩展名归类,不区分大小写;不读取文件内容识别类型。最多单列扫描中遇到的 100 种扩展名,其余合并为「其他类型」。</p></div>
|
|
23
23
|
<div id="statistics-times-group" hidden><table class="statistics-table"><caption>按最后修改时间</caption><thead><tr><th scope="col">时间范围</th><th scope="col">文件数量</th><th scope="col">总大小</th></tr></thead><tbody id="statistics-times"></tbody></table><p id="statistics-reference"></p><p>区间互不重叠,含下限、不含上限,一天按 24 小时计算。统计文件的最后修改时间,不等同于首次上传时间;统计期间内容变化可能影响结果。</p></div>
|
|
24
24
|
</div><div class="dialog-actions"><button id="statistics-stop" hidden>停止统计</button><button id="statistics-close">关闭</button></div></dialog>
|
|
25
|
+
<dialog id="links-dialog"><h2>临时下载链接</h2><p id="links-selection"></p><p>最多 100 个文件。文件夹生成逐文件链接清单;超过上限直接拒绝,不返回部分结果。</p><label class="check links-renew"><input id="links-renew" type="checkbox">生成前重新申请 STS</label><p id="links-session-hint"></p><label for="links-expires">链接有效期</label><select id="links-expires"><option value="300">5 分钟</option><option value="900">15 分钟</option><option value="1800">30 分钟</option><option value="3600">1 小时</option><option value="max">当前可用最长</option></select><p id="links-estimate" role="status"></p><p>持有链接者可在有效期内下载。实际时效受 STS 到期时间限制,续期不会延长已生成链接;有效期不保证所有文件能下载完成。</p><p id="links-status" role="status"></p><div id="links-result"></div><div class="dialog-actions"><button id="links-close">关闭</button><button id="links-generate" class="primary">生成链接</button></div></dialog>
|
|
25
26
|
<dialog id="stop-dialog"><h2>停止本机服务?</h2><p>上传下载任务将暂停并保存断点;删除、复制、移动等文件操作将停止,已完成部分保留且不会自动续做。随后关闭 CLI 服务。所有已打开的控制台都会断开连接。重新运行 numa oss web 后可继续任务。</p><div class="dialog-actions"><button id="stop-cancel">继续运行</button><button id="stop-confirm" class="destructive">停止服务</button></div></dialog>
|
|
26
27
|
<div id="permission-tip" class="permission-tip" role="tooltip" hidden></div>
|
|
27
28
|
<div id="file-menu" class="file-menu" role="menu" aria-label="文件操作" hidden></div>
|
|
@@ -33,6 +34,7 @@ export const OSS_WEB_CSS = String.raw `
|
|
|
33
34
|
.file-menu{position:fixed;z-index:20;min-width:175px;padding:5px;background:white;border:1px solid #d4ded6;border-radius:8px;box-shadow:0 8px 28px #20372e26}.file-menu button{display:block;width:100%;border:0;text-align:left;padding:9px 12px}.file-menu button:focus{background:#eff5f1}.file-menu button[aria-disabled=true]{color:#99a49e}.permission-tip{position:fixed;z-index:30;max-width:min(330px,calc(100vw - 16px));padding:10px 12px;background:#263c31;color:#fff;border-radius:6px;box-shadow:0 6px 24px #20372e26;font-size:12px;line-height:1.7;overflow-wrap:anywhere}
|
|
34
35
|
#statistics-result{display:grid;grid-template-columns:100px 1fr;gap:12px;margin:20px 0;font-variant-numeric:tabular-nums}#statistics-result dd{margin:0;font-weight:600}#statistics-status{margin-top:10px}
|
|
35
36
|
#statistics-dialog{width:min(640px,90vw);max-width:90vw;max-height:90vh;overflow:auto}#statistics-result dd{overflow-wrap:anywhere}.statistics-table{margin:22px 0 10px}.statistics-table caption{text-align:left;font-weight:600;margin-bottom:6px}.statistics-table th:first-child,.statistics-table td:first-child{width:44%;overflow-wrap:anywhere}.statistics-table th:not(:first-child),.statistics-table td:not(:first-child){text-align:right;font-variant-numeric:tabular-nums}.statistics-table td{color:#4a6554}#statistics-dialog .dialog-actions{position:sticky;bottom:-26px;background:#fff;padding:14px 0;margin-top:10px}
|
|
37
|
+
#links-dialog{width:min(680px,90vw);max-width:90vw;max-height:90vh;overflow:auto}#links-expires{margin-left:12px;max-width:100%}.links-renew{margin-top:12px;white-space:normal}#links-session-hint{margin:8px 0 12px}#links-result article{border-top:1px solid #dbe3de;padding:14px 0}#links-result h3{font-size:13px;overflow-wrap:anywhere;margin:0 0 8px}#links-result textarea{width:100%;font:11px/1.6 ui-monospace,monospace;resize:vertical;overflow-wrap:anywhere;min-height:72px}#links-result .link-copy{display:flex;align-items:center;gap:8px}#links-status{overflow-wrap:anywhere}#links-dialog .dialog-actions{position:sticky;bottom:-26px;background:white;padding:14px 0}
|
|
36
38
|
#operation-dialog{max-height:90vh;overflow:auto;width:min(520px,90vw)}
|
|
37
39
|
#destination-picker{margin-top:16px}#destination-hint{font-size:12px;margin-bottom:8px}.destination-tree{max-height:260px;overflow:auto;border:1px solid #dbe3de;border-radius:6px;padding:6px;background:#fafcfb}.destination-children{padding-left:18px}.destination-line{display:flex;align-items:center;gap:4px;min-height:34px;border-radius:4px}.destination-line button{border:0;background:transparent;padding:6px;color:#344b41}.destination-toggle{width:28px;flex-shrink:0}.destination-name{display:flex;align-items:center;gap:8px;flex:1;min-width:0;text-align:left;white-space:normal;overflow-wrap:anywhere}.destination-name .folder-icon{flex-shrink:0}.destination-item[aria-selected=true]>.destination-line{background:#dfede3}.destination-item:focus{outline:none}.destination-item:focus-visible>.destination-line{outline:2px solid #16805a;outline-offset:-2px}.destination-status{font-size:11px;color:#7b8d81;margin-left:32px}.destination-more{font-size:12px;margin:4px 0 4px 32px}.destination-editor{display:flex;flex-wrap:wrap;gap:6px;margin:6px 0 6px 32px}.destination-editor input{width:100%;min-width:0;padding:7px;border:1px solid #cbd9cf;border-radius:4px;background:white;color:#23322e}.destination-editor button{font-size:12px;padding:6px 9px}
|
|
38
40
|
#operation-dialog label{display:block;font-size:12px;margin:16px 0 8px}#operation-value{width:100%;border:1px solid #cbd9cf;padding:10px;border-radius:5px;color:#23322e}#operation-source{overflow-wrap:anywhere}#operation-help{margin-top:10px}#operation-error{color:#a7473d}
|
|
@@ -91,8 +93,8 @@ const fragment = new URLSearchParams(location.hash.slice(1));
|
|
|
91
93
|
const supplied = fragment.get('token');
|
|
92
94
|
let token = supplied || sessionStorage.getItem('numa-oss-web-token');
|
|
93
95
|
if (supplied) { sessionStorage.setItem('numa-oss-web-token', supplied); history.replaceState(null, '', location.pathname); }
|
|
94
|
-
const state = { id: 0, epoch: 0, dir: '', root: '', rows: [], marker: '', selected: new Set(), file: null,
|
|
95
|
-
stopping: false, stopped: false, session: null, offset: 0, ready: false, loading: false, mutating: false, controller: null, previewController: null, renewalBusy: false, grantsLoading: false };
|
|
96
|
+
const state = { id: 0, epoch: 0, dir: '', root: '', rows: [], marker: '', listPages: 1, selected: new Set(), file: null,
|
|
97
|
+
stopping: false, stopped: false, grants: [], session: null, offset: 0, ready: false, loading: false, mutating: false, controller: null, previewController: null, renewalBusy: false, grantsLoading: false };
|
|
96
98
|
const node = (tag, text, className) => { const el = document.createElement(tag); if (text !== undefined) el.textContent = text; if (className) el.className = className; return el; };
|
|
97
99
|
function notice(text, success = false) { $('notice').textContent = text; $('notice').className = 'notice' + (success ? ' success' : ''); $('notice').hidden = !text; }
|
|
98
100
|
function setBusy(id, busy) {
|
|
@@ -146,12 +148,14 @@ function controls() {
|
|
|
146
148
|
$('download').disabled = !active || !state.file || !state.session?.read;
|
|
147
149
|
$('delete').disabled = !active || !state.file || !state.session?.delete;
|
|
148
150
|
$('download-selected').disabled = !active || !state.selected.size || !state.session?.read;
|
|
151
|
+
$('links-selected').disabled = $('download-selected').disabled || state.rows.some(row=>state.selected.has(row.key)&&row.directory&&!state.session?.list);
|
|
152
|
+
$('file-link').disabled = $('download').disabled;
|
|
149
153
|
$('statistics-selected').disabled = !active || !state.selected.size || state.rows.some(row => state.selected.has(row.key) && !state.session?.[row.directory ? 'list' : 'read']);
|
|
150
154
|
$('delete-selected').disabled = !active || !state.selected.size || !state.session?.delete;
|
|
151
155
|
$('grant').disabled = state.mutating || state.stopping;
|
|
152
156
|
$('grants-refresh').disabled = state.mutating || state.grantsLoading || state.stopping;
|
|
153
157
|
$('refresh').disabled = state.mutating || state.loading || !state.session || state.stopping;
|
|
154
|
-
$('renew').disabled = !state.id || state.renewalBusy || state.stopping;
|
|
158
|
+
$('renew').disabled = !state.id || state.renewalBusy || state.mutating || state.stopping;
|
|
155
159
|
$('auto-renew').disabled = !state.session || state.renewalBusy || state.stopping;
|
|
156
160
|
$('service-stop').disabled = !token || state.stopping;
|
|
157
161
|
$('selection-count').textContent = state.selected.size ? '已选 ' + state.selected.size + ' 项' : '';
|
|
@@ -198,25 +202,45 @@ function renderRows() {
|
|
|
198
202
|
}
|
|
199
203
|
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; }
|
|
200
204
|
function clearPreview() { state.previewController?.abort(); state.file = null; $('preview').replaceChildren(node('div', '选择文件以查看内容与信息', 'preview-empty')); controls(); }
|
|
201
|
-
async function navigate(dir, append = false) {
|
|
205
|
+
async function navigate(dir, append = false, preserve = false) {
|
|
202
206
|
closeFileMenu();
|
|
203
207
|
if (state.mutating || state.stopping) return;
|
|
204
208
|
if (state.session && remaining() <= 0) { notice('STS 已到期,请点击立即续期后继续。'); return; }
|
|
205
209
|
const epoch = state.epoch, id = state.id; state.controller?.abort(); state.controller = new AbortController();
|
|
206
210
|
const controller = state.controller;
|
|
207
|
-
|
|
208
|
-
state.
|
|
211
|
+
const pageCount=preserve ? state.listPages : 1, scrollTop=$('files-scroll').scrollTop;
|
|
212
|
+
if (!append && !preserve) { state.dir = dir; state.rows = []; state.selected.clear(); state.marker = ''; clearPreview(); }
|
|
213
|
+
state.loading = true; setBusy('refresh', true); if(!preserve)renderRows(); breadcrumbs();
|
|
209
214
|
try {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
215
|
+
let marker=append ? state.marker : '', entries=[], pages=0;
|
|
216
|
+
for (;pages<pageCount;) {
|
|
217
|
+
const data = await api('list', { prefix: dir, ...(marker ? { marker } : {}) }, { signal: controller.signal }, id);
|
|
218
|
+
if (epoch !== state.epoch || controller !== state.controller) return;
|
|
219
|
+
setSession(data.session);
|
|
220
|
+
entries.push(...data.listing.directories.map(key => ({ key, directory: true })),...data.listing.objects.filter(r => r.key !== dir).map(r => ({ ...r, directory: r.key.endsWith('/') })));
|
|
221
|
+
const next=data.listing.truncated ? data.listing.nextMarker || '' : '';
|
|
222
|
+
if(next && next===marker)throw new Error('目录分页未前进,请手动刷新重试。');
|
|
223
|
+
marker=next;pages++;
|
|
224
|
+
if(!marker)break;
|
|
225
|
+
}
|
|
214
226
|
const merged = new Map((append ? state.rows : []).concat(entries).map(r => [r.key, r]));
|
|
215
227
|
state.rows = [...merged.values()].sort((a,b) => Number(b.directory) - Number(a.directory) || a.key.localeCompare(b.key));
|
|
216
|
-
state.marker =
|
|
228
|
+
state.marker = marker; state.listPages=append ? state.listPages+pages : pages;
|
|
229
|
+
if(preserve) {
|
|
230
|
+
state.selected=new Set([...state.selected].filter(key=>merged.has(key)));
|
|
231
|
+
if(state.file && !merged.has(state.file.key) && !marker)clearPreview();
|
|
232
|
+
}
|
|
217
233
|
} catch (error) { if (epoch === state.epoch && controller === state.controller) { fail(error); if (error.code?.includes('session') || error.code?.includes('refresh')) state.ready = false; } }
|
|
218
|
-
finally { if (epoch === state.epoch && controller === state.controller) { state.loading = false; setBusy('refresh', false); renderRows(); breadcrumbs(); } }
|
|
234
|
+
finally { if (epoch === state.epoch && controller === state.controller) { state.loading = false; setBusy('refresh', false); renderRows(); breadcrumbs(); if(preserve)$('files-scroll').scrollTop=scrollTop; } }
|
|
219
235
|
}
|
|
236
|
+
const dirtyListings = new Set();
|
|
237
|
+
function markListingChanged(grantId) { dirtyListings.add(Number(grantId)); }
|
|
238
|
+
async function refreshChangedListing() {
|
|
239
|
+
if(!dirtyListings.has(state.id) || !state.ready || !state.session?.list || state.loading || state.mutating || state.stopping || remaining()<=0)return;
|
|
240
|
+
dirtyListings.delete(state.id);
|
|
241
|
+
await navigate(state.dir,false,true);
|
|
242
|
+
}
|
|
243
|
+
setInterval(refreshChangedListing,1000);
|
|
220
244
|
async function preview(row) {
|
|
221
245
|
setPanelCollapsed('preview',false);
|
|
222
246
|
clearPreview(); state.file = row; renderRows();
|
|
@@ -251,6 +275,7 @@ async function preview(row) {
|
|
|
251
275
|
} catch (error) { if (epoch === state.epoch && controller === state.previewController && error.name !== 'AbortError') { $('preview').replaceChildren(node('h3', displayName(row.key.split('/').pop())), node('p', error.message)); } }
|
|
252
276
|
}
|
|
253
277
|
async function selectGrant(id) {
|
|
278
|
+
closeLinks();
|
|
254
279
|
state.loading = false; setBusy('refresh', false);
|
|
255
280
|
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 = '';
|
|
256
281
|
state.controller?.abort(); $('search').value = ''; clearPreview(); notice(''); tick(); renderRows(); $('scope').textContent = '正在连接…'; $('bucket').textContent = '正在连接…';
|
|
@@ -261,7 +286,7 @@ async function refreshGrants(initial = false) {
|
|
|
261
286
|
if (state.grantsLoading) return;
|
|
262
287
|
state.grantsLoading = true; setBusy('grants-refresh', true); controls();
|
|
263
288
|
try {
|
|
264
|
-
const data = await api('grants'); const select = $('grant'); const previous = state.id;
|
|
289
|
+
const data = await api('grants'); state.grants = data.grants; const select = $('grant'); const previous = state.id;
|
|
265
290
|
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); }
|
|
266
291
|
if (!data.grants.length) { state.ready = false; controls(); throw new Error('当前没有可用授权,请核对 Grant 配置。'); }
|
|
267
292
|
const id = data.grants.some(g => g.grantId === previous) ? previous : data.grants.some(g => g.grantId === data.selectedGrantId) ? data.selectedGrantId : data.grants[0].grantId;
|
|
@@ -270,7 +295,7 @@ async function refreshGrants(initial = false) {
|
|
|
270
295
|
finally { state.grantsLoading = false; setBusy('grants-refresh', false); controls(); }
|
|
271
296
|
}
|
|
272
297
|
async function renew(manual = false) {
|
|
273
|
-
if (!state.id || state.renewalBusy || state.stopping) return;
|
|
298
|
+
if (!state.id || state.renewalBusy || state.mutating || state.stopping) return;
|
|
274
299
|
const epoch = state.epoch, id = state.id; state.renewalBusy = true; controls();
|
|
275
300
|
if (manual) $('renew').textContent = '续期中…';
|
|
276
301
|
try { const data = await api(manual ? 'renew' : 'session', {}, manual ? { method: 'POST' } : {}, id); if (epoch === state.epoch) { setSession(data.session); if (manual) notice('STS 已续期,可继续使用。', true); } }
|
|
@@ -289,13 +314,95 @@ async function chooseTransfer(path, body) {
|
|
|
289
314
|
} catch (error) { if (error.name === 'AbortError') notice('选择或任务准备超时。请先查看传输任务,确认是否已加入队列。'); else fail(error); }
|
|
290
315
|
finally { clearTimeout(timeout); state.mutating = false; controls(); }
|
|
291
316
|
}
|
|
317
|
+
let linkSelection = null, linksController = null;
|
|
318
|
+
function linkTime(seconds) {
|
|
319
|
+
seconds=Math.max(0,Math.floor(seconds));
|
|
320
|
+
return Math.floor(seconds/60)+' 分 '+seconds%60+' 秒';
|
|
321
|
+
}
|
|
322
|
+
function linkCapacity() {
|
|
323
|
+
if ($('links-renew').checked) return Math.min(3600,state.grants.find(grant=>grant.grantId===state.id)?.maxSessionSeconds || 0);
|
|
324
|
+
return Math.min(3600,Math.max(0,Math.floor((Date.parse(state.session?.expiration)-Date.now()-state.offset)/1000)-5));
|
|
325
|
+
}
|
|
326
|
+
function updateLinkOptions() {
|
|
327
|
+
if (!linkSelection || state.mutating) return;
|
|
328
|
+
const renew=$('links-renew').checked, cap=linkCapacity(), select=$('links-expires');
|
|
329
|
+
const max=state.grants.find(grant=>grant.grantId===state.id)?.maxSessionSeconds || 0;
|
|
330
|
+
$('links-renew').disabled=state.renewalBusy || !max;
|
|
331
|
+
$('links-session-hint').textContent='当前 STS 剩余 '+linkTime(remaining())+'。'+(max ? '重新申请最多可获得 '+linkTime(max)+',仍受当前授权限制。' : '请刷新授权列表以读取可申请时长。');
|
|
332
|
+
for (const option of select.children) {
|
|
333
|
+
if (option.value==='max') {
|
|
334
|
+
option.textContent=renew ? '授权允许最长(约 '+linkTime(Math.max(0,cap-5))+')' : '当前可用最长(约 '+linkTime(cap)+')';
|
|
335
|
+
option.disabled=cap<60;
|
|
336
|
+
} else {
|
|
337
|
+
option.hidden=option.disabled=Number(option.value)>=cap;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (!select.value || select.value!=='max' && Number(select.value)>=cap) select.value='max';
|
|
341
|
+
const seconds=select.value==='max' ? cap : Number(select.value);
|
|
342
|
+
const usable=renew ? Math.min(seconds,cap-5) : seconds;
|
|
343
|
+
$('links-estimate').textContent=cap<60 ? '剩余时间不足 1 分钟,请勾选重新申请 STS。'
|
|
344
|
+
: (renew ? '重新申请成功后预计可用约 ' : '预计可用约 ')+linkTime(usable)+',预计到期 '+date(new Date(Date.now()+state.offset+usable*1000).toISOString())+'。检查文件会消耗时间,以生成结果为准。';
|
|
345
|
+
$('links-generate').disabled=cap<60 || state.renewalBusy || state.stopping;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function askLinks(entries) {
|
|
349
|
+
if (!state.ready || state.mutating || state.stopping || !entries.length || !state.session?.read) return;
|
|
350
|
+
if(entries.some(entry=>entry.directory&&!state.session?.list)) { notice('文件夹需要列举和下载权限。'); return; }
|
|
351
|
+
if(entries.length>100) { notice('所选内容超过 100 项,未生成链接。请缩小范围或使用批量下载。'); return; }
|
|
352
|
+
linkSelection={grantId:state.id,epoch:state.epoch,entries:entries.map(entry=>({key:entry.key,directory:entry.directory}))};
|
|
353
|
+
$('links-selection').textContent='Grant #'+state.id+' · 已选 '+entries.length+' 项';
|
|
354
|
+
$('links-renew').checked=false; $('links-expires').value='300'; $('links-status').textContent=''; $('links-result').replaceChildren();
|
|
355
|
+
$('links-expires').disabled=false; updateLinkOptions(); $('links-dialog').showModal();
|
|
356
|
+
}
|
|
357
|
+
function closeLinks() {
|
|
358
|
+
linksController?.abort(); linksController=null; linkSelection=null;
|
|
359
|
+
$('links-result').replaceChildren(); $('links-status').textContent=''; $('links-dialog').close();
|
|
360
|
+
}
|
|
361
|
+
async function generateLinks() {
|
|
362
|
+
const selected=linkSelection;
|
|
363
|
+
if(!selected || state.mutating || state.stopping || selected.grantId!==state.id || selected.epoch!==state.epoch) return;
|
|
364
|
+
updateLinkOptions();
|
|
365
|
+
if ($('links-generate').disabled) return;
|
|
366
|
+
const renewSts=$('links-renew').checked, expires=$('links-expires').value==='max' ? linkCapacity() : Number($('links-expires').value);
|
|
367
|
+
const controller=new AbortController(); linksController=controller; state.mutating=true; controls();
|
|
368
|
+
$('links-generate').disabled=true; $('links-expires').disabled=true; $('links-renew').disabled=true; $('links-result').replaceChildren();
|
|
369
|
+
$('links-status').textContent=(renewSts ? '正在重新申请 STS,随后检查下载权限…' : '正在列举并检查下载权限,完成后开始计算链接有效期…');
|
|
370
|
+
try {
|
|
371
|
+
const result=await api('download-links',{}, {method:'POST',signal:controller.signal,body:JSON.stringify({entries:selected.entries,expires,renewSts})},selected.grantId);
|
|
372
|
+
if(linksController!==controller || selected.epoch!==state.epoch) return;
|
|
373
|
+
if(result.session)setSession(result.session);
|
|
374
|
+
const data=result.data;
|
|
375
|
+
$('links-status').textContent=data.fileCount+' 个链接 · 最早到期 '+date(data.expiresAt)+'。过期后请重新生成。';
|
|
376
|
+
const all=node('div',undefined,'link-copy'); all.append(node('span','复制全部链接'),copyButton(data.links.map(link=>link.url).join('\n'),'全部下载链接')); $('links-result').append(all);
|
|
377
|
+
for(const link of data.links) {
|
|
378
|
+
const row=node('article'),heading=node('h3',displayName(link.key)),field=node('textarea');
|
|
379
|
+
field.value=link.url;field.readOnly=true;field.setAttribute('aria-label',link.key+' 的临时下载链接');
|
|
380
|
+
const copy=node('div',undefined,'link-copy');copy.append(node('span','到期 '+date(link.expiresAt)),copyButton(link.url,'下载链接'));
|
|
381
|
+
row.append(heading,field,copy);$('links-result').append(row);
|
|
382
|
+
}
|
|
383
|
+
} catch(error) {
|
|
384
|
+
if(linksController===controller && error.name!=='AbortError')$('links-status').textContent=error.message;
|
|
385
|
+
} finally {
|
|
386
|
+
if(linksController===controller)linksController=null;
|
|
387
|
+
state.mutating=false;controls();$('links-expires').disabled=false;updateLinkOptions();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
$('links-selected').onclick=()=>askLinks(state.rows.filter(row=>state.selected.has(row.key)));
|
|
391
|
+
$('file-link').onclick=()=>{if(state.file)askLinks([{key:state.file.key,directory:false}]);};
|
|
392
|
+
$('links-renew').onchange=updateLinkOptions;
|
|
393
|
+
$('links-expires').onchange=updateLinkOptions;
|
|
394
|
+
setInterval(updateLinkOptions,1000);
|
|
395
|
+
$('links-generate').onclick=generateLinks;
|
|
396
|
+
$('links-close').onclick=closeLinks;
|
|
397
|
+
$('links-dialog').oncancel=event=>{event.preventDefault();closeLinks();};
|
|
398
|
+
|
|
292
399
|
async function download(keys) {
|
|
293
400
|
if (!state.session?.read || !keys.length) return;
|
|
294
401
|
const entries = keys.map(key => ({ key, directory: !!state.rows.find(row => row.key === key)?.directory }));
|
|
295
402
|
await chooseTransfer('transfers/download', { entries });
|
|
296
403
|
}
|
|
297
404
|
const taskCards = new Map(), taskStatuses = new Map(), taskBusy = new Set();
|
|
298
|
-
let pollingTransfers = false,
|
|
405
|
+
let pollingTransfers = false, transfersLoaded = false;
|
|
299
406
|
const taskLabels = { queued:'排队中', running:'传输中', pausing:'正在暂停', paused:'已暂停', cancelling:'正在取消', cancelled:'已取消', completed:'已完成', failed:'失败' };
|
|
300
407
|
function showTasks(show) { $('tasks-panel').hidden = !show; $('tasks-toggle').setAttribute('aria-expanded', String(show)); }
|
|
301
408
|
function setText(element, value) { if (element.textContent !== value) element.textContent = value; }
|
|
@@ -320,7 +427,7 @@ function renderTasks(jobs) {
|
|
|
320
427
|
card = { root, title, meta, path, progress, stats, error, actions }; taskCards.set(job.id, card);
|
|
321
428
|
}
|
|
322
429
|
const prior = taskStatuses.get(job.id);
|
|
323
|
-
if (prior && prior !== 'completed' && job.status === 'completed' && job.direction === 'upload'
|
|
430
|
+
if ((prior || transfersLoaded) && prior !== 'completed' && job.status === 'completed' && job.direction === 'upload') markListingChanged(job.grantId);
|
|
324
431
|
taskStatuses.set(job.id, job.status);
|
|
325
432
|
setText(card.title, job.key); setText(card.meta, (job.direction === 'upload' ? '上传' : '下载') + ' · Grant #' + job.grantId + ' · ' + (taskLabels[job.status] || '状态未知'));
|
|
326
433
|
setText(card.path, job.localPath); card.path.title = job.localPath;
|
|
@@ -334,7 +441,7 @@ function renderTasks(jobs) {
|
|
|
334
441
|
$('tasks-empty').hidden = jobs.length > 0;
|
|
335
442
|
setText($('tasks-count'), String(jobs.length));
|
|
336
443
|
$('tasks-toggle').title = jobs.filter(job => ['queued','running','pausing','cancelling'].includes(job.status)).length + ' 个进行中的任务';
|
|
337
|
-
|
|
444
|
+
transfersLoaded = true;
|
|
338
445
|
}
|
|
339
446
|
async function pollTransfers() {
|
|
340
447
|
if (!token || pollingTransfers || state.stopping) return;
|
|
@@ -393,6 +500,10 @@ function openFileMenu(event, row, anchor) {
|
|
|
393
500
|
button.onclick = () => { if (missing.length) { showPermissionTip(button,missing); return; } closeFileMenu(); if (kind === 'delete') askDelete([row.key]); else askOperation(kind,row); }; menu.append(button);
|
|
394
501
|
}
|
|
395
502
|
const selected = state.selected.has(row.key) ? state.rows.filter(item => state.selected.has(item.key)) : [row];
|
|
503
|
+
const links = node('button', '临时下载链接…'); links.setAttribute('role','menuitem');
|
|
504
|
+
const linkMissing = ['read',...(selected.some(item=>item.directory)?['list']:[])].filter(permission=>!state.session[permission]).map(permission=>permissions[permission]);
|
|
505
|
+
links.setAttribute('aria-disabled',String(linkMissing.length>0)); links.onblur = hidePermissionTip;
|
|
506
|
+
links.onclick = () => { if(linkMissing.length){showPermissionTip(links,linkMissing);return;} closeFileMenu(); askLinks(selected); }; menu.append(links);
|
|
396
507
|
const statistics = node('button', selected.length > 1 ? '统计所选(' + selected.length + ' 项)…' : '统计…'); statistics.setAttribute('role','menuitem');
|
|
397
508
|
const missing = [...new Set(selected.map(item => item.directory ? 'list' : 'read'))].filter(permission => !state.session[permission]).map(permission => permissions[permission]);
|
|
398
509
|
statistics.setAttribute('aria-disabled',String(missing.length > 0)); statistics.onblur = hidePermissionTip;
|
|
@@ -521,7 +632,7 @@ async function createDestinationDirectory(tree, branch) {
|
|
|
521
632
|
for (;;) {
|
|
522
633
|
const result = await api('operations',{}, {signal:tree.controller.signal},tree.target.id);
|
|
523
634
|
const job = result.operations?.find(job => job.id === data.operation.id);
|
|
524
|
-
if (job?.status === 'completed') break;
|
|
635
|
+
if (job?.status === 'completed') { markListingChanged(tree.target.id); break; }
|
|
525
636
|
if (!job || ['failed','stopped'].includes(job.status)) throw new Error(job?.error || '目录创建未完成,请检查任务状态。');
|
|
526
637
|
if (Date.now() >= deadline) throw new Error('目录仍在创建,请稍后刷新目录核对结果。');
|
|
527
638
|
await new Promise(resolve => setTimeout(resolve,300));
|
|
@@ -659,7 +770,7 @@ async function pollOperations() {
|
|
|
659
770
|
card.text.title = job.currentKey || ''; card.button.hidden = !live; card.button.disabled = job.status === 'stopping';
|
|
660
771
|
card.dismiss.hidden = job.kind !== 'statistics' || live;
|
|
661
772
|
card.details.hidden = job.kind !== 'statistics'; card.job = job;
|
|
662
|
-
if (card.status
|
|
773
|
+
if (card.status !== job.status && !live && job.kind !== 'statistics') markListingChanged(job.grantId);
|
|
663
774
|
card.status = job.status; card.kind = job.kind;
|
|
664
775
|
}
|
|
665
776
|
$('operations-panel').hidden = !operationCards.size;
|
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,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
|
|
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;;;;;;;;;;;;;;;;;;;2BAmBxN,aAAa;eACzB,CAAC;AAEhB,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuDpC,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MA+wB9B,CAAC"}
|
|
@@ -8,9 +8,10 @@ export declare class OssWebSession {
|
|
|
8
8
|
private cached?;
|
|
9
9
|
private binding?;
|
|
10
10
|
private pending?;
|
|
11
|
-
|
|
11
|
+
private pendingSeconds;
|
|
12
|
+
constructor(grantId: number, issue: (seconds: number) => Promise<AccessSession>, now?: () => number);
|
|
12
13
|
get(): Promise<AccessSession>;
|
|
13
14
|
status(): Promise<AccessSession>;
|
|
14
|
-
renew(): Promise<AccessSession>;
|
|
15
|
+
renew(seconds?: number): Promise<AccessSession>;
|
|
15
16
|
private refresh;
|
|
16
17
|
}
|
package/dist/oss/web-session.js
CHANGED
|
@@ -10,6 +10,7 @@ export class OssWebSession {
|
|
|
10
10
|
cached;
|
|
11
11
|
binding;
|
|
12
12
|
pending;
|
|
13
|
+
pendingSeconds = 0;
|
|
13
14
|
constructor(grantId, issue, now = Date.now) {
|
|
14
15
|
this.grantId = grantId;
|
|
15
16
|
this.issue = issue;
|
|
@@ -28,15 +29,21 @@ export class OssWebSession {
|
|
|
28
29
|
async status() {
|
|
29
30
|
return this.cached && !this.autoRenew ? this.cached : this.get();
|
|
30
31
|
}
|
|
31
|
-
renew() {
|
|
32
|
-
if (
|
|
33
|
-
return
|
|
34
|
-
|
|
32
|
+
renew(seconds = 900) {
|
|
33
|
+
if (!Number.isInteger(seconds) || seconds < 900 || seconds > 3600) {
|
|
34
|
+
return Promise.reject(new OssError("Invalid STS duration", "oss_input_invalid", 400));
|
|
35
|
+
}
|
|
36
|
+
if (this.pending) {
|
|
37
|
+
// A concurrent routine renewal must not swallow a request for a longer session.
|
|
38
|
+
return seconds <= this.pendingSeconds ? this.pending : this.pending.then(() => this.renew(seconds));
|
|
39
|
+
}
|
|
40
|
+
this.pendingSeconds = seconds;
|
|
41
|
+
this.pending = this.refresh(seconds).finally(() => { this.pending = undefined; });
|
|
35
42
|
return this.pending;
|
|
36
43
|
}
|
|
37
|
-
async refresh() {
|
|
44
|
+
async refresh(seconds) {
|
|
38
45
|
try {
|
|
39
|
-
const parsed = AccessSessionSchema.safeParse(await this.issue());
|
|
46
|
+
const parsed = AccessSessionSchema.safeParse(await this.issue(seconds));
|
|
40
47
|
if (!parsed.success || parsed.data.grantId !== this.grantId
|
|
41
48
|
|| !(Date.parse(parsed.data.expiration) > this.now() + 90_000))
|
|
42
49
|
throw new Error("Invalid session");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-session.js","sourceRoot":"","sources":["../../src/oss/web-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAsB,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,mFAAmF;AACnF,MAAM,OAAO,aAAa;
|
|
1
|
+
{"version":3,"file":"web-session.js","sourceRoot":"","sources":["../../src/oss/web-session.ts"],"names":[],"mappings":"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"}
|
package/dist/oss/web.js
CHANGED
|
@@ -15,6 +15,7 @@ import { createLocalPicker } from "./local-picker.js";
|
|
|
15
15
|
import { planDownloadJobs, planUploadJobs } from "./transfer-paths.js";
|
|
16
16
|
import { createTransferQueue } from "./transfer-queue.js";
|
|
17
17
|
import { OssFileOperations } from "./file-operations.js";
|
|
18
|
+
import { createDownloadLinks, downloadLinksInputSchema, downloadLinksOutput } from "./download-links.js";
|
|
18
19
|
import { discoverWebService, writeWebService, removeWebService, serviceInfo } from "./web-service.js";
|
|
19
20
|
function queueNamespace(base, subject) {
|
|
20
21
|
return createHash("sha256").update(JSON.stringify([base, subject])).digest("hex").slice(0, 24);
|
|
@@ -70,6 +71,11 @@ function errorResponse(res, error) {
|
|
|
70
71
|
oss_grant_forbidden: "此 Grant 当前不可用,请刷新授权清单。",
|
|
71
72
|
oss_scope_violation: "路径超出所选 Grant 的授权范围。",
|
|
72
73
|
oss_operation_forbidden: "当前授权不允许此操作。",
|
|
74
|
+
oss_link_limit: "所选内容超过 100 个文件,或目录扫描达到上限,未生成链接清单。请缩小范围或使用批量下载。",
|
|
75
|
+
oss_link_empty: "所选文件夹中没有可下载的文件。",
|
|
76
|
+
oss_link_expired: "STS 剩余时间不足,请续期后重新生成链接。",
|
|
77
|
+
oss_link_duration: "所选时长超过当前授权的 STS 上限,请刷新授权清单后重新选择。",
|
|
78
|
+
oss_link_sign_failed: "下载链接生成失败,请稍后重试。",
|
|
73
79
|
oss_operation_busy: "已有文件操作正在执行,请等待完成或先停止该操作。",
|
|
74
80
|
oss_input_invalid: "请求参数无效,或尚未确认删除。",
|
|
75
81
|
oss_picker_busy: "已有系统文件选择器正在打开,请完成或取消后重试。",
|
|
@@ -186,6 +192,7 @@ export async function startOssWeb(options) {
|
|
|
186
192
|
const managers = new Map();
|
|
187
193
|
const pendingManagers = new Map();
|
|
188
194
|
let subject;
|
|
195
|
+
let linksBusy = false;
|
|
189
196
|
async function grantsForIdentity() {
|
|
190
197
|
const response = await client.listGrants();
|
|
191
198
|
if (subject !== undefined && response.subject !== subject) {
|
|
@@ -207,7 +214,7 @@ export async function startOssWeb(options) {
|
|
|
207
214
|
const available = readyWebGrants(response.grants);
|
|
208
215
|
if (!available.some(g => g.grantId === id))
|
|
209
216
|
throw new OssError("Grant unavailable", "oss_grant_forbidden", 403);
|
|
210
|
-
const manager = new OssWebSession(id,
|
|
217
|
+
const manager = new OssWebSession(id, seconds => client.issueSession(id, seconds), now);
|
|
211
218
|
await manager.get();
|
|
212
219
|
managers.set(id, manager);
|
|
213
220
|
return manager;
|
|
@@ -305,7 +312,7 @@ export async function startOssWeb(options) {
|
|
|
305
312
|
if (req.method === "GET" && url.pathname === "/api/grants") {
|
|
306
313
|
const grants = readyWebGrants((await grantsForIdentity()).grants).map(g => ({ grantId: g.grantId,
|
|
307
314
|
bucketName: g.bucketName, objectPrefix: g.objectPrefix ?? "", policyCode: g.policyCode ?? "",
|
|
308
|
-
permissionLevel: g.permissionLevel, actions: g.actions }));
|
|
315
|
+
permissionLevel: g.permissionLevel, actions: g.actions, maxSessionSeconds: g.maxSessionSeconds }));
|
|
309
316
|
json(res, 200, { ok: true, selectedGrantId: options.grantId, grants });
|
|
310
317
|
return;
|
|
311
318
|
}
|
|
@@ -355,6 +362,43 @@ export async function startOssWeb(options) {
|
|
|
355
362
|
json(res, 200, { ok: true, session: sessionView(await manager.status(), manager, now()) });
|
|
356
363
|
return;
|
|
357
364
|
}
|
|
365
|
+
if (req.method === "POST" && url.pathname === "/api/download-links") {
|
|
366
|
+
if (linksBusy)
|
|
367
|
+
throw new OssError("Another link request is running.", "oss_operation_busy", 409);
|
|
368
|
+
const input = await strictJson(req, ["entries", "expires", "renewSts"]);
|
|
369
|
+
if (Array.isArray(input.entries) && input.entries.length > 100) {
|
|
370
|
+
throw new OssError("Selection exceeds 100 files.", "oss_link_limit", 400);
|
|
371
|
+
}
|
|
372
|
+
const parsed = downloadLinksInputSchema.safeParse({ entries: input.entries, expires: input.expires });
|
|
373
|
+
if (!parsed.success || (input.renewSts !== undefined && typeof input.renewSts !== "boolean")) {
|
|
374
|
+
throw new OssError("Invalid link request", "oss_input_invalid", 400);
|
|
375
|
+
}
|
|
376
|
+
if (linksBusy)
|
|
377
|
+
throw new OssError("Another link request is running.", "oss_operation_busy", 409);
|
|
378
|
+
linksBusy = true;
|
|
379
|
+
try {
|
|
380
|
+
let session;
|
|
381
|
+
if (input.renewSts) {
|
|
382
|
+
const grant = readyWebGrants((await grantsForIdentity()).grants).find(g => g.grantId === id);
|
|
383
|
+
if (!grant)
|
|
384
|
+
throw new OssError("Grant unavailable", "oss_grant_forbidden", 403);
|
|
385
|
+
if (parsed.data.expires > grant.maxSessionSeconds)
|
|
386
|
+
throw new OssError("Duration exceeds Grant", "oss_link_duration", 400);
|
|
387
|
+
// Allow a little time for HEAD/list checks, within the current Grant's limit.
|
|
388
|
+
session = await manager.renew(Math.min(grant.maxSessionSeconds, Math.max(900, parsed.data.expires + 30)));
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
session = await manager.get();
|
|
392
|
+
}
|
|
393
|
+
const links = downloadLinksOutput(await createDownloadLinks(transfer, session, parsed.data));
|
|
394
|
+
if (!res.destroyed)
|
|
395
|
+
json(res, 200, { ok: true, data: links, session: sessionView(session, manager, now()) });
|
|
396
|
+
}
|
|
397
|
+
finally {
|
|
398
|
+
linksBusy = false;
|
|
399
|
+
}
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
358
402
|
const session = await manager.get();
|
|
359
403
|
if (req.method === "POST" && url.pathname === "/api/operations") {
|
|
360
404
|
const input = await strictJson(req, ["kind", "entries", "destination"]);
|