@file-viewer/renderer-archive 2.1.19 → 2.1.20
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.en.md +31 -1
- package/README.md +31 -1
- package/dist/archive.js +55 -34
- package/dist/archiveFallback.d.ts +5 -1
- package/dist/archiveFallback.js +74 -1
- package/dist/archiveShared.d.ts +3 -1
- package/dist/archiveShared.js +25 -0
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -34,7 +34,7 @@ const options = {
|
|
|
34
34
|
- Uses `libarchive.js` Worker + WASM first to keep large archive parsing off the main thread.
|
|
35
35
|
- Falls back to ZIP / TAR / GZIP parsing when the Worker cannot be started, which helps mobile WebViews, local static servers, and private intranet deployments. Encrypted archives never use the fallback path and require the libarchive Worker/WASM assets.
|
|
36
36
|
- Extracts internal files on demand, then delegates nested previews through `renderNestedBuffer` or the core dispatcher.
|
|
37
|
-
- Includes archive size limits, entry preview limits, worker timeout, IndexedDB cache, and
|
|
37
|
+
- Includes archive size limits, entry preview limits, worker timeout, IndexedDB cache, and configurable nested-entry download.
|
|
38
38
|
|
|
39
39
|
## Offline Assets
|
|
40
40
|
|
|
@@ -56,6 +56,36 @@ const options = {
|
|
|
56
56
|
}
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
## Nested Entry Download Policy
|
|
60
|
+
|
|
61
|
+
The download button in the archive entry preview toolbar stays visible by default. Hide it globally with a boolean:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const options = {
|
|
65
|
+
archive: {
|
|
66
|
+
entryActions: {
|
|
67
|
+
download: false,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Or decide per entry from path, extension, size, and other metadata:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
const options = {
|
|
77
|
+
archive: {
|
|
78
|
+
entryActions: {
|
|
79
|
+
download(entry) {
|
|
80
|
+
return entry.path.startsWith('public/')
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
This option only controls nested archive entries. The viewer-level toolbar action that downloads the original archive remains controlled by `toolbar` and operation guards.
|
|
88
|
+
|
|
59
89
|
## Encrypted Archives
|
|
60
90
|
|
|
61
91
|
By default, encrypted archives open a built-in password dialog. Applications can also provide an initial password or take over password collection:
|
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ const options = {
|
|
|
34
34
|
- 优先使用 `libarchive.js` Worker + WASM,避免大压缩包阻塞主线程。
|
|
35
35
|
- Worker 不可用时自动回退到 ZIP / TAR / GZIP 兼容模式,适合手机 WebView、本地临时服务器和内网静态部署排障;加密压缩包不会走 fallback,必须发布 libarchive Worker/WASM。
|
|
36
36
|
- 点击内部文件后才按需解压,并通过 `renderNestedBuffer` 或 core dispatcher 复用 PDF、Office、CAD、XMind、图片、代码等现有 renderer。
|
|
37
|
-
- 内置体积上限、单文件预览上限、Worker 超时、IndexedDB
|
|
37
|
+
- 内置体积上限、单文件预览上限、Worker 超时、IndexedDB 缓存和可配置的内部条目下载入口,避免一次性把压缩包全部展开到内存。
|
|
38
38
|
|
|
39
39
|
## 离线资产
|
|
40
40
|
|
|
@@ -56,6 +56,36 @@ const options = {
|
|
|
56
56
|
}
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
## 内部条目下载权限
|
|
60
|
+
|
|
61
|
+
压缩包内部文件预览栏的下载按钮默认保持可见。需要隐藏时可以配置全局布尔值:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const options = {
|
|
65
|
+
archive: {
|
|
66
|
+
entryActions: {
|
|
67
|
+
download: false,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
也可以按内部路径、扩展名、大小等元数据做判断:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
const options = {
|
|
77
|
+
archive: {
|
|
78
|
+
entryActions: {
|
|
79
|
+
download(entry) {
|
|
80
|
+
return entry.path.startsWith('public/')
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
这个选项只影响压缩包内部条目的下载按钮,不会关闭顶层 viewer 下载原始压缩包的工具栏动作。
|
|
88
|
+
|
|
59
89
|
## 加密压缩包
|
|
60
90
|
|
|
61
91
|
默认情况下,检测到加密内容会显示内置密码弹框。业务也可以预置密码,或接管密码获取流程:
|
package/dist/archive.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { resolveFileViewerArchiveWasmUrl, resolveFileViewerArchiveWorkerUrl, } from '@file-viewer/core/assets';
|
|
2
|
-
import { collectFileViewerRendererPlugins, createFileRenderHandlerLoader, createFileViewerCoreRendererRegistry, createFileViewerTranslator, createRendererRegistry, disposeFileViewerRendered, installFileViewerRendererPlugins, listFileViewerAutoRendererPresets, normalizeSource, resolveFileViewerRendererPresetInputs, } from '@file-viewer/core';
|
|
3
|
-
import { buildArchiveNestedRenderContext, createArchiveCacheKey, flattenArchiveObject, formatArchiveBytes, getArchiveEntryExtension, } from './archiveShared.js';
|
|
2
|
+
import { collectFileViewerRendererPlugins, createFileRenderHandlerLoader, createFileViewerCoreRendererRegistry, createFileViewerTranslator, createRendererRegistry, disposeFileViewerRendered, installFileViewerRendererPlugins, listFileViewerAutoRendererPresets, normalizeFileViewerUiDensity, normalizeSource, resolveFileViewerRendererPresetInputs, } from '@file-viewer/core';
|
|
3
|
+
import { buildArchiveNestedRenderContext, createArchiveCacheKey, flattenArchiveObject, formatArchiveBytes, getArchiveEntryExtension, isArchiveEntryDownloadAllowed, } from './archiveShared.js';
|
|
4
4
|
import { readArchiveCache, writeArchiveCache } from './archiveCache.js';
|
|
5
|
-
import { isLikelyEncryptedArchive, loadArchiveEntriesWithoutWorker, } from './archiveFallback.js';
|
|
5
|
+
import { hasLikelyGbkZipFilenames, isLikelyEncryptedArchive, loadArchiveEntriesWithoutWorker, } from './archiveFallback.js';
|
|
6
6
|
const DEFAULT_MAX_ARCHIVE_SIZE = 320 * 1024 * 1024;
|
|
7
7
|
const DEFAULT_MAX_ENTRY_PREVIEW_SIZE = 64 * 1024 * 1024;
|
|
8
8
|
const DEFAULT_WORKER_TIMEOUT_MS = 30000;
|
|
@@ -14,43 +14,45 @@ class ArchivePasswordCancelledError extends Error {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
const archiveStyle = `
|
|
17
|
-
.archive-shell,.archive-viewer{position:relative;box-sizing:border-box;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden;display:grid;grid-template-columns:minmax(280px,34%) minmax(0,1fr);grid-template-rows:minmax(0,1fr);background:#edf2f7;color:#172033;font-family:Aptos,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif}
|
|
18
|
-
.archive-shell *,.archive-viewer *{box-sizing:border-box}
|
|
19
|
-
.archive-
|
|
17
|
+
.archive-shell,.archive-viewer{position:relative;box-sizing:border-box;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden;display:grid;grid-template-columns:minmax(280px,34%) minmax(0,1fr);grid-template-rows:minmax(0,1fr);--archive-sidebar-gap:12px;--archive-sidebar-padding:16px;--archive-head-gap:10px;--archive-title-margin:4px;--archive-title-size:18px;--archive-stats-margin:8px;--archive-toggle-size:34px;--archive-toggle-radius:10px;--archive-notice-padding:10px 12px;--archive-notice-radius:12px;--archive-search-height:42px;--archive-search-padding:0 12px;--archive-search-radius:12px;--archive-list-gap:7px;--archive-list-padding:4px;--archive-entry-min-height:58px;--archive-entry-icon-column:42px;--archive-entry-gap:10px;--archive-entry-padding-y:8px;--archive-entry-padding-x:10px;--archive-entry-depth-step:10px;--archive-entry-radius:12px;--archive-entry-ext-height:34px;--archive-entry-ext-radius:10px;--archive-entry-size-width:74px;--archive-preview-toolbar-min-height:64px;--archive-preview-toolbar-gap:12px;--archive-preview-toolbar-padding:12px 16px;--archive-preview-button-height:34px;--archive-preview-button-padding:0 12px;--archive-preview-button-radius:10px;--archive-empty-padding:28px;--archive-state-gap:14px;--archive-state-padding:18px;--archive-state-radius:16px;background:#edf2f7;color:#172033;font-family:Aptos,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif}
|
|
18
|
+
.archive-shell *,.archive-viewer *{box-sizing:border-box}
|
|
19
|
+
.archive-shell[data-viewer-density='compact'],.archive-viewer[data-viewer-density='compact'],.file-viewer[data-viewer-density='compact'] .archive-shell,.file-viewer[data-viewer-density='compact'] .archive-viewer,.file-viewer-web-shell[data-viewer-density='compact'] .archive-shell,.file-viewer-web-shell[data-viewer-density='compact'] .archive-viewer{--archive-sidebar-gap:5px;--archive-sidebar-padding:5px;--archive-head-gap:5px;--archive-title-margin:2px;--archive-title-size:16px;--archive-stats-margin:3px;--archive-toggle-size:28px;--archive-toggle-radius:7px;--archive-notice-padding:4px 5px;--archive-notice-radius:7px;--archive-search-height:30px;--archive-search-padding:0 5px;--archive-search-radius:7px;--archive-list-gap:3px;--archive-list-padding:2px;--archive-entry-min-height:42px;--archive-entry-icon-column:30px;--archive-entry-gap:5px;--archive-entry-padding-y:3px;--archive-entry-padding-x:5px;--archive-entry-depth-step:7px;--archive-entry-radius:7px;--archive-entry-ext-height:26px;--archive-entry-ext-radius:6px;--archive-entry-size-width:58px;--archive-preview-toolbar-min-height:38px;--archive-preview-toolbar-gap:5px;--archive-preview-toolbar-padding:4px 5px;--archive-preview-button-height:28px;--archive-preview-button-padding:0 5px;--archive-preview-button-radius:7px;--archive-empty-padding:18px;--archive-state-gap:5px;--archive-state-padding:5px;--archive-state-radius:10px}
|
|
20
|
+
@media (pointer:coarse){.archive-shell[data-viewer-density='compact'],.archive-viewer[data-viewer-density='compact'],.file-viewer[data-viewer-density='compact'] .archive-shell,.file-viewer[data-viewer-density='compact'] .archive-viewer,.file-viewer-web-shell[data-viewer-density='compact'] .archive-shell,.file-viewer-web-shell[data-viewer-density='compact'] .archive-viewer{--archive-toggle-size:34px;--archive-search-height:36px;--archive-entry-min-height:50px;--archive-entry-ext-height:30px;--archive-preview-toolbar-min-height:48px;--archive-preview-button-height:34px}}
|
|
21
|
+
.archive-sidebar{grid-column:1;grid-row:1;min-width:0;min-height:0;overflow:hidden;display:flex;flex-direction:column;gap:var(--archive-sidebar-gap);padding:var(--archive-sidebar-padding);border-right:1px solid rgba(23,32,51,.08);background:rgba(255,255,255,.72);transition:opacity .18s ease,padding .18s ease,border-color .18s ease}
|
|
20
22
|
.archive-shell.archive-sidebar-collapsed,.archive-viewer.archive-sidebar-collapsed{grid-template-columns:0 minmax(0,1fr)}
|
|
21
23
|
.archive-sidebar-collapsed .archive-sidebar{display:none;width:0;max-width:0;padding:0;border-color:transparent;opacity:0;pointer-events:none}
|
|
22
24
|
.archive-sidebar-collapsed .archive-sidebar>*{visibility:hidden}
|
|
23
|
-
.archive-head{min-width:0;display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:
|
|
24
|
-
.archive-head-main{min-width:0}
|
|
25
|
-
.archive-head span,.archive-preview-toolbar span{color:#6c7c90;font-size:12px;font-weight:800;letter-spacing:0}
|
|
26
|
-
.archive-head strong,.archive-preview-toolbar strong{display:block;margin-top:
|
|
27
|
-
.archive-head p{min-width:0;margin:
|
|
28
|
-
.archive-sidebar-toggle{width:
|
|
29
|
-
.archive-sidebar-toggle:hover{border-color:rgba(31,122,88,.32);background:#f0fdf4}
|
|
30
|
-
.archive-warning,.archive-info,.archive-error{border-radius:
|
|
31
|
-
.archive-info{background:#ecfdf5;color:#166534}
|
|
32
|
-
.archive-search{width:100%;height:
|
|
33
|
-
.archive-list{flex:1;min-width:0;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:
|
|
34
|
-
.archive-entry{width:100%;min-width:0;min-height:
|
|
35
|
-
.archive-entry:hover,.archive-entry.active{border-color:rgba(33,129,95,.28);box-shadow:0 10px 22px rgba(23,32,51,.08)}
|
|
36
|
-
.entry-ext{height:
|
|
37
|
-
.entry-copy{min-width:0}
|
|
38
|
-
.entry-copy strong,.entry-copy em{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
39
|
-
.entry-copy em,.archive-entry small{color:#718096;font-size:12px;font-style:normal}
|
|
40
|
-
.archive-entry small{min-width:0;max-width:
|
|
25
|
+
.archive-head{min-width:0;display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:var(--archive-head-gap);align-items:start}
|
|
26
|
+
.archive-head-main{min-width:0}
|
|
27
|
+
.archive-head span,.archive-preview-toolbar span{color:#6c7c90;font-size:12px;font-weight:800;letter-spacing:0}
|
|
28
|
+
.archive-head strong,.archive-preview-toolbar strong{display:block;margin-top:var(--archive-title-margin);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--archive-title-size);line-height:1.25}
|
|
29
|
+
.archive-head p{min-width:0;margin:var(--archive-stats-margin) 0 0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#69798b;font-size:13px}
|
|
30
|
+
.archive-sidebar-toggle{width:var(--archive-toggle-size);height:var(--archive-toggle-size);flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;border:1px solid rgba(23,32,51,.1);border-radius:var(--archive-toggle-radius);background:#fff;color:#1f7a58;font:inherit;font-size:17px;font-weight:900;line-height:1;cursor:pointer;box-shadow:0 6px 16px rgba(23,32,51,.07)}
|
|
31
|
+
.archive-sidebar-toggle:hover{border-color:rgba(31,122,88,.32);background:#f0fdf4}
|
|
32
|
+
.archive-warning,.archive-info,.archive-error{border-radius:var(--archive-notice-radius);padding:var(--archive-notice-padding);background:#fff7e8;color:#8a4b00;font-size:13px;line-height:1.5}
|
|
33
|
+
.archive-info{background:#ecfdf5;color:#166534}
|
|
34
|
+
.archive-search{width:100%;height:var(--archive-search-height);padding:var(--archive-search-padding);border-radius:var(--archive-search-radius);border:1px solid rgba(23,32,51,.1);outline:none;background:#fff;color:#172033;font:inherit}
|
|
35
|
+
.archive-list{flex:1;min-width:0;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:var(--archive-list-gap);padding-right:var(--archive-list-padding)}
|
|
36
|
+
.archive-entry{width:100%;min-width:0;min-height:var(--archive-entry-min-height);display:grid;grid-template-columns:var(--archive-entry-icon-column) minmax(0,1fr) minmax(46px,auto);gap:var(--archive-entry-gap);align-items:center;padding:var(--archive-entry-padding-y) var(--archive-entry-padding-x) var(--archive-entry-padding-y) calc(var(--archive-entry-padding-x) + var(--entry-depth,0) * var(--archive-entry-depth-step));border:1px solid rgba(23,32,51,.07);border-radius:var(--archive-entry-radius);background:rgba(255,255,255,.86);color:inherit;text-align:left;cursor:pointer;font:inherit}
|
|
37
|
+
.archive-entry:hover,.archive-entry.active{border-color:rgba(33,129,95,.28);box-shadow:0 10px 22px rgba(23,32,51,.08)}
|
|
38
|
+
.entry-ext{height:var(--archive-entry-ext-height);display:inline-flex;align-items:center;justify-content:center;border-radius:var(--archive-entry-ext-radius);background:rgba(33,129,95,.12);color:#1d7a56;font-size:11px;font-weight:900;text-transform:uppercase}
|
|
39
|
+
.entry-copy{min-width:0}
|
|
40
|
+
.entry-copy strong,.entry-copy em{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
41
|
+
.entry-copy em,.archive-entry small{color:#718096;font-size:12px;font-style:normal}
|
|
42
|
+
.archive-entry small{min-width:0;max-width:var(--archive-entry-size-width);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right}
|
|
41
43
|
.archive-preview{grid-column:2;grid-row:1;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden;display:flex;flex-direction:column}
|
|
42
|
-
.archive-preview-toolbar{min-width:0;min-height:
|
|
43
|
-
.archive-preview-toolbar button{height:
|
|
44
|
+
.archive-preview-toolbar{min-width:0;min-height:var(--archive-preview-toolbar-min-height);display:flex;align-items:center;gap:var(--archive-preview-toolbar-gap);padding:var(--archive-preview-toolbar-padding);border-bottom:1px solid rgba(23,32,51,.08);background:rgba(255,255,255,.76)}
|
|
45
|
+
.archive-preview-toolbar button{height:var(--archive-preview-button-height);border:0;border-radius:var(--archive-preview-button-radius);padding:var(--archive-preview-button-padding);background:#1f7a58;color:#fff;font:inherit;font-size:13px;font-weight:800;cursor:pointer}
|
|
44
46
|
.archive-preview-toolbar .archive-sidebar-toggle{background:#fff;color:#1f7a58;border:1px solid rgba(23,32,51,.1);padding:0}
|
|
45
47
|
.archive-preview-title{min-width:0;flex:1}
|
|
46
48
|
.archive-preview-toolbar span{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
47
49
|
.archive-preview-toolbar .archive-download-button{flex:0 0 auto}
|
|
48
50
|
.archive-nested-target{position:relative;flex:1;min-height:0;overflow:auto}
|
|
49
51
|
.archive-nested-content{width:100%;height:100%;min-height:420px}
|
|
50
|
-
.archive-empty{height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:
|
|
52
|
+
.archive-empty{height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:var(--archive-empty-padding);text-align:center;color:#64748b}
|
|
51
53
|
.archive-empty strong{color:#172033;font-size:18px}
|
|
52
54
|
.archive-state{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(241,245,249,.82);backdrop-filter:blur(8px);z-index:4}
|
|
53
|
-
.archive-state>div{display:flex;align-items:center;gap:
|
|
55
|
+
.archive-state>div{display:flex;align-items:center;gap:var(--archive-state-gap);width:min(92%,430px);padding:var(--archive-state-padding);border-radius:var(--archive-state-radius);background:#fff;box-shadow:0 18px 42px rgba(15,23,42,.14)}
|
|
54
56
|
.archive-state p{margin:4px 0 0;color:#64748b}
|
|
55
57
|
.archive-spinner{width:34px;height:34px;flex-shrink:0;border-radius:999px;border:3px solid rgba(31,122,88,.16);border-top-color:#1f7a58;animation:archive-spin .9s linear infinite}
|
|
56
58
|
.archive-error{position:absolute;right:18px;bottom:18px;width:min(460px,calc(100% - 36px));box-shadow:0 16px 36px rgba(23,32,51,.14);z-index:5}
|
|
@@ -311,7 +313,7 @@ const renderNestedEntry = (buffer, type, target, context) => {
|
|
|
311
313
|
});
|
|
312
314
|
};
|
|
313
315
|
export default async function renderArchive(buffer, target, _type, context) {
|
|
314
|
-
var _a;
|
|
316
|
+
var _a, _b, _c;
|
|
315
317
|
const documentRef = target.ownerDocument;
|
|
316
318
|
const targetWindow = documentRef.defaultView || null;
|
|
317
319
|
const archiveOptions = (_a = context === null || context === void 0 ? void 0 : context.options) === null || _a === void 0 ? void 0 : _a.archive;
|
|
@@ -320,6 +322,7 @@ export default async function renderArchive(buffer, target, _type, context) {
|
|
|
320
322
|
const maxEntryPreviewSize = (archiveOptions === null || archiveOptions === void 0 ? void 0 : archiveOptions.maxEntryPreviewSize) || DEFAULT_MAX_ENTRY_PREVIEW_SIZE;
|
|
321
323
|
const cacheEnabled = (archiveOptions === null || archiveOptions === void 0 ? void 0 : archiveOptions.cache) !== false;
|
|
322
324
|
const workerTimeoutMs = (archiveOptions === null || archiveOptions === void 0 ? void 0 : archiveOptions.workerTimeoutMs) || DEFAULT_WORKER_TIMEOUT_MS;
|
|
325
|
+
const hasLegacyGbkZipFilenames = hasLikelyGbkZipFilenames(buffer, filename);
|
|
323
326
|
const objectUrls = [];
|
|
324
327
|
const cleanups = [];
|
|
325
328
|
let archiveReader = null;
|
|
@@ -339,6 +342,7 @@ export default async function renderArchive(buffer, target, _type, context) {
|
|
|
339
342
|
let sidebarCollapsed = false;
|
|
340
343
|
const style = createStyle(documentRef);
|
|
341
344
|
const root = createElement(documentRef, 'section', 'archive-shell archive-viewer');
|
|
345
|
+
root.dataset.viewerDensity = normalizeFileViewerUiDensity((_c = (_b = context === null || context === void 0 ? void 0 : context.options) === null || _b === void 0 ? void 0 : _b.ui) === null || _c === void 0 ? void 0 : _c.density);
|
|
342
346
|
const sidebar = createElement(documentRef, 'aside', 'archive-sidebar');
|
|
343
347
|
const head = createElement(documentRef, 'div', 'archive-head');
|
|
344
348
|
const headMain = createElement(documentRef, 'div', 'archive-head-main');
|
|
@@ -553,7 +557,11 @@ export default async function renderArchive(buffer, target, _type, context) {
|
|
|
553
557
|
stateHint.textContent = loadingHint;
|
|
554
558
|
error.classList.toggle('archive-hidden', !errorText);
|
|
555
559
|
errorMessage.textContent = errorText;
|
|
556
|
-
|
|
560
|
+
const downloadAllowed = selectedEntry
|
|
561
|
+
? isArchiveEntryDownloadAllowed(selectedEntry, archiveOptions)
|
|
562
|
+
: false;
|
|
563
|
+
downloadButton.hidden = !downloadAllowed;
|
|
564
|
+
downloadButton.disabled = !downloadAllowed;
|
|
557
565
|
const activeTitle = toolbarTitle.querySelector('strong');
|
|
558
566
|
if (activeTitle) {
|
|
559
567
|
activeTitle.textContent = (selectedEntry === null || selectedEntry === void 0 ? void 0 : selectedEntry.name) || t('archive.preview.chooseFile');
|
|
@@ -642,6 +650,9 @@ export default async function renderArchive(buffer, target, _type, context) {
|
|
|
642
650
|
const archiveFile = createArchiveFile(documentRef, buffer, filename);
|
|
643
651
|
const archive = await withTimeout(Archive.open(archiveFile), workerTimeoutMs, t('archive.error.candidateInitTimeout', { label: candidate.label }), targetWindow);
|
|
644
652
|
archiveReader = archive;
|
|
653
|
+
if (hasLegacyGbkZipFilenames && typeof archive.setLocale === 'function') {
|
|
654
|
+
await archive.setLocale('zh_CN.GBK').catch(() => undefined);
|
|
655
|
+
}
|
|
645
656
|
encrypted = await withTimeout(archive.hasEncryptedData(), workerTimeoutMs, t('archive.error.encryptedCheckTimeout', { label: candidate.label }), targetWindow).catch(() => null);
|
|
646
657
|
if (encrypted) {
|
|
647
658
|
await requestAndApplyPassword('encrypted');
|
|
@@ -662,15 +673,20 @@ export default async function renderArchive(buffer, target, _type, context) {
|
|
|
662
673
|
throw reason;
|
|
663
674
|
}
|
|
664
675
|
};
|
|
665
|
-
const tryOpenArchiveWithFallback = async () => {
|
|
666
|
-
|
|
676
|
+
const tryOpenArchiveWithFallback = async (options = {}) => {
|
|
677
|
+
const showWorkerFallbackNotice = options.showWorkerFallbackNotice !== false;
|
|
678
|
+
setLoading(true, showWorkerFallbackNotice
|
|
679
|
+
? t('archive.loading.workerFallback')
|
|
680
|
+
: t('archive.loading.readingDirectory'), showWorkerFallbackNotice
|
|
681
|
+
? t('archive.loading.workerFallbackHint')
|
|
682
|
+
: t('archive.loading.directoryReadyHint'));
|
|
667
683
|
const fallbackEntries = await loadArchiveEntriesWithoutWorker(buffer, filename);
|
|
668
684
|
if (!fallbackEntries) {
|
|
669
685
|
return false;
|
|
670
686
|
}
|
|
671
687
|
entries = fallbackEntries.sort((left, right) => left.path.localeCompare(right.path));
|
|
672
688
|
encrypted = null;
|
|
673
|
-
archiveNotice = t('archive.notice.workerFallback');
|
|
689
|
+
archiveNotice = showWorkerFallbackNotice ? t('archive.notice.workerFallback') : '';
|
|
674
690
|
syncState();
|
|
675
691
|
renderEntryList();
|
|
676
692
|
renderEmptyState();
|
|
@@ -688,6 +704,11 @@ export default async function renderArchive(buffer, target, _type, context) {
|
|
|
688
704
|
setError('');
|
|
689
705
|
archiveNotice = '';
|
|
690
706
|
try {
|
|
707
|
+
if (hasLegacyGbkZipFilenames && !isLikelyEncryptedArchive(buffer, filename)) {
|
|
708
|
+
if (await tryOpenArchiveWithFallback({ showWorkerFallbackNotice: false })) {
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
691
712
|
const [{ Archive }, candidates] = await Promise.all([
|
|
692
713
|
import('libarchive.js'),
|
|
693
714
|
resolveWorkerCandidates(documentRef, archiveOptions),
|
|
@@ -816,7 +837,7 @@ export default async function renderArchive(buffer, target, _type, context) {
|
|
|
816
837
|
renderEntryList();
|
|
817
838
|
});
|
|
818
839
|
listen(downloadButton, 'click', () => {
|
|
819
|
-
if (selectedEntry) {
|
|
840
|
+
if (selectedEntry && isArchiveEntryDownloadAllowed(selectedEntry, archiveOptions)) {
|
|
820
841
|
void downloadEntry(selectedEntry);
|
|
821
842
|
}
|
|
822
843
|
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type ArchiveEntryView } from './archiveShared.js';
|
|
2
2
|
type JSZipLike = {
|
|
3
|
-
loadAsync(data: ArrayBuffer
|
|
3
|
+
loadAsync(data: ArrayBuffer, options?: {
|
|
4
|
+
decodeFileName?: (bytes: Uint8Array | number[]) => string;
|
|
5
|
+
}): Promise<{
|
|
4
6
|
forEach(callback: (relativePath: string, file: {
|
|
5
7
|
dir?: boolean;
|
|
6
8
|
date?: Date;
|
|
@@ -8,7 +10,9 @@ type JSZipLike = {
|
|
|
8
10
|
}) => void): void;
|
|
9
11
|
}>;
|
|
10
12
|
};
|
|
13
|
+
export declare const decodeZipFilename: (bytes: Uint8Array | number[]) => string;
|
|
11
14
|
export declare const resolveJSZip: (module: unknown) => JSZipLike;
|
|
15
|
+
export declare const hasLikelyGbkZipFilenames: (data: ArrayBuffer, filename: string) => boolean;
|
|
12
16
|
export declare const isLikelyEncryptedArchive: (data: ArrayBuffer, filename: string) => boolean;
|
|
13
17
|
/**
|
|
14
18
|
* Worker fallback for constrained browsers, temporary local servers, and
|
package/dist/archiveFallback.js
CHANGED
|
@@ -5,11 +5,52 @@ const TAR_BLOCK_SIZE = 512;
|
|
|
5
5
|
const ZIP_CENTRAL_FILE_HEADER = 0x02014b50;
|
|
6
6
|
const ZIP_LOCAL_FILE_HEADER = 0x04034b50;
|
|
7
7
|
const ZIP_GENERAL_PURPOSE_ENCRYPTED_FLAG = 0x0001;
|
|
8
|
+
const ZIP_GENERAL_PURPOSE_UTF8_FLAG = 0x0800;
|
|
9
|
+
const UTF8_DECODER = new TextDecoder('utf-8');
|
|
10
|
+
const UTF8_FATAL_DECODER = new TextDecoder('utf-8', { fatal: true });
|
|
11
|
+
const CJK_TEXT_PATTERN = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/;
|
|
8
12
|
const isJSZipLike = (value) => {
|
|
9
13
|
return !!value &&
|
|
10
14
|
(typeof value === 'object' || typeof value === 'function') &&
|
|
11
15
|
typeof value.loadAsync === 'function';
|
|
12
16
|
};
|
|
17
|
+
const createTextDecoder = (label) => {
|
|
18
|
+
try {
|
|
19
|
+
return new TextDecoder(label);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const GBK_DECODER = createTextDecoder('gbk') || createTextDecoder('gb18030');
|
|
26
|
+
const decodeUtf8Strict = (bytes) => {
|
|
27
|
+
try {
|
|
28
|
+
return UTF8_FATAL_DECODER.decode(bytes);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const decodeGbk = (bytes) => {
|
|
35
|
+
try {
|
|
36
|
+
return (GBK_DECODER === null || GBK_DECODER === void 0 ? void 0 : GBK_DECODER.decode(bytes)) || null;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
export const decodeZipFilename = (bytes) => {
|
|
43
|
+
const data = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
44
|
+
const utf8 = decodeUtf8Strict(data);
|
|
45
|
+
if (utf8 !== null) {
|
|
46
|
+
return utf8;
|
|
47
|
+
}
|
|
48
|
+
const gbk = decodeGbk(data);
|
|
49
|
+
if (gbk && CJK_TEXT_PATTERN.test(gbk)) {
|
|
50
|
+
return gbk;
|
|
51
|
+
}
|
|
52
|
+
return UTF8_DECODER.decode(data);
|
|
53
|
+
};
|
|
13
54
|
// Vite can expose the CJS constructor as a function-shaped default export.
|
|
14
55
|
export const resolveJSZip = (module) => {
|
|
15
56
|
const record = module;
|
|
@@ -143,6 +184,36 @@ const hasEncryptedZipEntries = (data) => {
|
|
|
143
184
|
}
|
|
144
185
|
return false;
|
|
145
186
|
};
|
|
187
|
+
export const hasLikelyGbkZipFilenames = (data, filename) => {
|
|
188
|
+
const extension = getArchiveExtension(filename);
|
|
189
|
+
if (!ZIP_LIKE_EXTENSIONS.has(extension)) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
const view = new DataView(data);
|
|
193
|
+
for (let offset = 0; offset + 46 <= view.byteLength; offset += 1) {
|
|
194
|
+
if (readUint32(view, offset) !== ZIP_CENTRAL_FILE_HEADER) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const flag = readUint16(view, offset + 8);
|
|
198
|
+
const nameLength = readUint16(view, offset + 28);
|
|
199
|
+
const extraLength = readUint16(view, offset + 30);
|
|
200
|
+
const commentLength = readUint16(view, offset + 32);
|
|
201
|
+
const nameStart = offset + 46;
|
|
202
|
+
const nameEnd = nameStart + nameLength;
|
|
203
|
+
if (nameEnd > view.byteLength) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
if ((flag & ZIP_GENERAL_PURPOSE_UTF8_FLAG) === 0) {
|
|
207
|
+
const rawName = new Uint8Array(data, nameStart, nameLength);
|
|
208
|
+
const decoded = decodeZipFilename(rawName);
|
|
209
|
+
if (CJK_TEXT_PATTERN.test(decoded)) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
offset = nameEnd + extraLength + commentLength - 1;
|
|
214
|
+
}
|
|
215
|
+
return false;
|
|
216
|
+
};
|
|
146
217
|
export const isLikelyEncryptedArchive = (data, filename) => {
|
|
147
218
|
const extension = getArchiveExtension(filename);
|
|
148
219
|
if (ZIP_LIKE_EXTENSIONS.has(extension)) {
|
|
@@ -162,7 +233,9 @@ const getGzipEntryName = (filename) => {
|
|
|
162
233
|
};
|
|
163
234
|
const loadZipEntries = async (data) => {
|
|
164
235
|
const JSZip = resolveJSZip(await import('jszip'));
|
|
165
|
-
const zip = await JSZip.loadAsync(data
|
|
236
|
+
const zip = await JSZip.loadAsync(data, {
|
|
237
|
+
decodeFileName: decodeZipFilename,
|
|
238
|
+
});
|
|
166
239
|
const entries = [];
|
|
167
240
|
zip.forEach((relativePath, file) => {
|
|
168
241
|
var _a, _b;
|
package/dist/archiveShared.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FileRenderContext, FileViewerArchiveOptions } from '@file-viewer/core';
|
|
1
|
+
import type { FileRenderContext, FileViewerArchiveEntryActionContext, FileViewerArchiveOptions } from '@file-viewer/core';
|
|
2
2
|
export declare const ARCHIVE_PREVIEWABLE_EXTENSIONS: readonly string[];
|
|
3
3
|
export interface ArchiveEntryView {
|
|
4
4
|
id: string;
|
|
@@ -23,4 +23,6 @@ export declare const isPreviewableArchiveEntry: (name: string) => boolean;
|
|
|
23
23
|
export declare const formatArchiveBytes: (value: number) => string;
|
|
24
24
|
export declare const flattenArchiveObject: (input: Record<string, unknown>, prefix?: string) => ArchiveEntryView[];
|
|
25
25
|
export declare const createArchiveCacheKey: (archiveName: string, archiveSize: number, entry: ArchiveEntryView) => string;
|
|
26
|
+
export declare const createArchiveEntryActionContext: (entry: ArchiveEntryView) => FileViewerArchiveEntryActionContext;
|
|
27
|
+
export declare const isArchiveEntryDownloadAllowed: (entry: ArchiveEntryView, archiveOptions: FileViewerArchiveOptions | undefined) => boolean;
|
|
26
28
|
export declare const buildArchiveNestedRenderContext: (context: FileRenderContext | undefined, entry: Pick<ArchiveEntryView, "name">, archiveOptions: FileViewerArchiveOptions | undefined) => FileRenderContext;
|
package/dist/archiveShared.js
CHANGED
|
@@ -85,6 +85,31 @@ export const createArchiveCacheKey = (archiveName, archiveSize, entry) => {
|
|
|
85
85
|
entry.lastModified || 0,
|
|
86
86
|
].join(':');
|
|
87
87
|
};
|
|
88
|
+
export const createArchiveEntryActionContext = (entry) => ({
|
|
89
|
+
path: entry.path,
|
|
90
|
+
name: entry.name,
|
|
91
|
+
extension: entry.extension,
|
|
92
|
+
size: entry.size,
|
|
93
|
+
lastModified: entry.lastModified,
|
|
94
|
+
depth: entry.depth,
|
|
95
|
+
previewable: entry.previewable,
|
|
96
|
+
});
|
|
97
|
+
export const isArchiveEntryDownloadAllowed = (entry, archiveOptions) => {
|
|
98
|
+
var _a;
|
|
99
|
+
const policy = (_a = archiveOptions === null || archiveOptions === void 0 ? void 0 : archiveOptions.entryActions) === null || _a === void 0 ? void 0 : _a.download;
|
|
100
|
+
if (policy === undefined) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
if (typeof policy === 'boolean') {
|
|
104
|
+
return policy;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
return policy(createArchiveEntryActionContext(entry)) !== false;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
88
113
|
const buildNestedOptions = (context, archiveOptions) => ({
|
|
89
114
|
...((context === null || context === void 0 ? void 0 : context.options) || {}),
|
|
90
115
|
archive: archiveOptions,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@file-viewer/renderer-archive",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.20",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Standalone archive renderer plugin for Flyfish File Viewer with libarchive worker, ZIP/TAR fallback, IndexedDB cache, and nested previews.",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"LICENSE"
|
|
57
57
|
],
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@file-viewer/core": "^2.1.
|
|
59
|
+
"@file-viewer/core": "^2.1.20",
|
|
60
60
|
"jszip": "^3.10.1",
|
|
61
61
|
"libarchive.js": "^2.0.2"
|
|
62
62
|
},
|