///
/// 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;
}