diff --git a/src/TopoMojo.Api/Features/Admin/TransferService.cs b/src/TopoMojo.Api/Features/Admin/TransferService.cs index ea0a2ba..0e2365c 100644 --- a/src/TopoMojo.Api/Features/Admin/TransferService.cs +++ b/src/TopoMojo.Api/Features/Admin/TransferService.cs @@ -25,6 +25,28 @@ CoreOptions options WriteIndented = true }; + private readonly string DisksReadme = """ + # README + # This is a list disks where the folder matches the *workspace-id*. + # Any leading, non-hex character sequence of the workspace-id is its *tenant-prefix*. + # When importing data, workspace-id is changed to match the destination tenant-prefix, + # so care must be taken to ensure the destination folders match the new workspace-id. + # + # Folder with all zeros is "stock" folder; don't alter it. + # + # If neither source nor destination use a tenant-prefix, no action necessary. + # + # First, if the source has tenant-prefix, replace those characters with zeros. + # + # Then, if destination has tenant-prefix replace the leading characters with the + # destination tenant-prefix. + # + # The resulting folder names should be 32 characters; only *replace* + # characters, don't add or remove any. + # ---------- + + """; + public async Task> Import(string repoPath, string docPath) { List results = []; @@ -212,6 +234,27 @@ await reader.ReadToEndAsync(), return [.. (await ImportWorkspaces(data))]; } + private void UpdateTenant(Workspace ws) + { + string src = ws.Id.ExtractTenant(); + string dst = _options.Tenant; + string tenant = src.Length > dst.Length + ? string.Concat(dst, new string('0', src.Length - dst.Length)) + : dst + ; + + if (tenant.NotEmpty() && !ws.Id.StartsWith(tenant)) + { + string target = ws.Id; + ws.Id = string.Concat(tenant, ws.Id.AsSpan(tenant.Length)); + foreach (Template t in ws.Templates) + { + t.WorkspaceId = ws.Id; + t.Detail = t.Detail?.Replace(target, ws.Id); + } + } + } + private async Task ImportWorkspaces(IEnumerable data) { List results = []; @@ -220,20 +263,25 @@ private async Task ImportWorkspaces(IEnumerable data) foreach(var topo in data) { - // prevent overwriting - if (wids.Contains(topo.Id)) { - results.Add($"Duplicate: {topo.Name} {topo.Id}"); - continue; - } - // add new stock templates - if (Guid.Parse(topo.Id) == Guid.Empty) { + if (Guid.TryParse(topo.Id, out Guid guid) && guid == Guid.Empty) + { var stock = topo.Templates.Where(t => !tids.Contains(t.Id)).ToArray(); workspaceStore.DbContext.Templates.AddRange(stock); results.Add($"Stock templates: {stock.Length}"); continue; } + // add or remove tenant prefix + UpdateTenant(topo); + + // prevent overwriting + if (wids.Contains(topo.Id)) + { + results.Add($"Duplicate: {topo.Name} {topo.Id}"); + continue; + } + // add workspace to datacontext workspaceStore.DbContext.Workspaces.Add(topo); results.Add($"Success: {topo.Name} {topo.Id}"); @@ -258,7 +306,7 @@ private async Task CreateZipfile(IEnumerable data, IEnumerabl WriteFileToArchive( zipArchive, "_disks.txt", - Encoding.UTF8.GetBytes(string.Join("\n", disks)) + Encoding.UTF8.GetBytes(string.Concat(DisksReadme, string.Join("\n", disks))) ); // export markdown doc artifacts @@ -303,11 +351,11 @@ private async Task ProcessZipfile(Stream stream, string docPath) foreach (var entry in zipArchive.Entries) { // skip disks - if (entry.FullName == "_disks.txt") + if (entry.FullName.Equals("_disks.txt")) continue; // deserialize workspace data - if (entry.FullName == "_data.json") + if (entry.FullName.Equals("_data.json")) { results = await ImportSerializedWorkspaces(entry.Open()); continue; diff --git a/src/TopoMojo.Api/Features/Gamespace/GamespaceService.cs b/src/TopoMojo.Api/Features/Gamespace/GamespaceService.cs index cf66310..b14c024 100644 --- a/src/TopoMojo.Api/Features/Gamespace/GamespaceService.cs +++ b/src/TopoMojo.Api/Features/Gamespace/GamespaceService.cs @@ -231,7 +231,10 @@ private async Task Create(RegistrationContext ctx, User actor) ctx.Gamespace = new Data.Gamespace { - Id = Guid.NewGuid().ToString("n"), + Id = string.Concat( + _options.Tenant, + Guid.NewGuid().ToString("n").AsSpan(_options.Tenant.Length) + ), Name = ctx.Workspace.Name, Workspace = ctx.Workspace, ManagerId = actor.Id, @@ -244,9 +247,6 @@ private async Task Create(RegistrationContext ctx, User actor) GraderKey = ctx.Request.GraderKey.ToSha256() }; - if (string.IsNullOrEmpty(_options.Tenant).Equals(false)) - ctx.Gamespace.Id = _options.Tenant + ctx.Gamespace.Id.Substring(0, ctx.Gamespace.Id.Length - _options.Tenant.Length); - var gamespace = ctx.Gamespace; foreach (var player in ctx.Request.Players) diff --git a/src/TopoMojo.Api/Features/Workspace/WorkspaceService.cs b/src/TopoMojo.Api/Features/Workspace/WorkspaceService.cs index 6742a41..949cd08 100644 --- a/src/TopoMojo.Api/Features/Workspace/WorkspaceService.cs +++ b/src/TopoMojo.Api/Features/Workspace/WorkspaceService.cs @@ -167,7 +167,10 @@ public async Task Create(NewWorkspace model, string subjectId, string workspace.TemplateScope = ""; } - workspace.Id = Guid.NewGuid().ToString("n"); + workspace.Id = string.Concat( + _options.Tenant, + Guid.NewGuid().ToString("n").AsSpan(_options.Tenant.Length) + ); workspace.WhenCreated = DateTimeOffset.UtcNow; workspace.LastActivity = DateTimeOffset.UtcNow; @@ -184,9 +187,6 @@ public async Task Create(NewWorkspace model, string subjectId, string Permission = Permission.Manager }); - if (string.IsNullOrEmpty(_options.Tenant).Equals(false)) - workspace.Id = _options.Tenant + workspace.Id.Substring(0, workspace.Id.Length - _options.Tenant.Length); - workspace = await _store.Create(workspace); // TODO: consider handling document here diff --git a/src/TopoMojo.Api/Structure/StringExtensions.cs b/src/TopoMojo.Api/Structure/StringExtensions.cs index 2b4d55d..98dc679 100644 --- a/src/TopoMojo.Api/Structure/StringExtensions.cs +++ b/src/TopoMojo.Api/Structure/StringExtensions.cs @@ -310,5 +310,21 @@ public static string ToRandomIPv4(this string s) return result; } + + private static readonly char[] hex = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; + + public static string ExtractTenant(this string s) + { + string r = ""; + char[] c = s.ToLower().ToCharArray(); + for (int i = 0; i < c.Length; i++) + { + if (hex.Contains(c[i])) + break; + else + r += c[i]; + } + return r; + } } }