Files
decePubClient/Components/Settings/DataTransfer.razor
T

156 lines
5.1 KiB
Plaintext
Raw Normal View History

@inherits LocalizableComponentBase
@* The persona's follows, lists, blocks, mutes, blocked servers and bookmarks, exported and imported as the CSV files
Mastodon writes, which most fediverse servers read too: a persona moves here from another server, or away, with what
it gathered there. An import acts one row at a time, through PrivaPub, and can be stopped. *@
<div class="flex flex-col gap-5 w-full">
<section class="flex flex-col gap-2" data-export>
<h3 class="font-semibold text-sm">@Localizer["Export"]</h3>
<p class="text-sm opacity-70">@Localizer["Files in Mastodon's format, which most fediverse servers import."]</p>
<div class="flex flex-wrap gap-2">
@foreach (var data in Kinds)
{
<DButton Size="DSize.Sm" Disabled="@(IsDefaultDisabled || exporting is not null || importing is not null)" OnClick="() => Export(data)"
data-export-kind="@data">
@if (exporting == data)
{
<DLoading Size="DSize.Xs" />
}
else
{
<i class="ion-md-download"></i>
}
<span>@Name(data)</span>
</DButton>
}
</div>
</section>
<section class="flex flex-col gap-2" data-import>
<h3 class="font-semibold text-sm">@Localizer["Import"]</h3>
<div class="flex flex-wrap gap-2 items-end">
<label class="flex flex-col gap-1 text-sm">
<span>@Localizer["What the file holds"]</span>
<DSelect TValue="PortableData" Size="DSize.Sm" @bind-Value="importKind" disabled="@(importing is not null)" data-import-kind>
@foreach (var data in Kinds)
{
<option value="@data">@Name(data)</option>
}
</DSelect>
</label>
<DFileInput Size="DSize.Sm" Accept=".csv,text/csv" OnChange="Pick" disabled="@(importing is not null)" data-import-file />
@if (importing is null)
{
<DButton Size="DSize.Sm" Color="DColor.Primary" Disabled="@(IsDefaultDisabled || file is null || exporting is not null)" OnClick="Import"
data-import-start>
<i class="ion-md-cloud-upload"></i><span>@Localizer["Import"]</span>
</DButton>
}
else
{
<DButton Size="DSize.Sm" Color="DColor.Warning" OnClick="Stop">@Localizer["Stop"]</DButton>
}
</div>
@if (report is not null)
{
<div class="flex flex-col gap-1" data-import-report>
<DProgress class="w-full max-w-md" Value="@(report.Done + report.Failed.Count)" Max="@Math.Max(1, report.Total)" />
<span class="text-sm tabular-nums">
@Localizer["{0} of {1} done, {2} could not be.", report.Done, report.Total, report.Failed.Count]
</span>
<ErrorViewer AdditionalErrors="report.Failed" />
</div>
}
</section>
</div>
@code {
[Inject] PortabilityService PortabilityService { get; set; }
[Inject] IBlazorDownloadFileService DownloadFile { get; set; }
[Inject] ILoggingService Logger { get; set; }
// an export file of 5000 rows stays well under this
const long MaximumFileSize = 4 * 1024 * 1024;
static readonly PortableData[] Kinds = Enum.GetValues<PortableData>();
PortableData? exporting;
PortableData? importing;
PortableData importKind = PortableData.Follows;
string file;
ImportReport report;
CancellationTokenSource stopping;
protected override void OnInitialized() => IsLoading = false;
string Name(PortableData data) => data switch
{
PortableData.Follows => Localizer["Follows"],
PortableData.Lists => Localizer["Lists"],
PortableData.Blocks => Localizer["Blocked accounts"],
PortableData.Mutes => Localizer["Muted accounts"],
PortableData.ServerBlocks => Localizer["Blocked servers"],
_ => Localizer["Bookmarks"]
};
async Task Export(PortableData data)
{
exporting = data;
var result = await PortabilityService.Export(data, CascadingState.PersonaId);
exporting = null;
if (result.IsInvalid)
{
Toast.AddResponseError(result);
return;
}
await DownloadFile.DownloadFileAsync(PortabilityService.FileName(data), (byte[])result.Data, "text/csv");
}
async Task Pick(InputFileChangeEventArgs eventArgs)
{
file = null;
report = null;
if (eventArgs.File.Size > MaximumFileSize)
{
Toast.AddWarning(Localizer["Supplied file \"{0}\" with size {1} exceeds the maximum of {2}.", eventArgs.File.Name,
eventArgs.File.Size.GetFileSize(Localizer), MaximumFileSize.GetFileSize(Localizer)]);
return;
}
try
{
using var reader = new StreamReader(eventArgs.File.OpenReadStream(MaximumFileSize));
file = await reader.ReadToEndAsync();
}
catch (Exception ex)
{
await Logger.ProcessError(ex, $"{nameof(DataTransfer)}.{nameof(Pick)}:{eventArgs.File.Name}");
Toast.AddWarning(Localizer["The file \"{0}\" could not be read.", eventArgs.File.Name]);
}
}
async Task Import()
{
importing = importKind;
stopping = new();
report = new();
var progress = new Progress<ImportReport>(step =>
{
report = step;
StateHasChanged();
});
report = await PortabilityService.Import(importKind, file, progress, stopping.Token);
importing = null;
stopping.Dispose();
stopping = null;
Toast.AddResponseSuccess(Localizer["{0} of {1} done, {2} could not be.", report.Done, report.Total, report.Failed.Count]);
}
void Stop() => stopping?.Cancel();
public override async ValueTask DisposeAsync()
{
stopping?.Cancel();
await base.DisposeAsync();
}
}