@file-viewer/renderer-archive 2.0.11
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/LICENSE +160 -0
- package/README.en.md +60 -0
- package/README.md +60 -0
- package/dist/archive.d.ts +2 -0
- package/dist/archive.js +547 -0
- package/dist/archiveCache.d.ts +10 -0
- package/dist/archiveCache.js +96 -0
- package/dist/archiveFallback.d.ts +7 -0
- package/dist/archiveFallback.js +166 -0
- package/dist/archiveShared.d.ts +23 -0
- package/dist/archiveShared.js +71 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +17 -0
- package/package.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction, and
|
|
10
|
+
distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by the copyright
|
|
13
|
+
owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all other entities
|
|
16
|
+
that control, are controlled by, or are under common control with that entity.
|
|
17
|
+
For the purposes of this definition, "control" means (i) the power, direct or
|
|
18
|
+
indirect, to cause the direction or management of such entity, whether by
|
|
19
|
+
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
20
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
21
|
+
|
|
22
|
+
"You" (or "Your") shall mean an individual or Legal Entity exercising
|
|
23
|
+
permissions granted by this License.
|
|
24
|
+
|
|
25
|
+
"Source" form shall mean the preferred form for making modifications, including
|
|
26
|
+
but not limited to software source code, documentation source, and configuration
|
|
27
|
+
files.
|
|
28
|
+
|
|
29
|
+
"Object" form shall mean any form resulting from mechanical transformation or
|
|
30
|
+
translation of a Source form, including but not limited to compiled object code,
|
|
31
|
+
generated documentation, and conversions to other media types.
|
|
32
|
+
|
|
33
|
+
"Work" shall mean the work of authorship, whether in Source or Object form,
|
|
34
|
+
made available under the License, as indicated by a copyright notice that is
|
|
35
|
+
included in or attached to the work.
|
|
36
|
+
|
|
37
|
+
"Derivative Works" shall mean any work, whether in Source or Object form, that
|
|
38
|
+
is based on (or derived from) the Work and for which the editorial revisions,
|
|
39
|
+
annotations, elaborations, or other modifications represent, as a whole, an
|
|
40
|
+
original work of authorship. For the purposes of this License, Derivative Works
|
|
41
|
+
shall not include works that remain separable from, or merely link (or bind by
|
|
42
|
+
name) to the interfaces of, the Work and Derivative Works thereof.
|
|
43
|
+
|
|
44
|
+
"Contribution" shall mean any work of authorship, including the original version
|
|
45
|
+
of the Work and any modifications or additions to that Work or Derivative Works
|
|
46
|
+
thereof, that is intentionally submitted to Licensor for inclusion in the Work by
|
|
47
|
+
the copyright owner or by an individual or Legal Entity authorized to submit on
|
|
48
|
+
behalf of the copyright owner. For the purposes of this definition, "submitted"
|
|
49
|
+
means any form of electronic, verbal, or written communication sent to the
|
|
50
|
+
Licensor or its representatives, including but not limited to communication on
|
|
51
|
+
electronic mailing lists, source code control systems, and issue tracking systems
|
|
52
|
+
that are managed by, or on behalf of, the Licensor for the purpose of discussing
|
|
53
|
+
and improving the Work, but excluding communication that is conspicuously marked
|
|
54
|
+
or otherwise designated in writing by the copyright owner as "Not a
|
|
55
|
+
Contribution."
|
|
56
|
+
|
|
57
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
|
|
58
|
+
of whom a Contribution has been received by Licensor and subsequently
|
|
59
|
+
incorporated within the Work.
|
|
60
|
+
|
|
61
|
+
2. Grant of Copyright License. Subject to the terms and conditions of this
|
|
62
|
+
License, each Contributor hereby grants to You a perpetual, worldwide,
|
|
63
|
+
non-exclusive, no-charge, royalty-free, irrevocable copyright license to
|
|
64
|
+
reproduce, prepare Derivative Works of, publicly display, publicly perform,
|
|
65
|
+
sublicense, and distribute the Work and such Derivative Works in Source or
|
|
66
|
+
Object form.
|
|
67
|
+
|
|
68
|
+
3. Grant of Patent License. Subject to the terms and conditions of this License,
|
|
69
|
+
each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
|
|
70
|
+
no-charge, royalty-free, irrevocable (except as stated in this section) patent
|
|
71
|
+
license to make, have made, use, offer to sell, sell, import, and otherwise
|
|
72
|
+
transfer the Work, where such license applies only to those patent claims
|
|
73
|
+
licensable by such Contributor that are necessarily infringed by their
|
|
74
|
+
Contribution(s) alone or by combination of their Contribution(s) with the Work to
|
|
75
|
+
which such Contribution(s) was submitted. If You institute patent litigation
|
|
76
|
+
against any entity (including a cross-claim or counterclaim in a lawsuit)
|
|
77
|
+
alleging that the Work or a Contribution incorporated within the Work
|
|
78
|
+
constitutes direct or contributory patent infringement, then any patent licenses
|
|
79
|
+
granted to You under this License for that Work shall terminate as of the date
|
|
80
|
+
such litigation is filed.
|
|
81
|
+
|
|
82
|
+
4. Redistribution. You may reproduce and distribute copies of the Work or
|
|
83
|
+
Derivative Works thereof in any medium, with or without modifications, and in
|
|
84
|
+
Source or Object form, provided that You meet the following conditions:
|
|
85
|
+
|
|
86
|
+
(a) You must give any other recipients of the Work or Derivative Works a copy of
|
|
87
|
+
this License; and
|
|
88
|
+
|
|
89
|
+
(b) You must cause any modified files to carry prominent notices stating that
|
|
90
|
+
You changed the files; and
|
|
91
|
+
|
|
92
|
+
(c) You must retain, in the Source form of any Derivative Works that You
|
|
93
|
+
distribute, all copyright, patent, trademark, and attribution notices from the
|
|
94
|
+
Source form of the Work, excluding those notices that do not pertain to any part
|
|
95
|
+
of the Derivative Works; and
|
|
96
|
+
|
|
97
|
+
(d) If the Work includes a "NOTICE" text file as part of its distribution, then
|
|
98
|
+
any Derivative Works that You distribute must include a readable copy of the
|
|
99
|
+
attribution notices contained within such NOTICE file, excluding those notices
|
|
100
|
+
that do not pertain to any part of the Derivative Works, in at least one of the
|
|
101
|
+
following places: within a NOTICE text file distributed as part of the Derivative
|
|
102
|
+
Works; within the Source form or documentation, if provided along with the
|
|
103
|
+
Derivative Works; or, within a display generated by the Derivative Works, if and
|
|
104
|
+
wherever such third-party notices normally appear. The contents of the NOTICE
|
|
105
|
+
file are for informational purposes only and do not modify the License. You may
|
|
106
|
+
add Your own attribution notices within Derivative Works that You distribute,
|
|
107
|
+
alongside or as an addendum to the NOTICE text from the Work, provided that such
|
|
108
|
+
additional attribution notices cannot be construed as modifying the License.
|
|
109
|
+
|
|
110
|
+
You may add Your own copyright statement to Your modifications and may provide
|
|
111
|
+
additional or different license terms and conditions for use, reproduction, or
|
|
112
|
+
distribution of Your modifications, or for any such Derivative Works as a whole,
|
|
113
|
+
provided Your use, reproduction, and distribution of the Work otherwise complies
|
|
114
|
+
with the conditions stated in this License.
|
|
115
|
+
|
|
116
|
+
5. Submission of Contributions. Unless You explicitly state otherwise, any
|
|
117
|
+
Contribution intentionally submitted for inclusion in the Work by You to the
|
|
118
|
+
Licensor shall be under the terms and conditions of this License, without any
|
|
119
|
+
additional terms or conditions. Notwithstanding the above, nothing herein shall
|
|
120
|
+
supersede or modify the terms of any separate license agreement you may have
|
|
121
|
+
executed with Licensor regarding such Contributions.
|
|
122
|
+
|
|
123
|
+
6. Trademarks. This License does not grant permission to use the trade names,
|
|
124
|
+
trademarks, service marks, or product names of the Licensor, except as required
|
|
125
|
+
for reasonable and customary use in describing the origin of the Work and
|
|
126
|
+
reproducing the content of the NOTICE file.
|
|
127
|
+
|
|
128
|
+
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in
|
|
129
|
+
writing, Licensor provides the Work (and each Contributor provides its
|
|
130
|
+
Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
131
|
+
KIND, either express or implied, including, without limitation, any warranties or
|
|
132
|
+
conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
133
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
134
|
+
appropriateness of using or redistributing the Work and assume any risks
|
|
135
|
+
associated with Your exercise of permissions under this License.
|
|
136
|
+
|
|
137
|
+
8. Limitation of Liability. In no event and under no legal theory, whether in
|
|
138
|
+
tort (including negligence), contract, or otherwise, unless required by
|
|
139
|
+
applicable law (such as deliberate and grossly negligent acts) or agreed to in
|
|
140
|
+
writing, shall any Contributor be liable to You for damages, including any
|
|
141
|
+
direct, indirect, special, incidental, or consequential damages of any character
|
|
142
|
+
arising as a result of this License or out of the use or inability to use the
|
|
143
|
+
Work (including but not limited to damages for loss of goodwill, work stoppage,
|
|
144
|
+
computer failure or malfunction, or any and all other commercial damages or
|
|
145
|
+
losses), even if such Contributor has been advised of the possibility of such
|
|
146
|
+
damages.
|
|
147
|
+
|
|
148
|
+
9. Accepting Warranty or Additional Liability. While redistributing the Work or
|
|
149
|
+
Derivative Works thereof, You may choose to offer, and charge a fee for,
|
|
150
|
+
acceptance of support, warranty, indemnity, or other liability obligations and/or
|
|
151
|
+
rights consistent with this License. However, in accepting such obligations, You
|
|
152
|
+
may act only on Your own behalf and on Your sole responsibility, not on behalf of
|
|
153
|
+
any other Contributor, and only if You agree to indemnify, defend, and hold each
|
|
154
|
+
Contributor harmless for any liability incurred by, or claims asserted against,
|
|
155
|
+
such Contributor by reason of your accepting any such warranty or additional
|
|
156
|
+
liability.
|
|
157
|
+
|
|
158
|
+
END OF TERMS AND CONDITIONS
|
|
159
|
+
|
|
160
|
+
Copyright 2026 Flyfish Viewer
|
package/README.en.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# @file-viewer/renderer-archive
|
|
2
|
+
|
|
3
|
+
Standalone archive renderer package for Flyfish File Viewer. It reads ZIP, TAR, GZIP, RAR, 7z, and other archive directories with `libarchive.js` Worker + WASM, then extracts internal files only when the user opens them.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import FileViewer from '@file-viewer/vue3'
|
|
9
|
+
import { archiveRenderer } from '@file-viewer/renderer-archive'
|
|
10
|
+
|
|
11
|
+
const options = {
|
|
12
|
+
rendererMode: 'replace',
|
|
13
|
+
renderers: archiveRenderer,
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
You can also compose it with other renderer packages:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { archiveRenderer } from '@file-viewer/renderer-archive'
|
|
21
|
+
import { pdfRenderer } from '@file-viewer/renderer-pdf'
|
|
22
|
+
import { cadRenderer } from '@file-viewer/renderer-cad'
|
|
23
|
+
|
|
24
|
+
const options = {
|
|
25
|
+
rendererMode: 'replace',
|
|
26
|
+
renderers: [pdfRenderer, cadRenderer, archiveRenderer],
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Capabilities
|
|
31
|
+
|
|
32
|
+
- Previews ZIP, TAR, GZIP, RAR, 7z, and common archive directories.
|
|
33
|
+
- Uses `libarchive.js` Worker + WASM first to keep large archive parsing off the main thread.
|
|
34
|
+
- Falls back to ZIP / TAR / GZIP parsing when the Worker cannot be started, which helps mobile WebViews, local static servers, and private intranet deployments.
|
|
35
|
+
- Extracts internal files on demand, then delegates nested previews through `renderNestedBuffer` or the core dispatcher.
|
|
36
|
+
- Includes archive size limits, entry preview limits, worker timeout, IndexedDB cache, and single-entry download.
|
|
37
|
+
|
|
38
|
+
## Offline Assets
|
|
39
|
+
|
|
40
|
+
Default asset paths are:
|
|
41
|
+
|
|
42
|
+
- `vendor/libarchive/worker-bundle.js`
|
|
43
|
+
- `vendor/libarchive/libarchive.wasm`
|
|
44
|
+
|
|
45
|
+
For private deployments, override them with `options.archive.workerUrl` and `options.archive.wasmUrl`.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const options = {
|
|
49
|
+
archive: {
|
|
50
|
+
workerUrl: '/file-viewer/vendor/libarchive/worker-bundle.js',
|
|
51
|
+
wasmUrl: '/file-viewer/vendor/libarchive/libarchive.wasm',
|
|
52
|
+
cache: true,
|
|
53
|
+
workerTimeoutMs: 30000,
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Migration Note
|
|
59
|
+
|
|
60
|
+
The core package no longer bundles the archive renderer and no longer installs `libarchive.js` for the archive pipeline. ZIP/TAR/GZIP fallback, `jszip`, cache, and Worker logic are owned by this package; core may still temporarily retain `jszip` for the OFD vendor path until OFD is fully extracted. Install this renderer explicitly, or use `@file-viewer/preset-all`, when archive preview is required.
|
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# @file-viewer/renderer-archive
|
|
2
|
+
|
|
3
|
+
Flyfish File Viewer 的独立压缩包 renderer 包。它用 `libarchive.js` Worker + WASM 读取 RAR、7z、ZIP、TAR 等压缩包目录,并在点击内部文件时才按需解压、缓存和调用 File Viewer 的嵌套渲染能力。
|
|
4
|
+
|
|
5
|
+
## 用法
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import FileViewer from '@file-viewer/vue3'
|
|
9
|
+
import { archiveRenderer } from '@file-viewer/renderer-archive'
|
|
10
|
+
|
|
11
|
+
const options = {
|
|
12
|
+
rendererMode: 'replace',
|
|
13
|
+
renderers: archiveRenderer,
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
也可以和其他 renderer 一起组合:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { archiveRenderer } from '@file-viewer/renderer-archive'
|
|
21
|
+
import { pdfRenderer } from '@file-viewer/renderer-pdf'
|
|
22
|
+
import { cadRenderer } from '@file-viewer/renderer-cad'
|
|
23
|
+
|
|
24
|
+
const options = {
|
|
25
|
+
rendererMode: 'replace',
|
|
26
|
+
renderers: [pdfRenderer, cadRenderer, archiveRenderer],
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 能力边界
|
|
31
|
+
|
|
32
|
+
- 支持 ZIP、TAR、GZIP、RAR、7z 等常见压缩包目录预览。
|
|
33
|
+
- 优先使用 `libarchive.js` Worker + WASM,避免大压缩包阻塞主线程。
|
|
34
|
+
- Worker 不可用时自动回退到 ZIP / TAR / GZIP 兼容模式,适合手机 WebView、本地临时服务器和内网静态部署排障。
|
|
35
|
+
- 点击内部文件后才按需解压,并通过 `renderNestedBuffer` 或 core dispatcher 复用 PDF、Office、CAD、XMind、图片、代码等现有 renderer。
|
|
36
|
+
- 内置体积上限、单文件预览上限、Worker 超时、IndexedDB 缓存和下载入口,避免一次性把压缩包全部展开到内存。
|
|
37
|
+
|
|
38
|
+
## 离线资产
|
|
39
|
+
|
|
40
|
+
默认会从 viewer assets 下读取:
|
|
41
|
+
|
|
42
|
+
- `vendor/libarchive/worker-bundle.js`
|
|
43
|
+
- `vendor/libarchive/libarchive.wasm`
|
|
44
|
+
|
|
45
|
+
私有化部署时可以通过 `options.archive.workerUrl` 和 `options.archive.wasmUrl` 覆盖。
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const options = {
|
|
49
|
+
archive: {
|
|
50
|
+
workerUrl: '/file-viewer/vendor/libarchive/worker-bundle.js',
|
|
51
|
+
wasmUrl: '/file-viewer/vendor/libarchive/libarchive.wasm',
|
|
52
|
+
cache: true,
|
|
53
|
+
workerTimeoutMs: 30000,
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 迁移说明
|
|
59
|
+
|
|
60
|
+
`@file-viewer/core` 已不再内置 archive renderer,也不再为压缩包链路直接安装 `libarchive.js`。ZIP/TAR/GZIP fallback、`jszip`、缓存和 Worker 逻辑由本包维护;core 在 OFD 完全拆出前可能仍会因 OFD vendor 临时保留 `jszip`。需要压缩包预览时,请显式安装本包,或直接使用 `@file-viewer/preset-all` 聚合能力。
|
package/dist/archive.js
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { resolveFileViewerArchiveWasmUrl, resolveFileViewerArchiveWorkerUrl, } from '@file-viewer/core/assets';
|
|
2
|
+
import { disposeFileViewerRendered, } from '@file-viewer/core';
|
|
3
|
+
import { createArchiveCacheKey, flattenArchiveObject, formatArchiveBytes, getArchiveEntryExtension, } from './archiveShared.js';
|
|
4
|
+
import { readArchiveCache, writeArchiveCache } from './archiveCache.js';
|
|
5
|
+
import { loadArchiveEntriesWithoutWorker } from './archiveFallback.js';
|
|
6
|
+
const DEFAULT_MAX_ARCHIVE_SIZE = 320 * 1024 * 1024;
|
|
7
|
+
const DEFAULT_MAX_ENTRY_PREVIEW_SIZE = 64 * 1024 * 1024;
|
|
8
|
+
const DEFAULT_WORKER_TIMEOUT_MS = 30000;
|
|
9
|
+
const MAX_LISTED_ENTRIES = 5000;
|
|
10
|
+
const archiveStyle = `
|
|
11
|
+
.archive-shell,.archive-viewer{position:relative;box-sizing:border-box;height:100%;min-height:0;display:grid;grid-template-columns:minmax(280px,34%) minmax(0,1fr);background:#edf2f7;color:#172033;font-family:Aptos,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif}
|
|
12
|
+
.archive-shell *,.archive-viewer *{box-sizing:border-box}
|
|
13
|
+
.archive-sidebar{min-width:0;min-height:0;display:flex;flex-direction:column;gap:12px;padding:16px;border-right:1px solid rgba(23,32,51,.08);background:rgba(255,255,255,.72)}
|
|
14
|
+
.archive-head span,.archive-preview-toolbar span{color:#6c7c90;font-size:12px;font-weight:800;letter-spacing:0}
|
|
15
|
+
.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}
|
|
16
|
+
.archive-head p{margin:8px 0 0;color:#69798b;font-size:13px}
|
|
17
|
+
.archive-warning,.archive-info,.archive-error{border-radius:12px;padding:10px 12px;background:#fff7e8;color:#8a4b00;font-size:13px;line-height:1.5}
|
|
18
|
+
.archive-info{background:#ecfdf5;color:#166534}
|
|
19
|
+
.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}
|
|
20
|
+
.archive-list{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:7px;padding-right:4px}
|
|
21
|
+
.archive-entry{width:100%;min-height:58px;display:grid;grid-template-columns:42px minmax(0,1fr) 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}
|
|
22
|
+
.archive-entry:hover,.archive-entry.active{border-color:rgba(33,129,95,.28);box-shadow:0 10px 22px rgba(23,32,51,.08)}
|
|
23
|
+
.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}
|
|
24
|
+
.entry-copy{min-width:0}
|
|
25
|
+
.entry-copy strong,.entry-copy em{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
26
|
+
.entry-copy em,.archive-entry small{color:#718096;font-size:12px;font-style:normal}
|
|
27
|
+
.archive-preview{min-width:0;min-height:0;display:flex;flex-direction:column}
|
|
28
|
+
.archive-preview-toolbar{min-height:64px;display:flex;align-items:center;justify-content:space-between;gap:14px;padding:12px 16px;border-bottom:1px solid rgba(23,32,51,.08);background:rgba(255,255,255,.76)}
|
|
29
|
+
.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}
|
|
30
|
+
.archive-nested-target{position:relative;flex:1;min-height:0;overflow:auto}
|
|
31
|
+
.archive-nested-content{width:100%;height:100%;min-height:420px}
|
|
32
|
+
.archive-empty{height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:28px;text-align:center;color:#64748b}
|
|
33
|
+
.archive-empty strong{color:#172033;font-size:18px}
|
|
34
|
+
.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}
|
|
35
|
+
.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)}
|
|
36
|
+
.archive-state p{margin:4px 0 0;color:#64748b}
|
|
37
|
+
.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}
|
|
38
|
+
.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}
|
|
39
|
+
.archive-hidden{display:none!important}
|
|
40
|
+
.file-viewer[data-viewer-theme='dark'] .archive-shell,.file-viewer[data-viewer-theme='dark'] .archive-viewer{background:#101820;color:#e6edf3}
|
|
41
|
+
.file-viewer[data-viewer-theme='dark'] .archive-sidebar,.file-viewer[data-viewer-theme='dark'] .archive-preview-toolbar{border-color:rgba(139,148,158,.2);background:rgba(21,27,35,.82)}
|
|
42
|
+
.file-viewer[data-viewer-theme='dark'] .archive-entry,.file-viewer[data-viewer-theme='dark'] .archive-search,.file-viewer[data-viewer-theme='dark'] .archive-state>div{background:#151b23;color:#e6edf3;border-color:rgba(139,148,158,.2)}
|
|
43
|
+
.file-viewer[data-viewer-theme='dark'] .archive-empty strong{color:#f8fafc}
|
|
44
|
+
@media (prefers-color-scheme:dark){.file-viewer[data-viewer-theme='system'] .archive-shell,.file-viewer[data-viewer-theme='system'] .archive-viewer{background:#101820;color:#e6edf3}.file-viewer[data-viewer-theme='system'] .archive-sidebar,.file-viewer[data-viewer-theme='system'] .archive-preview-toolbar{border-color:rgba(139,148,158,.2);background:rgba(21,27,35,.82)}.file-viewer[data-viewer-theme='system'] .archive-entry,.file-viewer[data-viewer-theme='system'] .archive-search,.file-viewer[data-viewer-theme='system'] .archive-state>div{background:#151b23;color:#e6edf3;border-color:rgba(139,148,158,.2)}.file-viewer[data-viewer-theme='system'] .archive-empty strong{color:#f8fafc}}
|
|
45
|
+
@keyframes archive-spin{to{transform:rotate(360deg)}}
|
|
46
|
+
@media (max-width:860px){.archive-shell,.archive-viewer{grid-template-columns:1fr;grid-template-rows:minmax(220px,38%) minmax(0,1fr)}.archive-sidebar{border-right:0;border-bottom:1px solid rgba(23,32,51,.08)}}
|
|
47
|
+
`;
|
|
48
|
+
const createStyle = (documentRef) => {
|
|
49
|
+
const style = documentRef.createElement('style');
|
|
50
|
+
style.textContent = archiveStyle;
|
|
51
|
+
return style;
|
|
52
|
+
};
|
|
53
|
+
const createElement = (documentRef, tagName, className, text) => {
|
|
54
|
+
const element = documentRef.createElement(tagName);
|
|
55
|
+
if (className) {
|
|
56
|
+
element.className = className;
|
|
57
|
+
}
|
|
58
|
+
if (text !== undefined) {
|
|
59
|
+
element.textContent = text;
|
|
60
|
+
}
|
|
61
|
+
return element;
|
|
62
|
+
};
|
|
63
|
+
const normalizeWorkerError = (reason) => {
|
|
64
|
+
if (reason instanceof Error) {
|
|
65
|
+
return reason.message;
|
|
66
|
+
}
|
|
67
|
+
return typeof reason === 'string' ? reason : JSON.stringify(reason);
|
|
68
|
+
};
|
|
69
|
+
const withTimeout = async (promise, timeout, message, targetWindow) => {
|
|
70
|
+
let timer = 0;
|
|
71
|
+
const timerWindow = targetWindow || (typeof window !== 'undefined' ? window : undefined);
|
|
72
|
+
try {
|
|
73
|
+
return await Promise.race([
|
|
74
|
+
promise,
|
|
75
|
+
new Promise((_, reject) => {
|
|
76
|
+
timer = (timerWindow === null || timerWindow === void 0 ? void 0 : timerWindow.setTimeout)
|
|
77
|
+
? timerWindow.setTimeout(() => reject(new Error(message)), timeout)
|
|
78
|
+
: setTimeout(() => reject(new Error(message)), timeout);
|
|
79
|
+
}),
|
|
80
|
+
]);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
if (timerWindow === null || timerWindow === void 0 ? void 0 : timerWindow.clearTimeout) {
|
|
84
|
+
timerWindow.clearTimeout(timer);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const getDocumentBaseUrl = (documentRef) => {
|
|
92
|
+
return documentRef.baseURI ||
|
|
93
|
+
documentRef.URL ||
|
|
94
|
+
'http://localhost/';
|
|
95
|
+
};
|
|
96
|
+
const getWorkerConstructor = (documentRef) => {
|
|
97
|
+
var _a;
|
|
98
|
+
const WorkerCtor = ((_a = documentRef.defaultView) === null || _a === void 0 ? void 0 : _a.Worker) ||
|
|
99
|
+
(typeof Worker !== 'undefined' ? Worker : undefined);
|
|
100
|
+
if (!WorkerCtor) {
|
|
101
|
+
throw new Error('当前浏览器不支持 Web Worker');
|
|
102
|
+
}
|
|
103
|
+
return WorkerCtor;
|
|
104
|
+
};
|
|
105
|
+
const getFileConstructor = (documentRef) => {
|
|
106
|
+
var _a;
|
|
107
|
+
return (((_a = documentRef.defaultView) === null || _a === void 0 ? void 0 : _a.File) ||
|
|
108
|
+
(typeof File !== 'undefined' ? File : undefined));
|
|
109
|
+
};
|
|
110
|
+
const createArchiveFile = (documentRef, buffer, filename) => {
|
|
111
|
+
const FileCtor = getFileConstructor(documentRef);
|
|
112
|
+
if (FileCtor) {
|
|
113
|
+
return new FileCtor([buffer], filename || 'archive.bin', {
|
|
114
|
+
type: 'application/octet-stream',
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return Object.assign(new Blob([buffer], { type: 'application/octet-stream' }), {
|
|
118
|
+
name: filename || 'archive.bin',
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
const probeWorkerUrl = async (url) => {
|
|
122
|
+
try {
|
|
123
|
+
const response = await fetch(url, { method: 'HEAD', cache: 'no-cache' });
|
|
124
|
+
const contentType = response.headers.get('content-type') || '';
|
|
125
|
+
if (response.ok && /javascript|ecmascript|octet-stream/i.test(contentType)) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
if (response.status && response.status !== 405) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Some local servers do not support HEAD; continue with a tiny GET probe.
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const response = await fetch(url, {
|
|
137
|
+
method: 'GET',
|
|
138
|
+
cache: 'no-cache',
|
|
139
|
+
headers: {
|
|
140
|
+
Range: 'bytes=0-0',
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
const contentType = response.headers.get('content-type') || '';
|
|
144
|
+
return response.ok && /javascript|ecmascript|octet-stream/i.test(contentType);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const patchLibarchiveWorkerSource = (source, wasmUrl) => {
|
|
151
|
+
const wasmLiteral = JSON.stringify(wasmUrl);
|
|
152
|
+
return source.replace(/new URL\((['"])libarchive\.wasm\1\s*,\s*import\.meta\.url\)\.href/g, wasmLiteral);
|
|
153
|
+
};
|
|
154
|
+
const prepareWorkerUrl = async (candidate, objectUrls) => {
|
|
155
|
+
if (!candidate.wasmUrl) {
|
|
156
|
+
return candidate.workerUrl;
|
|
157
|
+
}
|
|
158
|
+
const response = await fetch(candidate.workerUrl, { cache: 'no-cache' });
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
throw new Error(`无法读取 libarchive Worker: ${response.status}`);
|
|
161
|
+
}
|
|
162
|
+
const source = await response.text();
|
|
163
|
+
const workerUrl = URL.createObjectURL(new Blob([
|
|
164
|
+
patchLibarchiveWorkerSource(source, candidate.wasmUrl),
|
|
165
|
+
], { type: 'application/javascript' }));
|
|
166
|
+
objectUrls.push(workerUrl);
|
|
167
|
+
return workerUrl;
|
|
168
|
+
};
|
|
169
|
+
const resolveWorkerCandidates = async (documentRef, options) => {
|
|
170
|
+
const candidates = [];
|
|
171
|
+
const baseUrl = getDocumentBaseUrl(documentRef);
|
|
172
|
+
const wasmUrl = (options === null || options === void 0 ? void 0 : options.wasmUrl)
|
|
173
|
+
? resolveFileViewerArchiveWasmUrl(options, '')
|
|
174
|
+
: undefined;
|
|
175
|
+
if (options === null || options === void 0 ? void 0 : options.workerUrl) {
|
|
176
|
+
candidates.push({
|
|
177
|
+
label: '自定义 libarchive Worker',
|
|
178
|
+
workerUrl: resolveFileViewerArchiveWorkerUrl(options, baseUrl),
|
|
179
|
+
wasmUrl,
|
|
180
|
+
});
|
|
181
|
+
return candidates;
|
|
182
|
+
}
|
|
183
|
+
const publicWorkerUrl = resolveFileViewerArchiveWorkerUrl(undefined, baseUrl);
|
|
184
|
+
if (await probeWorkerUrl(publicWorkerUrl)) {
|
|
185
|
+
candidates.push({
|
|
186
|
+
label: '静态 libarchive Worker',
|
|
187
|
+
workerUrl: publicWorkerUrl,
|
|
188
|
+
wasmUrl,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return candidates;
|
|
192
|
+
};
|
|
193
|
+
const buildNestedOptions = (context, archiveOptions) => ({
|
|
194
|
+
...((context === null || context === void 0 ? void 0 : context.options) || {}),
|
|
195
|
+
archive: archiveOptions,
|
|
196
|
+
});
|
|
197
|
+
const renderNestedWithCoreFallback = async (buffer, type, target, context) => {
|
|
198
|
+
const { fileViewerCoreRendererDispatcher } = await import('@file-viewer/core');
|
|
199
|
+
const handler = fileViewerCoreRendererDispatcher.resolve(type);
|
|
200
|
+
if (!handler) {
|
|
201
|
+
target.textContent = `不支持.${type}格式的在线预览,请下载后预览或转换为支持的格式`;
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
return handler(buffer, target, type, {
|
|
205
|
+
...context,
|
|
206
|
+
renderNestedBuffer: (context === null || context === void 0 ? void 0 : context.renderNestedBuffer) || renderNestedWithCoreFallback,
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
export default async function renderArchive(buffer, target, _type, context) {
|
|
210
|
+
var _a;
|
|
211
|
+
const documentRef = target.ownerDocument;
|
|
212
|
+
const targetWindow = documentRef.defaultView || null;
|
|
213
|
+
const archiveOptions = (_a = context === null || context === void 0 ? void 0 : context.options) === null || _a === void 0 ? void 0 : _a.archive;
|
|
214
|
+
const filename = (context === null || context === void 0 ? void 0 : context.filename) || 'archive.bin';
|
|
215
|
+
const maxArchiveSize = (archiveOptions === null || archiveOptions === void 0 ? void 0 : archiveOptions.maxArchiveSize) || DEFAULT_MAX_ARCHIVE_SIZE;
|
|
216
|
+
const maxEntryPreviewSize = (archiveOptions === null || archiveOptions === void 0 ? void 0 : archiveOptions.maxEntryPreviewSize) || DEFAULT_MAX_ENTRY_PREVIEW_SIZE;
|
|
217
|
+
const cacheEnabled = (archiveOptions === null || archiveOptions === void 0 ? void 0 : archiveOptions.cache) !== false;
|
|
218
|
+
const workerTimeoutMs = (archiveOptions === null || archiveOptions === void 0 ? void 0 : archiveOptions.workerTimeoutMs) || DEFAULT_WORKER_TIMEOUT_MS;
|
|
219
|
+
const objectUrls = [];
|
|
220
|
+
const cleanups = [];
|
|
221
|
+
let archiveReader = null;
|
|
222
|
+
let entries = [];
|
|
223
|
+
let selectedEntry = null;
|
|
224
|
+
let nestedRendered;
|
|
225
|
+
let loading = false;
|
|
226
|
+
let loadingText = '正在读取压缩包目录...';
|
|
227
|
+
let loadingHint = '大文件会在 Worker 中解析,避免阻塞主线程。';
|
|
228
|
+
let errorText = '';
|
|
229
|
+
let archiveNotice = '';
|
|
230
|
+
let encrypted = null;
|
|
231
|
+
let filterText = '';
|
|
232
|
+
const style = createStyle(documentRef);
|
|
233
|
+
const root = createElement(documentRef, 'section', 'archive-shell archive-viewer');
|
|
234
|
+
const sidebar = createElement(documentRef, 'aside', 'archive-sidebar');
|
|
235
|
+
const head = createElement(documentRef, 'div', 'archive-head');
|
|
236
|
+
const badge = createElement(documentRef, 'span', undefined, 'ARCHIVE');
|
|
237
|
+
const title = createElement(documentRef, 'strong', undefined, filename);
|
|
238
|
+
const stats = createElement(documentRef, 'p');
|
|
239
|
+
head.append(badge, title, stats);
|
|
240
|
+
const warning = createElement(documentRef, 'div', 'archive-warning');
|
|
241
|
+
const info = createElement(documentRef, 'div', 'archive-info');
|
|
242
|
+
const search = createElement(documentRef, 'input', 'archive-search');
|
|
243
|
+
search.type = 'search';
|
|
244
|
+
search.placeholder = '筛选压缩包内文件';
|
|
245
|
+
const list = createElement(documentRef, 'div', 'archive-list');
|
|
246
|
+
list.setAttribute('role', 'list');
|
|
247
|
+
sidebar.append(head, warning, info, search, list);
|
|
248
|
+
const preview = createElement(documentRef, 'main', 'archive-preview');
|
|
249
|
+
const toolbar = createElement(documentRef, 'div', 'archive-preview-toolbar');
|
|
250
|
+
const toolbarTitle = createElement(documentRef, 'div');
|
|
251
|
+
toolbarTitle.append(createElement(documentRef, 'span', undefined, '压缩包内预览'), createElement(documentRef, 'strong', undefined, '请选择一个文件'));
|
|
252
|
+
const downloadButton = createElement(documentRef, 'button', undefined, '下载文件');
|
|
253
|
+
downloadButton.type = 'button';
|
|
254
|
+
toolbar.append(toolbarTitle, downloadButton);
|
|
255
|
+
const nestedTarget = createElement(documentRef, 'div', 'archive-nested-target');
|
|
256
|
+
preview.append(toolbar, nestedTarget);
|
|
257
|
+
root.append(sidebar, preview);
|
|
258
|
+
const state = createElement(documentRef, 'div', 'archive-state');
|
|
259
|
+
const stateContent = createElement(documentRef, 'div');
|
|
260
|
+
const spinner = createElement(documentRef, 'span', 'archive-spinner');
|
|
261
|
+
const stateCopy = createElement(documentRef, 'div');
|
|
262
|
+
const stateTitle = createElement(documentRef, 'strong', undefined, loadingText);
|
|
263
|
+
const stateHint = createElement(documentRef, 'p', undefined, loadingHint);
|
|
264
|
+
stateCopy.append(stateTitle, stateHint);
|
|
265
|
+
stateContent.append(spinner, stateCopy);
|
|
266
|
+
state.append(stateContent);
|
|
267
|
+
root.append(state);
|
|
268
|
+
const error = createElement(documentRef, 'div', 'archive-error');
|
|
269
|
+
const errorTitle = createElement(documentRef, 'strong', undefined, '压缩包预览提示');
|
|
270
|
+
const errorMessage = createElement(documentRef, 'p');
|
|
271
|
+
error.append(errorTitle, errorMessage);
|
|
272
|
+
root.append(error);
|
|
273
|
+
target.replaceChildren(style, root);
|
|
274
|
+
const listen = (element, event, listener) => {
|
|
275
|
+
element.addEventListener(event, listener);
|
|
276
|
+
cleanups.push(() => element.removeEventListener(event, listener));
|
|
277
|
+
};
|
|
278
|
+
const getArchiveStats = () => {
|
|
279
|
+
const totalSize = entries.reduce((sum, entry) => sum + entry.size, 0);
|
|
280
|
+
const previewableCount = entries.filter(entry => entry.previewable).length;
|
|
281
|
+
return {
|
|
282
|
+
count: entries.length,
|
|
283
|
+
totalSize,
|
|
284
|
+
previewableCount,
|
|
285
|
+
};
|
|
286
|
+
};
|
|
287
|
+
const getFilteredEntries = () => {
|
|
288
|
+
const keyword = filterText.trim().toLowerCase();
|
|
289
|
+
const source = keyword
|
|
290
|
+
? entries.filter(entry => entry.path.toLowerCase().includes(keyword))
|
|
291
|
+
: entries;
|
|
292
|
+
return source.slice(0, MAX_LISTED_ENTRIES);
|
|
293
|
+
};
|
|
294
|
+
const clearNestedPreview = async () => {
|
|
295
|
+
await disposeFileViewerRendered(nestedRendered);
|
|
296
|
+
nestedRendered = undefined;
|
|
297
|
+
nestedTarget.replaceChildren();
|
|
298
|
+
};
|
|
299
|
+
const closeArchive = async () => {
|
|
300
|
+
var _a;
|
|
301
|
+
await ((_a = archiveReader === null || archiveReader === void 0 ? void 0 : archiveReader.close) === null || _a === void 0 ? void 0 : _a.call(archiveReader));
|
|
302
|
+
archiveReader = null;
|
|
303
|
+
};
|
|
304
|
+
const syncState = () => {
|
|
305
|
+
const archiveStats = getArchiveStats();
|
|
306
|
+
stats.textContent = `${archiveStats.count} 个文件 · ${formatArchiveBytes(archiveStats.totalSize)} · ${archiveStats.previewableCount} 个可直接预览`;
|
|
307
|
+
warning.textContent = '检测到加密内容,当前在线预览不接收密码,建议下载后本地解压。';
|
|
308
|
+
warning.classList.toggle('archive-hidden', !encrypted);
|
|
309
|
+
info.textContent = archiveNotice;
|
|
310
|
+
info.classList.toggle('archive-hidden', !archiveNotice);
|
|
311
|
+
state.classList.toggle('archive-hidden', !loading);
|
|
312
|
+
stateTitle.textContent = loadingText;
|
|
313
|
+
stateHint.textContent = loadingHint;
|
|
314
|
+
error.classList.toggle('archive-hidden', !errorText);
|
|
315
|
+
errorMessage.textContent = errorText;
|
|
316
|
+
downloadButton.hidden = !selectedEntry;
|
|
317
|
+
const activeTitle = toolbarTitle.querySelector('strong');
|
|
318
|
+
if (activeTitle) {
|
|
319
|
+
activeTitle.textContent = (selectedEntry === null || selectedEntry === void 0 ? void 0 : selectedEntry.name) || '请选择一个文件';
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
const renderEmptyState = () => {
|
|
323
|
+
if (selectedEntry || loading || nestedTarget.childElementCount) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const empty = createElement(documentRef, 'div', 'archive-empty');
|
|
327
|
+
empty.append(createElement(documentRef, 'strong', undefined, '选择左侧文件即可预览'), createElement(documentRef, 'p', undefined, '压缩包只读取目录;文件内容会在点击后按需解压,并在体积允许时缓存到 IndexedDB。'));
|
|
328
|
+
nestedTarget.replaceChildren(empty);
|
|
329
|
+
};
|
|
330
|
+
const renderEntryList = () => {
|
|
331
|
+
list.replaceChildren();
|
|
332
|
+
getFilteredEntries().forEach(entry => {
|
|
333
|
+
const button = createElement(documentRef, 'button', 'archive-entry');
|
|
334
|
+
button.type = 'button';
|
|
335
|
+
button.style.setProperty('--entry-depth', String(entry.depth));
|
|
336
|
+
button.classList.toggle('active', (selectedEntry === null || selectedEntry === void 0 ? void 0 : selectedEntry.id) === entry.id);
|
|
337
|
+
const icon = createElement(documentRef, 'span', 'entry-ext', entry.extension || 'file');
|
|
338
|
+
const copy = createElement(documentRef, 'span', 'entry-copy');
|
|
339
|
+
copy.append(createElement(documentRef, 'strong', undefined, entry.name), createElement(documentRef, 'em', undefined, entry.path));
|
|
340
|
+
button.append(icon, copy, createElement(documentRef, 'small', undefined, formatArchiveBytes(entry.size)));
|
|
341
|
+
button.addEventListener('click', () => {
|
|
342
|
+
void previewEntry(entry);
|
|
343
|
+
});
|
|
344
|
+
list.append(button);
|
|
345
|
+
});
|
|
346
|
+
};
|
|
347
|
+
const setLoading = (next, text, hint) => {
|
|
348
|
+
loading = next;
|
|
349
|
+
if (text) {
|
|
350
|
+
loadingText = text;
|
|
351
|
+
}
|
|
352
|
+
if (hint) {
|
|
353
|
+
loadingHint = hint;
|
|
354
|
+
}
|
|
355
|
+
syncState();
|
|
356
|
+
renderEmptyState();
|
|
357
|
+
};
|
|
358
|
+
const setError = (message) => {
|
|
359
|
+
errorText = message;
|
|
360
|
+
syncState();
|
|
361
|
+
};
|
|
362
|
+
const terminateWorkers = (workers) => {
|
|
363
|
+
workers.forEach(worker => worker.terminate());
|
|
364
|
+
workers.length = 0;
|
|
365
|
+
};
|
|
366
|
+
const tryOpenArchiveWithWorker = async (Archive, candidate) => {
|
|
367
|
+
const createdWorkers = [];
|
|
368
|
+
const workerUrl = await prepareWorkerUrl(candidate, objectUrls);
|
|
369
|
+
const WorkerCtor = getWorkerConstructor(documentRef);
|
|
370
|
+
try {
|
|
371
|
+
Archive.init({
|
|
372
|
+
getWorker: () => {
|
|
373
|
+
const worker = new WorkerCtor(workerUrl, { type: 'module' });
|
|
374
|
+
createdWorkers.push(worker);
|
|
375
|
+
return worker;
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
setLoading(true, `正在初始化${candidate.label}...`, '如果当前服务器没有正确发布 Worker/WASM,会自动切换兼容模式。');
|
|
379
|
+
const archiveFile = createArchiveFile(documentRef, buffer, filename);
|
|
380
|
+
const archive = await withTimeout(Archive.open(archiveFile), workerTimeoutMs, `${candidate.label} 初始化超时`, targetWindow);
|
|
381
|
+
archiveReader = archive;
|
|
382
|
+
encrypted = await withTimeout(archive.hasEncryptedData(), workerTimeoutMs, `${candidate.label} 加密检测超时`, targetWindow).catch(() => null);
|
|
383
|
+
setLoading(true, '正在读取压缩包目录...', '目录读取完成后,点击内部文件才会按需解压。');
|
|
384
|
+
const fileTree = await withTimeout(archive.getFilesObject(), workerTimeoutMs, `${candidate.label} 读取目录超时`, targetWindow);
|
|
385
|
+
entries = flattenArchiveObject(fileTree)
|
|
386
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
387
|
+
syncState();
|
|
388
|
+
renderEntryList();
|
|
389
|
+
renderEmptyState();
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
catch (reason) {
|
|
393
|
+
if (!archiveReader) {
|
|
394
|
+
terminateWorkers(createdWorkers);
|
|
395
|
+
}
|
|
396
|
+
throw reason;
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
const tryOpenArchiveWithFallback = async () => {
|
|
400
|
+
setLoading(true, 'Worker 不可用,正在切换 ZIP/TAR 兼容模式...', '兼容模式无需额外静态 Worker,适合手机 WebView 或本地临时服务器。');
|
|
401
|
+
const fallbackEntries = await loadArchiveEntriesWithoutWorker(buffer, filename);
|
|
402
|
+
if (!fallbackEntries) {
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
entries = fallbackEntries.sort((left, right) => left.path.localeCompare(right.path));
|
|
406
|
+
encrypted = null;
|
|
407
|
+
archiveNotice = '当前环境的 libarchive Worker 未能启动,已自动切换到 ZIP/TAR/GZIP 兼容模式。RAR、7z 等格式仍建议发布 vendor/libarchive/worker-bundle.js 与 libarchive.wasm。';
|
|
408
|
+
syncState();
|
|
409
|
+
renderEntryList();
|
|
410
|
+
renderEmptyState();
|
|
411
|
+
return true;
|
|
412
|
+
};
|
|
413
|
+
const openArchive = async () => {
|
|
414
|
+
if (buffer.byteLength > maxArchiveSize) {
|
|
415
|
+
setError(`压缩包体积 ${formatArchiveBytes(buffer.byteLength)} 超过安全上限 ${formatArchiveBytes(maxArchiveSize)},请下载后在本地解压。`);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
setLoading(true, '正在初始化压缩包解析 Worker...', '大文件会在 Worker 中解析,避免阻塞主线程。');
|
|
419
|
+
setError('');
|
|
420
|
+
archiveNotice = '';
|
|
421
|
+
try {
|
|
422
|
+
const [{ Archive }, candidates] = await Promise.all([
|
|
423
|
+
import('libarchive.js'),
|
|
424
|
+
resolveWorkerCandidates(documentRef, archiveOptions),
|
|
425
|
+
]);
|
|
426
|
+
const errors = [];
|
|
427
|
+
for (const candidate of candidates) {
|
|
428
|
+
try {
|
|
429
|
+
await closeArchive();
|
|
430
|
+
await tryOpenArchiveWithWorker(Archive, candidate);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
catch (reason) {
|
|
434
|
+
errors.push(`${candidate.label}: ${normalizeWorkerError(reason)}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
await closeArchive();
|
|
438
|
+
if (await tryOpenArchiveWithFallback()) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
throw new Error(errors.join(';') || '压缩包 Worker 初始化失败');
|
|
442
|
+
}
|
|
443
|
+
catch (nextError) {
|
|
444
|
+
console.error(nextError);
|
|
445
|
+
setError(nextError instanceof Error ? nextError.message : String(nextError));
|
|
446
|
+
}
|
|
447
|
+
finally {
|
|
448
|
+
setLoading(false);
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
const renderEntryBuffer = async (entry, entryBuffer) => {
|
|
452
|
+
await clearNestedPreview();
|
|
453
|
+
const child = createElement(documentRef, 'div', 'archive-nested-content');
|
|
454
|
+
nestedTarget.append(child);
|
|
455
|
+
const nestedContext = {
|
|
456
|
+
...context,
|
|
457
|
+
filename: entry.name,
|
|
458
|
+
options: buildNestedOptions(context, archiveOptions),
|
|
459
|
+
};
|
|
460
|
+
nestedRendered = (context === null || context === void 0 ? void 0 : context.renderNestedBuffer)
|
|
461
|
+
? await context.renderNestedBuffer(entryBuffer, entry.extension, child, nestedContext)
|
|
462
|
+
: await renderNestedWithCoreFallback(entryBuffer, entry.extension, child, nestedContext);
|
|
463
|
+
};
|
|
464
|
+
const extractEntryBuffer = async (entry) => {
|
|
465
|
+
const cacheKey = createArchiveCacheKey(filename, buffer.byteLength, entry);
|
|
466
|
+
if (cacheEnabled) {
|
|
467
|
+
const cached = await readArchiveCache(cacheKey);
|
|
468
|
+
if (cached) {
|
|
469
|
+
return cached.buffer;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const file = await entry.compressedFile.extract();
|
|
473
|
+
const entryBuffer = await file.arrayBuffer();
|
|
474
|
+
if (cacheEnabled) {
|
|
475
|
+
await writeArchiveCache({
|
|
476
|
+
key: cacheKey,
|
|
477
|
+
filename: entry.name,
|
|
478
|
+
size: entryBuffer.byteLength,
|
|
479
|
+
updatedAt: Date.now(),
|
|
480
|
+
buffer: entryBuffer,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
return entryBuffer;
|
|
484
|
+
};
|
|
485
|
+
async function previewEntry(entry) {
|
|
486
|
+
selectedEntry = entry;
|
|
487
|
+
renderEntryList();
|
|
488
|
+
syncState();
|
|
489
|
+
if (entry.size > maxEntryPreviewSize) {
|
|
490
|
+
setError(`压缩包内文件 ${entry.name} 体积 ${formatArchiveBytes(entry.size)} 超过预览上限 ${formatArchiveBytes(maxEntryPreviewSize)}。`);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
setLoading(true, `正在按需解压 ${entry.name}...`);
|
|
494
|
+
setError('');
|
|
495
|
+
try {
|
|
496
|
+
const entryBuffer = await extractEntryBuffer(entry);
|
|
497
|
+
setLoading(true, `正在渲染 ${entry.name}...`);
|
|
498
|
+
await renderEntryBuffer(entry, entryBuffer);
|
|
499
|
+
}
|
|
500
|
+
catch (nextError) {
|
|
501
|
+
console.error(nextError);
|
|
502
|
+
setError(nextError instanceof Error ? nextError.message : String(nextError));
|
|
503
|
+
}
|
|
504
|
+
finally {
|
|
505
|
+
setLoading(false);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
const downloadEntry = async (entry) => {
|
|
509
|
+
setLoading(true, `正在导出 ${entry.name}...`);
|
|
510
|
+
try {
|
|
511
|
+
const entryBuffer = await extractEntryBuffer(entry);
|
|
512
|
+
const url = URL.createObjectURL(new Blob([entryBuffer]));
|
|
513
|
+
objectUrls.push(url);
|
|
514
|
+
const link = documentRef.createElement('a');
|
|
515
|
+
link.href = url;
|
|
516
|
+
link.download = entry.name;
|
|
517
|
+
documentRef.body.append(link);
|
|
518
|
+
link.click();
|
|
519
|
+
link.remove();
|
|
520
|
+
}
|
|
521
|
+
finally {
|
|
522
|
+
setLoading(false);
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
listen(search, 'input', () => {
|
|
526
|
+
filterText = search.value;
|
|
527
|
+
renderEntryList();
|
|
528
|
+
});
|
|
529
|
+
listen(downloadButton, 'click', () => {
|
|
530
|
+
if (selectedEntry) {
|
|
531
|
+
void downloadEntry(selectedEntry);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
syncState();
|
|
535
|
+
renderEmptyState();
|
|
536
|
+
void openArchive();
|
|
537
|
+
return {
|
|
538
|
+
$el: root,
|
|
539
|
+
async unmount() {
|
|
540
|
+
cleanups.splice(0).forEach(cleanup => cleanup());
|
|
541
|
+
await clearNestedPreview();
|
|
542
|
+
await closeArchive();
|
|
543
|
+
objectUrls.forEach(url => URL.revokeObjectURL(url));
|
|
544
|
+
target.replaceChildren();
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface CachedArchiveEntry {
|
|
2
|
+
key: string;
|
|
3
|
+
filename: string;
|
|
4
|
+
size: number;
|
|
5
|
+
updatedAt: number;
|
|
6
|
+
buffer: ArrayBuffer;
|
|
7
|
+
}
|
|
8
|
+
export declare const readArchiveCache: (key: string) => Promise<CachedArchiveEntry | null>;
|
|
9
|
+
export declare const writeArchiveCache: (entry: CachedArchiveEntry) => Promise<void>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const DB_NAME = 'flyfish-file-viewer-cache';
|
|
2
|
+
const STORE_NAME = 'archiveEntries';
|
|
3
|
+
const DB_VERSION = 1;
|
|
4
|
+
const MAX_CACHE_ENTRY_BYTES = 24 * 1024 * 1024;
|
|
5
|
+
const MAX_CACHE_TOTAL_BYTES = 96 * 1024 * 1024;
|
|
6
|
+
let dbPromise = null;
|
|
7
|
+
const canUseIndexedDB = () => typeof indexedDB !== 'undefined';
|
|
8
|
+
const openCacheDb = () => {
|
|
9
|
+
if (!canUseIndexedDB()) {
|
|
10
|
+
return Promise.reject(new Error('IndexedDB 不可用'));
|
|
11
|
+
}
|
|
12
|
+
if (dbPromise) {
|
|
13
|
+
return dbPromise;
|
|
14
|
+
}
|
|
15
|
+
dbPromise = new Promise((resolve, reject) => {
|
|
16
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
17
|
+
request.onupgradeneeded = () => {
|
|
18
|
+
const db = request.result;
|
|
19
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
20
|
+
const store = db.createObjectStore(STORE_NAME, { keyPath: 'key' });
|
|
21
|
+
store.createIndex('updatedAt', 'updatedAt');
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
request.onsuccess = () => resolve(request.result);
|
|
25
|
+
request.onerror = () => reject(request.error);
|
|
26
|
+
});
|
|
27
|
+
return dbPromise;
|
|
28
|
+
};
|
|
29
|
+
const runStore = async (mode, runner) => {
|
|
30
|
+
const db = await openCacheDb();
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const transaction = db.transaction(STORE_NAME, mode);
|
|
33
|
+
const request = runner(transaction.objectStore(STORE_NAME));
|
|
34
|
+
request.onsuccess = () => resolve(request.result);
|
|
35
|
+
request.onerror = () => reject(request.error);
|
|
36
|
+
transaction.onerror = () => reject(transaction.error);
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
export const readArchiveCache = async (key) => {
|
|
40
|
+
try {
|
|
41
|
+
const cached = await runStore('readonly', store => store.get(key));
|
|
42
|
+
return cached || null;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const pruneArchiveCache = async () => {
|
|
49
|
+
try {
|
|
50
|
+
const db = await openCacheDb();
|
|
51
|
+
await new Promise((resolve, reject) => {
|
|
52
|
+
const transaction = db.transaction(STORE_NAME, 'readwrite');
|
|
53
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
54
|
+
const index = store.index('updatedAt');
|
|
55
|
+
const entries = [];
|
|
56
|
+
let total = 0;
|
|
57
|
+
index.openCursor().onsuccess = event => {
|
|
58
|
+
const cursor = event.target.result;
|
|
59
|
+
if (!cursor) {
|
|
60
|
+
while (total > MAX_CACHE_TOTAL_BYTES && entries.length) {
|
|
61
|
+
const entry = entries.shift();
|
|
62
|
+
if (entry) {
|
|
63
|
+
total -= entry.size;
|
|
64
|
+
store.delete(entry.key);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const value = cursor.value;
|
|
70
|
+
total += value.size || 0;
|
|
71
|
+
entries.push({ key: value.key, size: value.size || 0 });
|
|
72
|
+
cursor.continue();
|
|
73
|
+
};
|
|
74
|
+
transaction.oncomplete = () => resolve();
|
|
75
|
+
transaction.onerror = () => reject(transaction.error);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Cache pruning is a best-effort optimization and must not block preview.
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
export const writeArchiveCache = async (entry) => {
|
|
83
|
+
if (entry.size > MAX_CACHE_ENTRY_BYTES) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
await runStore('readwrite', store => store.put({
|
|
88
|
+
...entry,
|
|
89
|
+
updatedAt: Date.now(),
|
|
90
|
+
}));
|
|
91
|
+
await pruneArchiveCache();
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Quota, private-mode, or browser policy errors simply disable cache writes.
|
|
95
|
+
}
|
|
96
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ArchiveEntryView } from './archiveShared.js';
|
|
2
|
+
/**
|
|
3
|
+
* Worker fallback for constrained browsers, temporary local servers, and
|
|
4
|
+
* mobile WebViews. The main libarchive path still covers broader formats;
|
|
5
|
+
* this covers common ZIP/TAR/GZIP archives without an extra static Worker.
|
|
6
|
+
*/
|
|
7
|
+
export declare const loadArchiveEntriesWithoutWorker: (data: ArrayBuffer, filename: string) => Promise<ArchiveEntryView[] | null>;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { getArchiveEntryExtension, isPreviewableArchiveEntry, } from './archiveShared.js';
|
|
2
|
+
const ZIP_LIKE_EXTENSIONS = new Set(['zip', 'zipx', 'jar', 'war', 'ear', 'apk', 'cbz']);
|
|
3
|
+
const TAR_LIKE_EXTENSIONS = new Set(['tar', 'tgz', 'gz', 'gzip']);
|
|
4
|
+
const TAR_BLOCK_SIZE = 512;
|
|
5
|
+
const toArrayBuffer = (bytes) => {
|
|
6
|
+
const output = new Uint8Array(bytes.byteLength);
|
|
7
|
+
output.set(bytes);
|
|
8
|
+
return output.buffer;
|
|
9
|
+
};
|
|
10
|
+
const decompressBytes = async (bytes, format) => {
|
|
11
|
+
if (typeof DecompressionStream === 'undefined') {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const stream = new Blob([toArrayBuffer(bytes)])
|
|
15
|
+
.stream()
|
|
16
|
+
.pipeThrough(new DecompressionStream(format));
|
|
17
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
18
|
+
};
|
|
19
|
+
const normalizeArchivePath = (path) => {
|
|
20
|
+
return path.replace(/^\/+/, '').replace(/\\/g, '/');
|
|
21
|
+
};
|
|
22
|
+
const getPathName = (path) => {
|
|
23
|
+
const parts = normalizeArchivePath(path).split('/');
|
|
24
|
+
return parts[parts.length - 1] || path;
|
|
25
|
+
};
|
|
26
|
+
const getPathDepth = (path) => {
|
|
27
|
+
return Math.max(0, normalizeArchivePath(path).split('/').length - 1);
|
|
28
|
+
};
|
|
29
|
+
const createEntryView = (source) => {
|
|
30
|
+
const path = normalizeArchivePath(source.path);
|
|
31
|
+
const name = getPathName(path);
|
|
32
|
+
return {
|
|
33
|
+
id: path,
|
|
34
|
+
path,
|
|
35
|
+
name,
|
|
36
|
+
extension: getArchiveEntryExtension(name),
|
|
37
|
+
size: source.size,
|
|
38
|
+
lastModified: source.lastModified,
|
|
39
|
+
depth: getPathDepth(path),
|
|
40
|
+
previewable: isPreviewableArchiveEntry(name),
|
|
41
|
+
compressedFile: {
|
|
42
|
+
name,
|
|
43
|
+
size: source.size,
|
|
44
|
+
lastModified: source.lastModified,
|
|
45
|
+
async extract() {
|
|
46
|
+
const buffer = await source.load();
|
|
47
|
+
return new File([buffer], name, {
|
|
48
|
+
type: 'application/octet-stream',
|
|
49
|
+
lastModified: source.lastModified || Date.now(),
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
const parseOctal = (bytes, start, length) => {
|
|
56
|
+
const text = new TextDecoder('ascii')
|
|
57
|
+
.decode(bytes.slice(start, start + length))
|
|
58
|
+
.replace(/\0.*$/, '')
|
|
59
|
+
.trim();
|
|
60
|
+
return text ? Number.parseInt(text, 8) || 0 : 0;
|
|
61
|
+
};
|
|
62
|
+
const readTarName = (bytes, offset) => {
|
|
63
|
+
const decoder = new TextDecoder('utf-8');
|
|
64
|
+
const name = decoder.decode(bytes.slice(offset, offset + 100)).replace(/\0.*$/, '');
|
|
65
|
+
const prefix = decoder.decode(bytes.slice(offset + 345, offset + 500)).replace(/\0.*$/, '');
|
|
66
|
+
return normalizeArchivePath(prefix ? `${prefix}/${name}` : name);
|
|
67
|
+
};
|
|
68
|
+
const parseTarEntries = (bytes) => {
|
|
69
|
+
const entries = [];
|
|
70
|
+
let offset = 0;
|
|
71
|
+
while (offset + TAR_BLOCK_SIZE <= bytes.length) {
|
|
72
|
+
const header = bytes.slice(offset, offset + TAR_BLOCK_SIZE);
|
|
73
|
+
if (header.every(value => value === 0)) {
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
const path = readTarName(bytes, offset);
|
|
77
|
+
const size = parseOctal(bytes, offset + 124, 12);
|
|
78
|
+
const typeFlag = String.fromCharCode(bytes[offset + 156] || 0);
|
|
79
|
+
const dataOffset = offset + TAR_BLOCK_SIZE;
|
|
80
|
+
const nextOffset = dataOffset + Math.ceil(size / TAR_BLOCK_SIZE) * TAR_BLOCK_SIZE;
|
|
81
|
+
if (path && typeFlag !== '5') {
|
|
82
|
+
const fileBytes = bytes.slice(dataOffset, dataOffset + size);
|
|
83
|
+
entries.push(createEntryView({
|
|
84
|
+
path,
|
|
85
|
+
size,
|
|
86
|
+
load: async () => toArrayBuffer(fileBytes),
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
offset = nextOffset;
|
|
90
|
+
}
|
|
91
|
+
return entries;
|
|
92
|
+
};
|
|
93
|
+
const getArchiveExtension = (filename) => {
|
|
94
|
+
const lower = filename.toLowerCase();
|
|
95
|
+
if (lower.endsWith('.tar.gz') || lower.endsWith('.tgz')) {
|
|
96
|
+
return 'tgz';
|
|
97
|
+
}
|
|
98
|
+
return getArchiveEntryExtension(filename);
|
|
99
|
+
};
|
|
100
|
+
const getGzipEntryName = (filename) => {
|
|
101
|
+
const lower = filename.toLowerCase();
|
|
102
|
+
if (lower.endsWith('.gzip')) {
|
|
103
|
+
return filename.slice(0, -5) || 'archive';
|
|
104
|
+
}
|
|
105
|
+
if (lower.endsWith('.gz')) {
|
|
106
|
+
return filename.slice(0, -3) || 'archive';
|
|
107
|
+
}
|
|
108
|
+
return `${filename || 'archive'}.bin`;
|
|
109
|
+
};
|
|
110
|
+
const loadZipEntries = async (data) => {
|
|
111
|
+
const { default: JSZip } = await import('jszip');
|
|
112
|
+
const zip = await JSZip.loadAsync(data);
|
|
113
|
+
const entries = [];
|
|
114
|
+
zip.forEach((relativePath, file) => {
|
|
115
|
+
var _a, _b;
|
|
116
|
+
if (file.dir) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const metadata = file;
|
|
120
|
+
const normalizedPath = normalizeArchivePath(relativePath);
|
|
121
|
+
entries.push(createEntryView({
|
|
122
|
+
path: normalizedPath,
|
|
123
|
+
size: ((_a = metadata._data) === null || _a === void 0 ? void 0 : _a.uncompressedSize) || 0,
|
|
124
|
+
lastModified: (_b = file.date) === null || _b === void 0 ? void 0 : _b.getTime(),
|
|
125
|
+
load: async () => file.async('arraybuffer'),
|
|
126
|
+
}));
|
|
127
|
+
});
|
|
128
|
+
return entries;
|
|
129
|
+
};
|
|
130
|
+
const loadTarEntries = async (data, filename, extension) => {
|
|
131
|
+
const source = new Uint8Array(data);
|
|
132
|
+
const bytes = extension === 'tar'
|
|
133
|
+
? source
|
|
134
|
+
: await decompressBytes(source, 'gzip');
|
|
135
|
+
if (!bytes) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
if (extension === 'gz' || extension === 'gzip') {
|
|
139
|
+
const lower = filename.toLowerCase();
|
|
140
|
+
const isTarGz = lower.endsWith('.tar.gz') || lower.endsWith('.tgz');
|
|
141
|
+
if (!isTarGz) {
|
|
142
|
+
const name = getGzipEntryName(filename);
|
|
143
|
+
return [createEntryView({
|
|
144
|
+
path: name,
|
|
145
|
+
size: bytes.byteLength,
|
|
146
|
+
load: async () => toArrayBuffer(bytes),
|
|
147
|
+
})];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return parseTarEntries(bytes);
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Worker fallback for constrained browsers, temporary local servers, and
|
|
154
|
+
* mobile WebViews. The main libarchive path still covers broader formats;
|
|
155
|
+
* this covers common ZIP/TAR/GZIP archives without an extra static Worker.
|
|
156
|
+
*/
|
|
157
|
+
export const loadArchiveEntriesWithoutWorker = async (data, filename) => {
|
|
158
|
+
const extension = getArchiveExtension(filename);
|
|
159
|
+
if (ZIP_LIKE_EXTENSIONS.has(extension)) {
|
|
160
|
+
return loadZipEntries(data);
|
|
161
|
+
}
|
|
162
|
+
if (TAR_LIKE_EXTENSIONS.has(extension)) {
|
|
163
|
+
return loadTarEntries(data, filename, extension);
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const ARCHIVE_PREVIEWABLE_EXTENSIONS: readonly string[];
|
|
2
|
+
export interface ArchiveEntryView {
|
|
3
|
+
id: string;
|
|
4
|
+
path: string;
|
|
5
|
+
name: string;
|
|
6
|
+
extension: string;
|
|
7
|
+
size: number;
|
|
8
|
+
lastModified?: number;
|
|
9
|
+
depth: number;
|
|
10
|
+
previewable: boolean;
|
|
11
|
+
compressedFile: {
|
|
12
|
+
name: string;
|
|
13
|
+
size: number;
|
|
14
|
+
lastModified?: number;
|
|
15
|
+
extract(): Promise<File>;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export declare const getArchiveEntryExtension: (name: string) => string;
|
|
19
|
+
export declare const isArchiveExtension: (extension: string) => boolean;
|
|
20
|
+
export declare const isPreviewableArchiveEntry: (name: string) => boolean;
|
|
21
|
+
export declare const formatArchiveBytes: (value: number) => string;
|
|
22
|
+
export declare const flattenArchiveObject: (input: Record<string, unknown>, prefix?: string) => ArchiveEntryView[];
|
|
23
|
+
export declare const createArchiveCacheKey: (archiveName: string, archiveSize: number, entry: ArchiveEntryView) => string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ARCHIVE_EXTENSIONS, DEFAULT_SUPPORTED_EXTENSIONS, } from '@file-viewer/core';
|
|
2
|
+
export const ARCHIVE_PREVIEWABLE_EXTENSIONS = DEFAULT_SUPPORTED_EXTENSIONS;
|
|
3
|
+
export const getArchiveEntryExtension = (name) => {
|
|
4
|
+
const clean = name.split(/[?#]/)[0] || name;
|
|
5
|
+
const dot = clean.lastIndexOf('.');
|
|
6
|
+
return dot === -1 ? '' : clean.slice(dot + 1).toLowerCase();
|
|
7
|
+
};
|
|
8
|
+
export const isArchiveExtension = (extension) => (ARCHIVE_EXTENSIONS.includes(extension.toLowerCase()));
|
|
9
|
+
export const isPreviewableArchiveEntry = (name) => {
|
|
10
|
+
const extension = getArchiveEntryExtension(name);
|
|
11
|
+
return ARCHIVE_PREVIEWABLE_EXTENSIONS.includes(extension);
|
|
12
|
+
};
|
|
13
|
+
export const formatArchiveBytes = (value) => {
|
|
14
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
15
|
+
return '-';
|
|
16
|
+
}
|
|
17
|
+
if (value < 1024) {
|
|
18
|
+
return `${value} B`;
|
|
19
|
+
}
|
|
20
|
+
const units = ['KB', 'MB', 'GB'];
|
|
21
|
+
let next = value / 1024;
|
|
22
|
+
for (const unit of units) {
|
|
23
|
+
if (next < 1024 || unit === units[units.length - 1]) {
|
|
24
|
+
return `${next.toFixed(next < 10 ? 1 : 0)} ${unit}`;
|
|
25
|
+
}
|
|
26
|
+
next /= 1024;
|
|
27
|
+
}
|
|
28
|
+
return `${value} B`;
|
|
29
|
+
};
|
|
30
|
+
const isCompressedFile = (value) => {
|
|
31
|
+
return typeof value === 'object' &&
|
|
32
|
+
value !== null &&
|
|
33
|
+
'extract' in value &&
|
|
34
|
+
typeof value.extract === 'function';
|
|
35
|
+
};
|
|
36
|
+
export const flattenArchiveObject = (input, prefix = '') => {
|
|
37
|
+
const entries = [];
|
|
38
|
+
Object.entries(input).forEach(([key, value]) => {
|
|
39
|
+
const path = prefix ? `${prefix}/${key}` : key;
|
|
40
|
+
if (isCompressedFile(value)) {
|
|
41
|
+
const name = value.name || key;
|
|
42
|
+
const extension = getArchiveEntryExtension(name);
|
|
43
|
+
entries.push({
|
|
44
|
+
id: path,
|
|
45
|
+
path,
|
|
46
|
+
name,
|
|
47
|
+
extension,
|
|
48
|
+
size: value.size || 0,
|
|
49
|
+
lastModified: value.lastModified,
|
|
50
|
+
depth: path.split('/').length - 1,
|
|
51
|
+
previewable: isPreviewableArchiveEntry(name),
|
|
52
|
+
compressedFile: value,
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (value && typeof value === 'object') {
|
|
57
|
+
entries.push(...flattenArchiveObject(value, path));
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
return entries;
|
|
61
|
+
};
|
|
62
|
+
export const createArchiveCacheKey = (archiveName, archiveSize, entry) => {
|
|
63
|
+
return [
|
|
64
|
+
'archive-entry',
|
|
65
|
+
archiveName || 'archive',
|
|
66
|
+
archiveSize,
|
|
67
|
+
entry.path,
|
|
68
|
+
entry.size,
|
|
69
|
+
entry.lastModified || 0,
|
|
70
|
+
].join(':');
|
|
71
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type FileRenderHandler, type FileViewerRenderedInstance, type FileViewerRendererPlugin, type RendererDefinition } from '@file-viewer/core';
|
|
2
|
+
export declare const archiveRendererDefinition: RendererDefinition;
|
|
3
|
+
export declare const renderFileViewerArchive: FileRenderHandler<FileViewerRenderedInstance, HTMLDivElement>;
|
|
4
|
+
export declare const archiveRenderer: FileViewerRendererPlugin<FileRenderHandler<FileViewerRenderedInstance, HTMLDivElement>>;
|
|
5
|
+
export default archiveRenderer;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { DEFAULT_RENDERER_DEFINITIONS, } from '@file-viewer/core';
|
|
2
|
+
const archiveDefinition = DEFAULT_RENDERER_DEFINITIONS.find(definition => definition.id === 'archive');
|
|
3
|
+
if (!archiveDefinition) {
|
|
4
|
+
throw new Error('@file-viewer/renderer-archive could not locate the shared archive format definition.');
|
|
5
|
+
}
|
|
6
|
+
export const archiveRendererDefinition = archiveDefinition;
|
|
7
|
+
export const renderFileViewerArchive = (buffer, target, type, context) => import('./archive.js').then(({ default: renderArchive }) => renderArchive(buffer, target, type, context));
|
|
8
|
+
export const archiveRenderer = {
|
|
9
|
+
id: 'file-viewer-renderer-archive',
|
|
10
|
+
label: 'Flyfish File Viewer archive renderer',
|
|
11
|
+
definitions: [archiveRendererDefinition],
|
|
12
|
+
handlers: [{
|
|
13
|
+
rendererId: archiveRendererDefinition.id,
|
|
14
|
+
handler: renderFileViewerArchive,
|
|
15
|
+
}],
|
|
16
|
+
};
|
|
17
|
+
export default archiveRenderer;
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@file-viewer/renderer-archive",
|
|
3
|
+
"version": "2.0.11",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Standalone archive renderer plugin for Flyfish File Viewer with libarchive worker, ZIP/TAR fallback, IndexedDB cache, and nested previews.",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"file-viewer",
|
|
9
|
+
"renderer",
|
|
10
|
+
"archive",
|
|
11
|
+
"zip",
|
|
12
|
+
"rar",
|
|
13
|
+
"7z",
|
|
14
|
+
"tar",
|
|
15
|
+
"document-preview",
|
|
16
|
+
"document-viewer",
|
|
17
|
+
"file-preview",
|
|
18
|
+
"self-hosted"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public",
|
|
22
|
+
"registry": "https://registry.npmjs.org/"
|
|
23
|
+
},
|
|
24
|
+
"author": {
|
|
25
|
+
"name": "Wangyu",
|
|
26
|
+
"email": "wybaby168@gmail.com"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/flyfish-dev/file-viewer-renderer-archive.git",
|
|
31
|
+
"directory": "packages/renderers/archive"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://doc.file-viewer.app/guide/on-demand-renderers",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/flyfish-dev/file-viewer-renderer-archive/issues"
|
|
36
|
+
},
|
|
37
|
+
"funding": {
|
|
38
|
+
"type": "individual",
|
|
39
|
+
"url": "https://dev.flyfish.group/shop"
|
|
40
|
+
},
|
|
41
|
+
"main": "./dist/index.js",
|
|
42
|
+
"module": "./dist/index.js",
|
|
43
|
+
"types": "./dist/index.d.ts",
|
|
44
|
+
"exports": {
|
|
45
|
+
".": {
|
|
46
|
+
"types": "./dist/index.d.ts",
|
|
47
|
+
"import": "./dist/index.js",
|
|
48
|
+
"default": "./dist/index.js"
|
|
49
|
+
},
|
|
50
|
+
"./package.json": "./package.json"
|
|
51
|
+
},
|
|
52
|
+
"files": [
|
|
53
|
+
"dist",
|
|
54
|
+
"README.md",
|
|
55
|
+
"README.en.md",
|
|
56
|
+
"LICENSE"
|
|
57
|
+
],
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@file-viewer/core": "^2.0.11",
|
|
60
|
+
"jszip": "^3.10.1",
|
|
61
|
+
"libarchive.js": "^2.0.2"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"typescript": "^6.0.3"
|
|
65
|
+
},
|
|
66
|
+
"license": "Apache-2.0",
|
|
67
|
+
"scripts": {
|
|
68
|
+
"build": "tsc -b tsconfig.json",
|
|
69
|
+
"type-check": "tsc -b tsconfig.json"
|
|
70
|
+
}
|
|
71
|
+
}
|