/// /// todo: this function can currently cause dataloss. see #10070 /// /// success private async Task RenameCustomerFolderAsync( OrgSlug source, OrgSlug destination, OrgId owner, CancellationToken cancellationToken) { if (source == destination) return true; if (!await CanClaimCustomerFolderAsync(destination, owner, cancellationToken)) return false; var sourcePrefix = source.ToString(); var destinationPrefix = destination.ToString(); var sourceKeys = await ListKeysAsync(sourcePrefix, cancellationToken); // The marker of the customer folder carries the efecte-org-id, so it is copied last and // deleted first: the destination only becomes resolvable once it is complete. Everything // below the customer folder keeps its relative key, including subfolders Houston does not // know, so no document ever ends up somewhere else. var documentKeys = sourceKeys.Where(key => key != sourcePrefix).ToList(); var markerKeys = sourceKeys.Where(key => key == sourcePrefix).ToList(); var copiedKeys = new List(sourceKeys.Count); try { foreach (var key in documentKeys.Concat(markerKeys)) { var destinationKey = destinationPrefix + key[sourcePrefix.Length..]; await CopyObjectAsync(key, destinationKey, cancellationToken); copiedKeys.Add(destinationKey); } } catch (AmazonS3Exception exception) { ExceptionlessClient.Default.SubmitLog( $"Could not move customer folder '{source}' to '{destination}': {exception.Message}. " + "The folder stays where it is.", LogLevel.Error); // Leftover copies would block every later attempt, because the destination is only // claimed while it is empty. Cleaning them up keeps the move retryable. await TryDeleteObjectsAsync(copiedKeys, cancellationToken); return false; } // From here on the destination is authoritative. A failure while deleting only leaves // garbage behind, the customer already sees all of his documents under the new name. await TryDeleteObjectsAsync(documentKeys, cancellationToken); await TryDeleteObjectsAsync(markerKeys, cancellationToken); return true; }