@@ -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()}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user