mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-02 16:15:39 +00:00
152 lines
4.8 KiB
TypeScript
152 lines
4.8 KiB
TypeScript
/**
|
|
* Where an import stopped, per file, so a cancelled, crashed or paused import
|
|
* of a large file continues instead of importing every message again
|
|
* (Email/import doesn't skip duplicates). Kept in IndexedDB, keyed by the
|
|
* Bulwark account, the target and the file's name, size and date; removed
|
|
* when the import finishes, when the account signs out and on a full
|
|
* sign-out. Falls back to memory where IndexedDB isn't available.
|
|
*/
|
|
|
|
export interface ImportCheckpoint {
|
|
key: string;
|
|
/** The Bulwark account that ran the import. */
|
|
scope: string;
|
|
fileName: string;
|
|
/** Items handled (imported, skipped or failed) from the start of the file. */
|
|
done: number;
|
|
imported: number;
|
|
updatedAt: number;
|
|
}
|
|
|
|
const DB_NAME = "bulwark-mail-import";
|
|
const STORE = "checkpoints";
|
|
/** Older checkpoints are dropped: the file has likely changed or been forgotten. */
|
|
const MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
|
|
export function checkpointKey(parts: {
|
|
scope: string;
|
|
accountId: string;
|
|
target: string | null;
|
|
keepFolders: boolean;
|
|
file: { name: string; size: number; lastModified: number };
|
|
}): string {
|
|
return [parts.scope, parts.accountId, parts.target ?? "", parts.keepFolders ? "1" : "0", parts.file.name, parts.file.size, parts.file.lastModified].join("\u0000");
|
|
}
|
|
|
|
const memory = new Map<string, ImportCheckpoint>();
|
|
|
|
function hasIdb(): boolean {
|
|
try {
|
|
return typeof indexedDB !== "undefined" && indexedDB !== null;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
let dbPromise: Promise<IDBDatabase> | null = null;
|
|
|
|
function openDb(): Promise<IDBDatabase> {
|
|
if (dbPromise) return dbPromise;
|
|
dbPromise = new Promise<IDBDatabase>((resolve, reject) => {
|
|
const request = indexedDB.open(DB_NAME, 1);
|
|
request.onupgradeneeded = () => {
|
|
const db = request.result;
|
|
if (!db.objectStoreNames.contains(STORE)) {
|
|
db.createObjectStore(STORE, { keyPath: "key" }).createIndex("scope", "scope");
|
|
}
|
|
};
|
|
request.onsuccess = () => {
|
|
const db = request.result;
|
|
// Another tab deleting the database (sign-out) must not wait on us.
|
|
db.onversionchange = () => { db.close(); dbPromise = null; };
|
|
resolve(db);
|
|
};
|
|
request.onerror = () => { dbPromise = null; reject(request.error); };
|
|
request.onblocked = () => { dbPromise = null; reject(new Error("IndexedDB blocked")); };
|
|
});
|
|
return dbPromise;
|
|
}
|
|
|
|
function done<T>(request: IDBRequest<T>): Promise<T> {
|
|
return new Promise((resolve, reject) => {
|
|
request.onsuccess = () => resolve(request.result);
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
}
|
|
|
|
async function withStore<T>(mode: IDBTransactionMode, run: (store: IDBObjectStore) => IDBRequest<T>): Promise<T> {
|
|
const db = await openDb();
|
|
return done(run(db.transaction(STORE, mode).objectStore(STORE)));
|
|
}
|
|
|
|
export async function readCheckpoint(key: string, now = Date.now()): Promise<ImportCheckpoint | null> {
|
|
let found: ImportCheckpoint | null;
|
|
if (!hasIdb()) {
|
|
found = memory.get(key) ?? null;
|
|
} else {
|
|
try {
|
|
found = ((await withStore("readonly", (s) => s.get(key))) as ImportCheckpoint | undefined) ?? null;
|
|
} catch {
|
|
found = memory.get(key) ?? null;
|
|
}
|
|
}
|
|
if (found && now - found.updatedAt > MAX_AGE_MS) {
|
|
await deleteCheckpoint(key);
|
|
return null;
|
|
}
|
|
return found;
|
|
}
|
|
|
|
export async function writeCheckpoint(checkpoint: ImportCheckpoint): Promise<void> {
|
|
memory.set(checkpoint.key, checkpoint);
|
|
if (!hasIdb()) return;
|
|
try {
|
|
await withStore("readwrite", (s) => s.put(checkpoint));
|
|
memory.delete(checkpoint.key);
|
|
} catch {
|
|
/* kept in memory for this tab */
|
|
}
|
|
}
|
|
|
|
export async function deleteCheckpoint(key: string): Promise<void> {
|
|
memory.delete(key);
|
|
if (!hasIdb()) return;
|
|
try {
|
|
await withStore("readwrite", (s) => s.delete(key));
|
|
} catch {
|
|
/* nothing stored */
|
|
}
|
|
}
|
|
|
|
/** Removes the checkpoints of one Bulwark account (it signs out). */
|
|
export async function clearCheckpointsFor(scope: string): Promise<void> {
|
|
for (const [key, value] of memory) if (value.scope === scope) memory.delete(key);
|
|
if (!hasIdb()) return;
|
|
try {
|
|
const db = await openDb();
|
|
const tx = db.transaction(STORE, "readwrite");
|
|
const keys = await done(tx.objectStore(STORE).index("scope").getAllKeys(scope));
|
|
const store = db.transaction(STORE, "readwrite").objectStore(STORE);
|
|
await Promise.all(keys.map((k) => done(store.delete(k))));
|
|
} catch {
|
|
/* nothing stored */
|
|
}
|
|
}
|
|
|
|
/** Removes every checkpoint (everyone signed out). */
|
|
export async function clearAllCheckpoints(): Promise<void> {
|
|
memory.clear();
|
|
if (!hasIdb()) return;
|
|
const db = dbPromise ? await dbPromise.catch(() => null) : null;
|
|
db?.close();
|
|
dbPromise = null;
|
|
await new Promise<void>((resolve) => {
|
|
try {
|
|
const request = indexedDB.deleteDatabase(DB_NAME);
|
|
request.onsuccess = request.onerror = request.onblocked = () => resolve();
|
|
} catch {
|
|
resolve();
|
|
}
|
|
});
|
|
}
|