fix: deduplicate UIDs during iCal import to prevent mass failures #113

This commit is contained in:
Linus Rath
2026-03-30 15:06:28 +02:00
parent 1eebec292a
commit 0fb8b81acd
11 changed files with 30 additions and 11 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ interface ICalImportModalProps {
onClose: () => void;
}
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
const ACCEPTED_EXTENSIONS = [".ics", ".ical"];
type ImportStep = "select" | "preview" | "importing";
+1 -1
View File
@@ -2057,7 +2057,7 @@
"importing": "Termine werden importiert...",
"success": "{count} Termine erfolgreich importiert",
"error": "Kalender konnte nicht importiert werden",
"file_too_large": "Datei überschreitet das 5-MB-Limit",
"file_too_large": "Datei überschreitet das 10-MB-Limit",
"invalid_format": "Ungültiges Kalenderdateiformat"
},
"management": {
+1 -1
View File
@@ -2057,7 +2057,7 @@
"importing": "Importing events...",
"success": "{count} events imported successfully",
"error": "Failed to import calendar",
"file_too_large": "File exceeds 5MB limit",
"file_too_large": "File exceeds 10MB limit",
"invalid_format": "Invalid calendar file format"
},
"management": {
+1 -1
View File
@@ -2057,7 +2057,7 @@
"importing": "Importando eventos...",
"success": "{count} eventos importados correctamente",
"error": "Error al importar el calendario",
"file_too_large": "El archivo supera el límite de 5 MB",
"file_too_large": "El archivo supera el límite de 10 MB",
"invalid_format": "Formato de archivo de calendario no válido"
},
"management": {
+1 -1
View File
@@ -2057,7 +2057,7 @@
"importing": "Importation en cours...",
"success": "{count} événements importés avec succès",
"error": "Échec de l'importation du calendrier",
"file_too_large": "Le fichier dépasse la limite de 5 Mo",
"file_too_large": "Le fichier dépasse la limite de 10 Mo",
"invalid_format": "Format de fichier calendrier invalide"
},
"management": {
+1 -1
View File
@@ -2057,7 +2057,7 @@
"importing": "Importazione eventi...",
"success": "{count} eventi importati con successo",
"error": "Importazione del calendario fallita",
"file_too_large": "Il file supera il limite di 5 MB",
"file_too_large": "Il file supera il limite di 10 MB",
"invalid_format": "Formato del file calendario non valido"
},
"management": {
+1 -1
View File
@@ -2057,7 +2057,7 @@
"importing": "イベントをインポート中...",
"success": "{count}件のイベントをインポートしました",
"error": "カレンダーのインポートに失敗しました",
"file_too_large": "ファイルサイズが5MBを超えています",
"file_too_large": "ファイルサイズが10MBを超えています",
"invalid_format": "無効なカレンダーファイル形式"
},
"management": {
+1 -1
View File
@@ -2057,7 +2057,7 @@
"importing": "Evenementen importeren...",
"success": "{count} evenementen succesvol geïmporteerd",
"error": "Agenda importeren mislukt",
"file_too_large": "Bestand overschrijdt de limiet van 5 MB",
"file_too_large": "Bestand overschrijdt de limiet van 10 MB",
"invalid_format": "Ongeldig agendabestandsformaat"
},
"management": {
+1 -1
View File
@@ -2057,7 +2057,7 @@
"importing": "Importando eventos...",
"success": "{count} eventos importados com sucesso",
"error": "Falha ao importar calendário",
"file_too_large": "Arquivo excede o limite de 5 MB",
"file_too_large": "Arquivo excede o limite de 10 MB",
"invalid_format": "Formato de arquivo de calendário inválido"
},
"management": {
+1 -1
View File
@@ -2057,7 +2057,7 @@
"importing": "Импортирование событий...",
"success": "{count} событий успешно импортировано",
"error": "Не удалось импортировать календарь",
"file_too_large": "Файл превышает лимит 5 МБ",
"file_too_large": "Файл превышает лимит 10 МБ",
"invalid_format": "Неверный формат файла календаря"
},
"management": {
+20 -1
View File
@@ -383,9 +383,28 @@ export const useCalendarStore = create<CalendarStore>()(
const realCalendarId = cal?.originalId || calendarId;
const targetAccountId = cal?.accountId;
// Deduplicate: skip events whose UIDs already exist on the server
// (Stalwart enforces UID uniqueness across all calendars)
let eventsToProcess = events;
try {
const allServerEvents = await client.getCalendarEvents(undefined, targetAccountId);
const existingUids = new Set<string>();
for (const e of allServerEvents) {
if (e.uid) existingUids.add(e.uid);
}
const before = eventsToProcess.length;
eventsToProcess = eventsToProcess.filter(e => !e.uid || !existingUids.has(e.uid));
const skipped = before - eventsToProcess.length;
if (skipped > 0) {
debug.log(`Import: skipped ${skipped} events with duplicate UIDs (already on server)`);
}
} catch (error) {
debug.warn('Could not fetch existing events for deduplication, proceeding without:', error);
}
// Prepare all events for batch creation
const prepared: Partial<CalendarEvent>[] = [];
for (const event of events) {
for (const event of eventsToProcess) {
const src = sanitizeOutgoingCalendarEventData(event as Partial<CalendarEvent>);
let cleanParticipants: Record<string, CalendarParticipant> | null = null;
if (src.participants) {