[backend/api] Add note export endpoint
This commit is contained in:
parent
49bd10bc68
commit
9d4d4a027e
3 changed files with 164 additions and 0 deletions
39
Iceshrimp.Backend/Controllers/Web/Schemas/ExportNote.cs
Normal file
39
Iceshrimp.Backend/Controllers/Web/Schemas/ExportNote.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Shared.Helpers;
|
||||
|
||||
namespace Iceshrimp.Backend.Controllers.Web.Schemas;
|
||||
|
||||
public class ExportNote : IIdentifiable
|
||||
{
|
||||
public required string Id { get; set; }
|
||||
public required string? Text { get; set; }
|
||||
public required DateTime CreatedAt { get; set; }
|
||||
public required List<string> FileIds { get; set; }
|
||||
public required List<ExportFile> Files { get; set; }
|
||||
public required string? ReplyId { get; set; }
|
||||
public required string? RenoteId { get; set; }
|
||||
public required object? Poll { get; set; }
|
||||
public required string? Cw { get; set; }
|
||||
public required Note.NoteVisibility Visibility { get; set; }
|
||||
public required List<string> VisibleUserIds { get; set; }
|
||||
public required bool LocalOnly { get; set; }
|
||||
}
|
||||
|
||||
public class ExportFile : IIdentifiable
|
||||
{
|
||||
public required string Id { get; set; }
|
||||
public required DateTime CreatedAt { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public required string Type { get; set; }
|
||||
public required int Size { get; set; }
|
||||
public required bool IsSensitive { get; set; }
|
||||
public required string? Blurhash { get; set; }
|
||||
public required DriveFile.FileProperties Properties { get; set; }
|
||||
public required string Url { get; set; }
|
||||
public required string ThumbnailUrl { get; set; }
|
||||
public required string? Comment { get; set; }
|
||||
public string? FolderId { get; set; }
|
||||
public object? Folder { get; set; }
|
||||
public string? UserId { get; set; }
|
||||
public object? User { get; set; }
|
||||
}
|
|
@ -9,6 +9,7 @@ using Iceshrimp.Backend.Core.Database.Tables;
|
|||
using Iceshrimp.Backend.Core.Extensions;
|
||||
using Iceshrimp.Backend.Core.Helpers;
|
||||
using Iceshrimp.Backend.Core.Middleware;
|
||||
using Iceshrimp.Backend.Core.Queues;
|
||||
using Iceshrimp.Backend.Core.Services;
|
||||
using Iceshrimp.Shared.Schemas.Web;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -29,6 +30,7 @@ public class SettingsController(
|
|||
DatabaseContext db,
|
||||
ImportExportService importExportSvc,
|
||||
MetaService meta,
|
||||
QueueService queueSvc,
|
||||
IOptions<Config.InstanceSection> instance
|
||||
) : ControllerBase
|
||||
{
|
||||
|
@ -164,6 +166,28 @@ public class SettingsController(
|
|||
return File(Encoding.UTF8.GetBytes(following), "text/csv", $"following-{DateTime.Now:yyyy-MM-dd-HH-mm-ss}.csv");
|
||||
}
|
||||
|
||||
// TODO: Choose a more appropriate rate limit
|
||||
[HttpPost("export/notes")]
|
||||
[EnableRateLimiting("imports")]
|
||||
[ProducesResults(HttpStatusCode.Accepted)]
|
||||
[ProducesErrors(HttpStatusCode.BadRequest)]
|
||||
public async Task<AcceptedResult> ExportNotes([FromQuery] bool includePrivate = false)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
|
||||
var noteCount = await db.Notes
|
||||
.CountAsync(p => p.UserId == user.Id);
|
||||
if (noteCount < 1)
|
||||
throw GracefulException.BadRequest("You do not have any notes");
|
||||
|
||||
await queueSvc.BackgroundTaskQueue.EnqueueAsync(new NoteExportJobData
|
||||
{
|
||||
UserId = user.Id, IncludePrivate = includePrivate
|
||||
});
|
||||
|
||||
return Accepted();
|
||||
}
|
||||
|
||||
[HttpPost("import/following")]
|
||||
[EnableRateLimiting("imports")]
|
||||
[ProducesResults(HttpStatusCode.Accepted)]
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Iceshrimp.Backend.Controllers.Web.Schemas;
|
||||
using Iceshrimp.Backend.Core.Configuration;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Extensions;
|
||||
using Iceshrimp.Backend.Core.Services;
|
||||
using Iceshrimp.EntityFrameworkCore.Extensions;
|
||||
using Iceshrimp.Shared.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||
|
@ -50,6 +53,9 @@ public class BackgroundTaskQueue(int parallelism)
|
|||
case ProfileFieldUpdateJobData profileFieldUpdateJob:
|
||||
await ProcessProfileFieldUpdateAsync(profileFieldUpdateJob, scope, token);
|
||||
break;
|
||||
case NoteExportJobData noteExportJob:
|
||||
await ProcessNoteExportAsync(noteExportJob, scope, token);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -342,6 +348,94 @@ public class BackgroundTaskQueue(int parallelism)
|
|||
|
||||
logger.LogDebug("Profile fields for user {id} updated successfully", jobData.UserId);
|
||||
}
|
||||
|
||||
private static async Task ProcessNoteExportAsync(
|
||||
NoteExportJobData jobData, IServiceProvider scope, CancellationToken token
|
||||
)
|
||||
{
|
||||
var db = scope.GetRequiredService<DatabaseContext>();
|
||||
var driveSvc = scope.GetRequiredService<DriveService>();
|
||||
var logger = scope.GetRequiredService<ILogger<BackgroundTaskQueue>>();
|
||||
|
||||
logger.LogDebug("Processing note export for user {id}", jobData.UserId);
|
||||
|
||||
var user = await db.Users.FirstOrDefaultAsync(p => p.Id == jobData.UserId, token);
|
||||
if (user == null)
|
||||
{
|
||||
logger.LogDebug("Failed to export notes for user {id}: user not found in database", jobData.UserId);
|
||||
return;
|
||||
}
|
||||
|
||||
var fileIds = await db.Notes
|
||||
.Where(p => p.UserId == user.Id
|
||||
&& (jobData.IncludePrivate || p.VisibilityIsPublicOrHome)
|
||||
&& p.FileIds.Count != 0)
|
||||
.SelectMany(p => p.FileIds)
|
||||
.ToListAsync(token);
|
||||
|
||||
var files = await db.DriveFiles
|
||||
.Where(p => fileIds.Any(id => p.Id == id))
|
||||
.Select(p => new ExportFile
|
||||
{
|
||||
Id = p.Id,
|
||||
CreatedAt = p.CreatedAt,
|
||||
Name = p.Name,
|
||||
Type = p.Type,
|
||||
Size = p.Size,
|
||||
IsSensitive = p.IsSensitive,
|
||||
Blurhash = p.Blurhash,
|
||||
Properties = p.Properties,
|
||||
Url = p.RawAccessUrl,
|
||||
ThumbnailUrl = p.RawThumbnailAccessUrl,
|
||||
Comment = p.Comment
|
||||
})
|
||||
.ToListAsync(token);
|
||||
|
||||
var notes = await db.Notes
|
||||
.Where(p => p.UserId == user.Id && (jobData.IncludePrivate || p.VisibilityIsPublicOrHome))
|
||||
.Select(p => new ExportNote
|
||||
{
|
||||
Id = p.Id,
|
||||
Text = p.Text,
|
||||
CreatedAt = p.CreatedAt,
|
||||
FileIds = p.FileIds,
|
||||
Files = new List<ExportFile>(),
|
||||
ReplyId = p.ReplyId,
|
||||
RenoteId = p.RenoteId,
|
||||
Poll = null,
|
||||
Cw = p.Cw,
|
||||
Visibility = p.Visibility,
|
||||
VisibleUserIds = p.VisibleUserIds,
|
||||
LocalOnly = p.LocalOnly
|
||||
})
|
||||
.ToListAsync(token);
|
||||
|
||||
notes.ForEach(p => p.Files = files.Where(f => p.FileIds.Contains(f.Id)).ToList());
|
||||
|
||||
var request = new DriveFileCreationRequest
|
||||
{
|
||||
Comment = null,
|
||||
Filename = $"notes-{DateTime.UtcNow:yyyy-MM-dd-HH-mm-ss}.json",
|
||||
IsSensitive = jobData.IncludePrivate,
|
||||
MimeType = "application/json",
|
||||
RequestHeaders = null,
|
||||
RequestIp = null,
|
||||
Source = null,
|
||||
Uri = null,
|
||||
FolderId = null
|
||||
};
|
||||
|
||||
var stream = new MemoryStream();
|
||||
await using var sr = new StreamWriter(stream);
|
||||
|
||||
await sr.WriteAsync(JsonSerializer.Serialize(notes, JsonSerialization.Options));
|
||||
|
||||
await driveSvc.StoreFileAsync(stream, user, request, true);
|
||||
|
||||
logger.LogDebug("Exported notes for user {id} successfully", jobData.UserId);
|
||||
|
||||
// TODO: Send some kind of system notification letting the user know the file was created
|
||||
}
|
||||
}
|
||||
|
||||
[JsonDerivedType(typeof(DriveFileDeleteJobData), "driveFileDelete")]
|
||||
|
@ -351,6 +445,7 @@ public class BackgroundTaskQueue(int parallelism)
|
|||
[JsonDerivedType(typeof(UserDeleteJobData), "userDelete")]
|
||||
[JsonDerivedType(typeof(UserPurgeJobData), "userPurge")]
|
||||
[JsonDerivedType(typeof(ProfileFieldUpdateJobData), "profileFieldUpdate")]
|
||||
[JsonDerivedType(typeof(NoteExportJobData), "noteExport")]
|
||||
public abstract class BackgroundTaskJobData;
|
||||
|
||||
public class DriveFileDeleteJobData : BackgroundTaskJobData
|
||||
|
@ -387,4 +482,10 @@ public class UserPurgeJobData : BackgroundTaskJobData
|
|||
public class ProfileFieldUpdateJobData : BackgroundTaskJobData
|
||||
{
|
||||
[JR] [J("userId")] public required string UserId { get; set; }
|
||||
}
|
||||
|
||||
public class NoteExportJobData : BackgroundTaskJobData
|
||||
{
|
||||
[JR] [J("userId")] public required string UserId { get; set; }
|
||||
[JR] [J("includePrivate")] public required bool IncludePrivate { get; set; }
|
||||
}
|
Loading…
Add table
Reference in a new issue