public async Task <ImportResult> ImportSystem(DataFileSystem data, PKSystem system, ulong accountId) { // TODO: make atomic, somehow - we'd need to obtain one IDbConnection and reuse it // which probably means refactoring SystemStore.Save and friends etc var result = new ImportResult { AddedNames = new List <string>(), ModifiedNames = new List <string>(), Success = true // Assume success unless indicated otherwise }; var dataFileToMemberMapping = new Dictionary <string, PKMember>(); var unmappedMembers = new List <DataFileMember>(); // If we don't already have a system to save to, create one if (system == null) { system = await _data.CreateSystem(data.Name); } result.System = system; // Apply system info system.Name = data.Name; if (data.Description != null) { system.Description = data.Description; } if (data.Tag != null) { system.Tag = data.Tag; } if (data.AvatarUrl != null) { system.AvatarUrl = data.AvatarUrl; } if (data.TimeZone != null) { system.UiTz = data.TimeZone ?? "UTC"; } await _data.SaveSystem(system); // Make sure to link the sender account, too await _data.AddAccount(system, accountId); // Determine which members already exist and which ones need to be created var membersByHid = new Dictionary <string, PKMember>(); var membersByName = new Dictionary <string, PKMember>(); await foreach (var member in _data.GetSystemMembers(system)) { membersByHid[member.Hid] = member; membersByName[member.Name] = member; } foreach (var d in data.Members) { PKMember match = null; if (membersByHid.TryGetValue(d.Id, out var matchByHid)) { match = matchByHid; // Try to look up the member with the given ID } else if (membersByName.TryGetValue(d.Name, out var matchByName)) { match = matchByName; // Try with the name instead } if (match != null) { dataFileToMemberMapping.Add(d.Id, match); // Relate the data file ID to the PKMember for importing switches result.ModifiedNames.Add(d.Name); } else { unmappedMembers.Add(d); // Track members that weren't found so we can create them all result.AddedNames.Add(d.Name); } } // If creating the unmatched members would put us over the member limit, abort before creating any members // new total: # in the system + (# in the file - # in the file that already exist) if (data.Members.Count - dataFileToMemberMapping.Count + membersByHid.Count > Limits.MaxMemberCount) { result.Success = false; result.Message = $"Import would exceed the maximum number of members ({Limits.MaxMemberCount})."; result.AddedNames.Clear(); result.ModifiedNames.Clear(); return(result); } // Create all unmapped members in one transaction // These consist of members from another PluralKit system or another framework (e.g. Tupperbox) var membersToCreate = new Dictionary <string, string>(); unmappedMembers.ForEach(x => membersToCreate.Add(x.Id, x.Name)); var newMembers = await _data.CreateMembersBulk(system, membersToCreate); foreach (var member in newMembers) { dataFileToMemberMapping.Add(member.Key, member.Value); } // Update members with data file properties // TODO: parallelize? foreach (var dataMember in data.Members) { dataFileToMemberMapping.TryGetValue(dataMember.Id, out PKMember member); if (member == null) { continue; } // Apply member info member.Name = dataMember.Name; if (dataMember.DisplayName != null) { member.DisplayName = dataMember.DisplayName; } if (dataMember.Description != null) { member.Description = dataMember.Description; } if (dataMember.Color != null) { member.Color = dataMember.Color.ToLower(); } if (dataMember.AvatarUrl != null) { member.AvatarUrl = dataMember.AvatarUrl; } if (dataMember.Prefix != null || dataMember.Suffix != null) { member.ProxyTags = new List <ProxyTag> { new ProxyTag(dataMember.Prefix, dataMember.Suffix) }; } else { // Ignore proxy tags where both prefix and suffix are set to null (would be invalid anyway) member.ProxyTags = (dataMember.ProxyTags ?? new ProxyTag[] { }).Where(tag => !tag.IsEmpty).ToList(); } member.KeepProxy = dataMember.KeepProxy; if (dataMember.Birthday != null) { var birthdayParse = DateTimeFormats.DateExportFormat.Parse(dataMember.Birthday); member.Birthday = birthdayParse.Success ? (LocalDate?)birthdayParse.Value : null; } await _data.SaveMember(member); } // Re-map the switch members in the likely case IDs have changed var mappedSwitches = new List <ImportedSwitch>(); foreach (var sw in data.Switches) { var timestamp = InstantPattern.ExtendedIso.Parse(sw.Timestamp).Value; var swMembers = new List <PKMember>(); swMembers.AddRange(sw.Members.Select(x => dataFileToMemberMapping.FirstOrDefault(y => y.Key.Equals(x)).Value)); mappedSwitches.Add(new ImportedSwitch { Timestamp = timestamp, Members = swMembers }); } // Import switches if (mappedSwitches.Any()) { await _data.AddSwitchesBulk(system, mappedSwitches); } _logger.Information("Imported system {System}", system.Hid); return(result); }
public async Task <ImportResult> ImportSystem(DataFileSystem data, PKSystem system, ulong accountId) { var result = new ImportResult { AddedNames = new List <string>(), ModifiedNames = new List <string>(), System = system, Success = true // Assume success unless indicated otherwise }; // If we don't already have a system to save to, create one if (system == null) { system = result.System = await _data.CreateSystem(data.Name); await _data.AddAccount(system, accountId); } await using var conn = await _db.Obtain(); // Apply system info var patch = new SystemPatch { Name = data.Name }; if (data.Description != null) { patch.Description = data.Description; } if (data.Tag != null) { patch.Tag = data.Tag; } if (data.AvatarUrl != null) { patch.AvatarUrl = data.AvatarUrl; } if (data.TimeZone != null) { patch.UiTz = data.TimeZone ?? "UTC"; } await conn.UpdateSystem(system.Id, patch); // -- Member/switch import -- await using (var imp = await BulkImporter.Begin(system, conn)) { // Tally up the members that didn't exist before, and check member count on import // If creating the unmatched members would put us over the member limit, abort before creating any members var memberCountBefore = await conn.GetSystemMemberCount(system.Id); var membersToAdd = data.Members.Count(m => imp.IsNewMember(m.Id, m.Name)); if (memberCountBefore + membersToAdd > Limits.MaxMemberCount) { result.Success = false; result.Message = $"Import would exceed the maximum number of members ({Limits.MaxMemberCount})."; return(result); } async Task DoImportMember(BulkImporter imp, DataFileMember fileMember) { var isCreatingNewMember = imp.IsNewMember(fileMember.Id, fileMember.Name); // Use the file member's id as the "unique identifier" for the importing (actual value is irrelevant but needs to be consistent) _logger.Debug( "Importing member with identifier {FileId} to system {System} (is creating new member? {IsCreatingNewMember})", fileMember.Id, system.Id, isCreatingNewMember); var newMember = await imp.AddMember(fileMember.Id, fileMember.Id, fileMember.Name, ToMemberPatch(fileMember)); if (isCreatingNewMember) { result.AddedNames.Add(newMember.Name); } else { result.ModifiedNames.Add(newMember.Name); } } // Can't parallelize this because we can't reuse the same connection/tx inside the importer foreach (var m in data.Members) { await DoImportMember(imp, m); } // Lastly, import the switches await imp.AddSwitches(data.Switches.Select(sw => new BulkImporter.SwitchInfo { Timestamp = DateTimeFormats.TimestampExportFormat.Parse(sw.Timestamp).Value, // "Members" here is from whatever ID the data file uses, which the bulk importer can map to the real IDs! :) MemberIdentifiers = sw.Members.ToList() }).ToList()); } _logger.Information("Imported system {System}", system.Hid); return(result); }