@file-viewer/renderer-archive 2.1.19 → 2.1.21

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 CHANGED
@@ -30,11 +30,12 @@ const options = {
30
30
  ## Capabilities
31
31
 
32
32
  - Previews ZIP, TAR, GZIP, RAR, 7z, and common archive directories.
33
+ - Decodes legacy ZIP entry names that omit the UTF-8 flag but are actually encoded as GBK/GB18030.
33
34
  - Supports encrypted archives: encrypted content is handled through the unified `libarchive.js` path, the built-in dialog asks for a password, and a correct password unlocks directory reading or nested entry previews.
34
35
  - Uses `libarchive.js` Worker + WASM first to keep large archive parsing off the main thread.
35
36
  - 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
37
  - 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 single-entry download.
38
+ - Includes archive size limits, entry preview limits, worker timeout, IndexedDB cache, and configurable nested-entry download.
38
39
 
39
40
  ## Offline Assets
40
41
 
@@ -44,6 +45,7 @@ Default asset paths are:
44
45
  - `vendor/libarchive/libarchive.wasm`
45
46
 
46
47
  For private deployments, override them with `options.archive.workerUrl` and `options.archive.wasmUrl`.
48
+ The Vue 3 / Vue 2.7 / Vue 2.6 full packages default their viewer asset root to `/file-viewer/` and automatically point to these libarchive files. When deploying under another static prefix, call the matching full package export `setDefaultFullAssetBaseUrl('/your-prefix/')` first.
47
49
 
48
50
  ```ts
49
51
  const options = {
@@ -56,6 +58,36 @@ const options = {
56
58
  }
57
59
  ```
58
60
 
61
+ ## Nested Entry Download Policy
62
+
63
+ The download button in the archive entry preview toolbar stays visible by default. Hide it globally with a boolean:
64
+
65
+ ```ts
66
+ const options = {
67
+ archive: {
68
+ entryActions: {
69
+ download: false,
70
+ },
71
+ },
72
+ }
73
+ ```
74
+
75
+ Or decide per entry from path, extension, size, and other metadata:
76
+
77
+ ```ts
78
+ const options = {
79
+ archive: {
80
+ entryActions: {
81
+ download(entry) {
82
+ return entry.path.startsWith('public/')
83
+ },
84
+ },
85
+ },
86
+ }
87
+ ```
88
+
89
+ This option only controls nested archive entries. The viewer-level toolbar action that downloads the original archive remains controlled by `toolbar` and operation guards.
90
+
59
91
  ## Encrypted Archives
60
92
 
61
93
  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
@@ -30,11 +30,12 @@ const options = {
30
30
  ## 能力边界
31
31
 
32
32
  - 支持 ZIP、TAR、GZIP、RAR、7z 等常见压缩包目录预览。
33
+ - 自动兼容未声明 UTF-8、实际使用 GBK/GB18030 编码的旧 ZIP 中文文件名。
33
34
  - 支持加密压缩包:检测到加密内容后使用 `libarchive.js` 统一解密,默认弹框要求用户输入密码,密码正确后继续读取目录或预览内部文件。
34
35
  - 优先使用 `libarchive.js` Worker + WASM,避免大压缩包阻塞主线程。
35
36
  - Worker 不可用时自动回退到 ZIP / TAR / GZIP 兼容模式,适合手机 WebView、本地临时服务器和内网静态部署排障;加密压缩包不会走 fallback,必须发布 libarchive Worker/WASM。
36
37
  - 点击内部文件后才按需解压,并通过 `renderNestedBuffer` 或 core dispatcher 复用 PDF、Office、CAD、XMind、图片、代码等现有 renderer。
37
- - 内置体积上限、单文件预览上限、Worker 超时、IndexedDB 缓存和下载入口,避免一次性把压缩包全部展开到内存。
38
+ - 内置体积上限、单文件预览上限、Worker 超时、IndexedDB 缓存和可配置的内部条目下载入口,避免一次性把压缩包全部展开到内存。
38
39
 
39
40
  ## 离线资产
40
41
 
@@ -44,6 +45,7 @@ const options = {
44
45
  - `vendor/libarchive/libarchive.wasm`
45
46
 
46
47
  私有化部署时可以通过 `options.archive.workerUrl` 和 `options.archive.wasmUrl` 覆盖。
48
+ Vue 3 / Vue 2.7 / Vue 2.6 full 包默认把 viewer assets 根设为 `/file-viewer/`,会自动指向这两个 libarchive 文件;如果部署在其它静态前缀,先调用对应 full 包导出的 `setDefaultFullAssetBaseUrl('/your-prefix/')` 即可。
47
49
 
48
50
  ```ts
49
51
  const options = {
@@ -56,6 +58,36 @@ const options = {
56
58
  }
57
59
  ```
58
60
 
61
+ ## 内部条目下载权限
62
+
63
+ 压缩包内部文件预览栏的下载按钮默认保持可见。需要隐藏时可以配置全局布尔值:
64
+
65
+ ```ts
66
+ const options = {
67
+ archive: {
68
+ entryActions: {
69
+ download: false,
70
+ },
71
+ },
72
+ }
73
+ ```
74
+
75
+ 也可以按内部路径、扩展名、大小等元数据做判断:
76
+
77
+ ```ts
78
+ const options = {
79
+ archive: {
80
+ entryActions: {
81
+ download(entry) {
82
+ return entry.path.startsWith('public/')
83
+ },
84
+ },
85
+ },
86
+ }
87
+ ```
88
+
89
+ 这个选项只影响压缩包内部条目的下载按钮,不会关闭顶层 viewer 下载原始压缩包的工具栏动作。
90
+
59
91
  ## 加密压缩包
60
92
 
61
93
  默认情况下,检测到加密内容会显示内置密码弹框。业务也可以预置密码,或接管密码获取流程:
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-sidebar{grid-column:1;grid-row:1;min-width:0;min-height:0;overflow:hidden;display:flex;flex-direction:column;gap:12px;padding:16px;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}
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:10px;align-items:start}
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:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:18px;line-height:1.25}
27
- .archive-head p{min-width:0;margin:8px 0 0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#69798b;font-size:13px}
28
- .archive-sidebar-toggle{width:34px;height:34px;flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;border:1px solid rgba(23,32,51,.1);border-radius:10px;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)}
29
- .archive-sidebar-toggle:hover{border-color:rgba(31,122,88,.32);background:#f0fdf4}
30
- .archive-warning,.archive-info,.archive-error{border-radius:12px;padding:10px 12px;background:#fff7e8;color:#8a4b00;font-size:13px;line-height:1.5}
31
- .archive-info{background:#ecfdf5;color:#166534}
32
- .archive-search{width:100%;height:42px;padding:0 12px;border-radius:12px;border:1px solid rgba(23,32,51,.1);outline:none;background:#fff;color:#172033;font:inherit}
33
- .archive-list{flex:1;min-width:0;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:7px;padding-right:4px}
34
- .archive-entry{width:100%;min-width:0;min-height:58px;display:grid;grid-template-columns:42px minmax(0,1fr) minmax(46px,auto);gap:10px;align-items:center;padding:8px 10px 8px calc(10px + var(--entry-depth,0) * 10px);border:1px solid rgba(23,32,51,.07);border-radius:12px;background:rgba(255,255,255,.86);color:inherit;text-align:left;cursor:pointer;font:inherit}
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:34px;display:inline-flex;align-items:center;justify-content:center;border-radius:10px;background:rgba(33,129,95,.12);color:#1d7a56;font-size:11px;font-weight:900;text-transform:uppercase}
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:74px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right}
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:64px;display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid rgba(23,32,51,.08);background:rgba(255,255,255,.76)}
43
- .archive-preview-toolbar button{height:34px;border:0;border-radius:10px;padding:0 12px;background:#1f7a58;color:#fff;font:inherit;font-size:13px;font-weight:800;cursor:pointer}
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:28px;text-align:center;color:#64748b}
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:14px;width:min(92%,430px);padding:18px;border-radius:16px;background:#fff;box-shadow:0 18px 42px rgba(15,23,42,.14)}
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
- downloadButton.hidden = !selectedEntry;
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
- setLoading(true, t('archive.loading.workerFallback'), t('archive.loading.workerFallbackHint'));
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): Promise<{
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
@@ -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;
@@ -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;
@@ -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.19",
3
+ "version": "2.1.21",
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.19",
59
+ "@file-viewer/core": "^2.1.21",
60
60
  "jszip": "^3.10.1",
61
61
  "libarchive.js": "^2.0.2"
62
62
  },