Skip to content

Commit 3fbbc3c

Browse files
committed
New feature:
- Restore File Selection dialogue has been implemented - Now we can backup only newer files - Now we can apply deletion - We can restore the vault totally. - And we can restore the vault by only the newer files. Tidied - Many files has been rewritten, and abstracted.
1 parent 3c4c284 commit 3fbbc3c

File tree

14 files changed

+4164
-2221
lines changed

14 files changed

+4164
-2221
lines changed

Archive.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import * as fflate from "fflate";
2+
import { promiseWithResolver } from "octagonal-wheels/promises";
3+
4+
/**
5+
* A class to archive files
6+
*/
7+
export class Archiver {
8+
_zipFile: fflate.Zip;
9+
10+
_aborted: boolean = false;
11+
_output: Uint8Array[] = [];
12+
13+
_processedCount: number = 0;
14+
_processedLength: number = 0;
15+
_archivedCount: number = 0;
16+
_archiveSize: number = 0;
17+
18+
19+
progressReport(type: string) {
20+
// console.warn(
21+
// `Archiver: ${type} processed: ${this._processedCount} (${this._processedLength} bytes) ${this._archivedCount} (${this._archiveSize} bytes)`
22+
// )
23+
}
24+
25+
_zipFilePromise = promiseWithResolver<Uint8Array>();
26+
get archivedZipFile(): Promise<Uint8Array> {
27+
return this._zipFilePromise.promise;
28+
}
29+
30+
get currentSize(): number {
31+
return this._output.reduce((acc, val) => acc + val.length, 0);
32+
}
33+
34+
constructor() {
35+
// this._archiveName = archiveName;
36+
const zipFile = new fflate.Zip(async (error, dat, final) => this._onProgress(
37+
error, dat, final
38+
));
39+
this._zipFile = zipFile;
40+
}
41+
42+
_onProgress(err: fflate.FlateError | null, data: Uint8Array, final: boolean) {
43+
if (err) {
44+
return this._onError(err);
45+
}
46+
if (data && data.length > 0) {
47+
this._output.push(data);
48+
this._archiveSize += data.length;
49+
}
50+
// No error
51+
this.progressReport("progress");
52+
if (this._aborted) {
53+
return this._onAborted();
54+
}
55+
if (final) {
56+
void this._onFinalise();
57+
}
58+
}
59+
60+
async _onFinalise(): Promise<void> {
61+
this._zipFile.terminate();
62+
const out = new Blob(this._output, { type: "application/zip" });
63+
const result = new Uint8Array(await out.arrayBuffer());
64+
this._zipFilePromise.resolve(result);
65+
}
66+
67+
_onAborted() {
68+
this._zipFile.terminate();
69+
this._zipFilePromise.reject(new Error("Aborted"));
70+
}
71+
_onError(err: fflate.FlateError): void {
72+
this._zipFile.terminate();
73+
this._zipFilePromise.reject(err);
74+
}
75+
76+
addTextFile(text: string, path: string, options?: { mtime?: number }): void {
77+
const binary = new TextEncoder().encode(text);
78+
this.addFile(binary, path, options);
79+
}
80+
81+
addFile(file: Uint8Array, path: string, options?: { mtime?: number }): void {
82+
const fflateFile = new fflate.ZipDeflate(path, {
83+
level: 9,
84+
});
85+
if (options?.mtime) {
86+
fflateFile.mtime = options.mtime;
87+
} else {
88+
fflateFile.mtime = Date.now();
89+
}
90+
this._processedLength += file.length;
91+
this.progressReport("add");
92+
this._zipFile.add(fflateFile);
93+
94+
// TODO: Check if the large file can be added in a single chunks
95+
fflateFile.push(file, true);
96+
}
97+
98+
finalize() {
99+
this._zipFile.end();
100+
return this.archivedZipFile;
101+
}
102+
103+
104+
105+
106+
}
107+
108+
/**
109+
* A class to extract files from a zip archive
110+
*/
111+
export class Extractor {
112+
_zipFile: fflate.Unzip;
113+
_isFileShouldBeExtracted: (file: fflate.UnzipFile) => boolean | Promise<boolean>;
114+
_onExtracted: (filename: string, content: Uint8Array) => Promise<void>;
115+
constructor(isFileShouldBeExtracted: typeof this["_isFileShouldBeExtracted"], callback: typeof this["_onExtracted"],) {
116+
const unzipper = new fflate.Unzip();
117+
unzipper.register(fflate.UnzipInflate);
118+
this._zipFile = unzipper;
119+
this._isFileShouldBeExtracted = isFileShouldBeExtracted;
120+
this._onExtracted = callback;
121+
unzipper.onfile = async (file: fflate.UnzipFile) => {
122+
if (await this._isFileShouldBeExtracted(file)) {
123+
const data: Uint8Array[] = [];
124+
file.ondata = async (err, dat, isFinal) => {
125+
if (err) {
126+
console.error("Error extracting file", err);
127+
return;
128+
}
129+
if (dat && dat.length > 0) {
130+
data.push(dat);
131+
}
132+
133+
if (isFinal) {
134+
const total = new Blob(data, { type: "application/octet-stream" });
135+
const result = new Uint8Array(await total.arrayBuffer());
136+
await this._onExtracted(file.name, result);
137+
}
138+
}
139+
file.start();
140+
} else {
141+
// Skip the file
142+
}
143+
}
144+
}
145+
addZippedContent(data: Uint8Array, isFinal = false) {
146+
this._zipFile.push(data, isFinal);
147+
}
148+
finalise() {
149+
this._zipFile.push(new Uint8Array(), true);
150+
}
151+
}

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ We can store all the files which have been modified, into a ZIP file.
3232
| Key | Description |
3333
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3434
| Start backup at launch | When the plug-in has been loaded, Differential backup will be created automatically. |
35+
| Auto backup style | Check differences to... `Full` to all files, `Only new` to the files which were newer than the backup, `Non-destructive` as same as Only new but not includes the deletion. |
3536
| Include hidden folder | Backup also the configurations, plugins, themes, and, snippets. |
3637
| Backup Destination | Where to save the backup `Inside the vault`, `Anywhere (Desktop only)`, and `S3 bucket` are available. `Anywhere` is on the bleeding edge. Not safe. Only available on desktop devices. |
3738
| Restore folder | The folder which restored files will be stored. |
@@ -68,6 +69,14 @@ We can store all the files which have been modified, into a ZIP file.
6869
- `Test`: Test the connection to the S3 bucket.
6970
- `Create Bucket`: Create a bucket in the S3 bucket.
7071

72+
#### Tools
73+
Here are some tools to manage settings among your devices.
74+
75+
| Key | Description |
76+
| ----------------------- | ------------------------------------------------------------------------------------- |
77+
| Passphrase | Passphrase for encrypting/decrypting the configuration. Please write this down as it will not be saved. |
78+
| Copy setting to another device via URI | When the button is clicked, the URI will be copied to the clipboard. Paste it to another device to copy the settings. |
79+
| Paste setting from another device | Paste the URI from another device to copy the settings, and click `Apply` button. |
7180

7281
## Misc
7382

RestoreFileInfo.svelte

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<script lang="ts">
2+
import DiffZipBackupPlugin from "./main";
3+
import type { FileInfos } from "./main";
4+
import { LATEST, type ListOperations } from "./RestoreView";
5+
6+
interface Props {
7+
plugin: DiffZipBackupPlugin;
8+
toc: FileInfos;
9+
filename: string;
10+
commands: ListOperations;
11+
12+
selected: number;
13+
}
14+
let {
15+
toc = $bindable(),
16+
filename = $bindable(),
17+
commands = $bindable(),
18+
selected = $bindable(),
19+
}: Props = $props();
20+
21+
const isFolder = $derived(filename.endsWith("*"));
22+
const relatedFiles = $derived(
23+
isFolder
24+
? Object.keys(toc).filter((f) =>
25+
f.startsWith(filename.slice(0, -1)),
26+
)
27+
: [filename],
28+
);
29+
const relatedFilesInfo = $derived(relatedFiles.map((f) => toc[f]));
30+
const timeStamps = $derived(
31+
[
32+
...new Set(
33+
relatedFilesInfo
34+
.map((f) =>
35+
f.history.map((e) => new Date(e.modified).getTime()),
36+
)
37+
.flat(),
38+
),
39+
].sort((a, b) => b - a),
40+
);
41+
42+
let selectedTimestamp = $state(0);
43+
</script>
44+
45+
<div class="diffzip-list-row">
46+
<span class="diffzip-list-file">
47+
<span class="title">{filename}</span>
48+
{#if isFolder}
49+
<span class="filecount">({relatedFiles.length})</span>
50+
{/if}
51+
</span>
52+
<span class="diffzip-list-timestamp">
53+
{#if timeStamps.length === 0}
54+
<span class="empty">No Timestamp</span>
55+
{:else}
56+
<select
57+
class="dropdown"
58+
onchange={(e) =>
59+
commands.fileSelected(
60+
filename,
61+
Number.parseInt((e.target as HTMLSelectElement)?.value),
62+
)}
63+
value={selected}
64+
>
65+
<option value={LATEST}>Latest</option>
66+
{#each timeStamps as ts}
67+
<option value={ts}>{new Date(ts).toLocaleString()}</option>
68+
{/each}
69+
<option value={0}> - </option>
70+
</select>
71+
{/if}
72+
</span>
73+
<span class="diffzip-list-actions">
74+
{#if isFolder}
75+
<button
76+
title="Expand Folder"
77+
onclick={() => commands.expandFolder(filename)}
78+
>
79+
📂
80+
</button>
81+
{/if}
82+
<button onclick={() => commands.remove(filename)}> 🗑️ </button>
83+
</span>
84+
</div>
85+
86+
<style>
87+
select {
88+
height: var(--input-height);
89+
}
90+
.diffzip-list-row {
91+
display: flex;
92+
flex-direction: row;
93+
flex-wrap: wrap;
94+
flex-grow: 1;
95+
min-height: 2em;
96+
padding: 2px 0;
97+
}
98+
99+
.diffzip-list-row > span:not(:last-child) {
100+
margin-right: 4px;
101+
}
102+
.diffzip-list-file {
103+
flex-grow: 1;
104+
word-break: break-all;
105+
margin-bottom: 0.25em;
106+
margin-top: 0.25em;
107+
}
108+
.diffzip-list-timestamp {
109+
margin-left: auto;
110+
}
111+
.diffzip-list-actions {
112+
word-wrap: none;
113+
word-break: keep-all;
114+
}
115+
</style>

RestoreFiles.svelte

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<script lang="ts">
2+
import type DiffZipBackupPlugin from "./main";
3+
import type { FileInfos } from "./main";
4+
import { type ListOperations } from "./RestoreView";
5+
import RestoreFileInfo from "./RestoreFileInfo.svelte";
6+
import type { Writable } from "svelte/store";
7+
let test = $state("");
8+
interface Props {
9+
plugin: DiffZipBackupPlugin;
10+
toc: FileInfos;
11+
fileList: Writable<string[]>;
12+
selectedTimestamp: Writable<Record<string, number>>;
13+
}
14+
let {
15+
plugin,
16+
toc = $bindable(),
17+
fileList,
18+
selectedTimestamp,
19+
}: Props = $props();
20+
const files = $derived(
21+
$fileList.sort((a, b) =>
22+
a.localeCompare(b, undefined, { numeric: true }),
23+
),
24+
);
25+
const allFiles = $derived(Object.keys(toc));
26+
function clearList() {
27+
fileList.set([]);
28+
}
29+
function expandFolder(name: string, preventRender = false) {
30+
const folderPrefix = name.slice(0, -1);
31+
const files = allFiles.filter((e) => e.startsWith(folderPrefix));
32+
const newFiles = [...new Set([...$fileList, ...files])].filter(
33+
(e) => e !== name,
34+
);
35+
fileList.set(newFiles);
36+
}
37+
38+
function expandAll() {
39+
const folders = $fileList.filter((e) => e.endsWith("*"));
40+
for (const folder of folders) {
41+
expandFolder(folder, true);
42+
}
43+
}
44+
45+
function remove(file: string) {
46+
fileList.set($fileList.filter((e) => e !== file));
47+
}
48+
function fileSelected(file: string, timestamp: number) {
49+
selectedTimestamp.update((ts) => {
50+
if (timestamp === 0) {
51+
delete ts[file];
52+
} else {
53+
ts[file] = timestamp;
54+
}
55+
return ts;
56+
});
57+
}
58+
const commands: ListOperations = {
59+
clearList,
60+
expandFolder,
61+
expandAll,
62+
remove,
63+
fileSelected,
64+
};
65+
</script>
66+
67+
<div class="diff-zip-files">
68+
{#if files}
69+
{#each files as file (file)}
70+
<RestoreFileInfo
71+
{commands}
72+
{plugin}
73+
{toc}
74+
filename={file}
75+
selected={$selectedTimestamp?.[file] ?? 0}
76+
/>
77+
{/each}
78+
{/if}
79+
</div>

0 commit comments

Comments
 (0)