Release Archive Tools 0.2.0
Verify / verify (push) Canceled after 0s

This commit is contained in:
2026-09-02 09:34:59 +02:00
parent a49ec6b17a
commit fe578f46bd
47 changed files with 3337 additions and 253 deletions
+59 -6
View File
@@ -10,6 +10,10 @@ export function CreateWorkspace() {
const [files, setFiles] = useState<File[]>([]);
const [format, setFormat] = useState<CreateFormat>("zip");
const [name, setName] = useState("archive");
const [encryption, setEncryption] = useState<
"none" | "aes-256" | "zipcrypto"
>("none");
const [password, setPassword] = useState("");
const [busy, setBusy] = useState(false);
const [status, setStatus] = useState<string>();
const [error, setError] = useState<string>();
@@ -23,16 +27,26 @@ export function CreateWorkspace() {
setBusy(true);
setError(undefined);
try {
const blob = await createArchive(files, format, next.signal, (progress) =>
setStatus(
`${progress.stage}: ${progress.completed} / ${progress.total}`,
),
const blob = await createArchive(
files,
format,
next.signal,
(progress) =>
setStatus(
`${progress.stage}: ${progress.completed} / ${progress.total}`,
),
format === "zip" && encryption !== "none"
? { password, method: encryption }
: undefined,
);
const extension = format === "tar.gz" ? "tar.gz" : format;
downloadBlob(blob, `${name.trim() || "archive"}.${extension}`);
setStatus(
`Created deterministic ${format.toLocaleUpperCase("en-US")} locally.`,
encryption === "none" || format !== "zip"
? `Created deterministic ${format.toLocaleUpperCase("en-US")} locally.`
: `Created encrypted ${format.toLocaleUpperCase("en-US")} locally; random salt makes encrypted bytes intentionally non-deterministic.`,
);
if (encryption !== "none") setPassword("");
} catch (reason) {
if (!next.signal.aborted) setError(errorMessage(reason));
} finally {
@@ -88,13 +102,52 @@ export function CreateWorkspace() {
permissions are normalized; ZIP compression uses a fixed
implementation and level.
</p>
{format === "zip" ? (
<div className="field-grid">
<label className="field">
<span>Encryption</span>
<select
value={encryption}
onChange={(event) =>
setEncryption(
event.target.value as "none" | "aes-256" | "zipcrypto",
)
}
>
<option value="none">None</option>
<option value="aes-256">AES-256 (recommended)</option>
<option value="zipcrypto">ZipCrypto (legacy, weak)</option>
</select>
</label>
{encryption !== "none" ? (
<label className="field">
<span>Password (memory only)</span>
<input
type="password"
autoComplete="new-password"
value={password}
onChange={(event) => setPassword(event.target.value)}
/>
</label>
) : null}
</div>
) : null}
{format === "zip" && encryption === "zipcrypto" ? (
<p className="policy-note" role="note">
ZipCrypto is offered only for compatibility and is not secure
against modern password attacks. Prefer AES-256.
</p>
) : null}
<StatusMessage error={error} status={status} />
<div className="button-row">
<button
className="primary-button"
type="button"
disabled={
!files.length || busy || total > ARCHIVE_LIMITS.maxCreateBytes
!files.length ||
busy ||
total > ARCHIVE_LIMITS.maxCreateBytes ||
(format === "zip" && encryption !== "none" && !password)
}
onClick={() => void build()}
>