Перенос контента файлов из БД на диск
Перенос контента файлов из БД на диск¶
Если контент файла хранится в БД, но доступ к ним может быть несколько медленнее, чем к файлам на файловой системе в силу особенностей СУБД. Если для некоторых из файлов требуется перенести контент на диск, то можно написать универсальный запрос к сервису карточек и использовать его из кода или по нажатию кнопки.
Это расширение можно не реализовывать самостоятельно, т.к. оно уже есть в типовом решении (Tessa.Extensions.Default.Server.Cards
и Tessa.Extensions.Default.Shared.Cards
). Для пояснения работы расширения его код приведён ниже.
Код расширения пишется на сервере в сборке Tessa.Extensions.Server
:
public sealed class MoveFileRequestExtension : CardRequestExtension
{
#region Fields
private readonly ICardContentStrategy contentStrategy;
private readonly ITransactionStrategy transactionStrategy;
private readonly ICardFileVersionStrategy versionStrategy;
private readonly ICardFileVersionStrategy deletedVersionStrategy;
#endregion
#region Constructors
public MoveFileRequestExtension(
ICardContentStrategy contentStrategy,
ITransactionStrategy transactionStrategy,
ICardFileVersionStrategy versionStrategy,
[Dependency(CardFileVersionStrategyNames.Deleted)]
ICardFileVersionStrategy deletedVersionStrategy)
{
this.contentStrategy = NotNullOrThrow(contentStrategy);
this.transactionStrategy = NotNullOrThrow(transactionStrategy);
this.versionStrategy = NotNullOrThrow(versionStrategy);
this.deletedVersionStrategy = NotNullOrThrow(deletedVersionStrategy);
}
#endregion
#region Base Overrides
public override async Task AfterRequest(ICardRequestExtensionContext context)
{
if (!context.RequestIsSuccessful || !context.ValidationResult.IsSuccessful())
{
return;
}
if (!context.Session.User.IsAdministrator())
{
ValidationSequence
.Begin(context.ValidationResult)
.SetObjectName(this)
.Error(ValidationKeys.UserIsNotAdmin)
.End();
return;
}
if (context.Request.CardID is not { } requestCardID)
{
ValidationSequence
.Begin(context.ValidationResult)
.SetObjectName(this)
.Error(CardValidationKeys.UnspecifiedCardID)
.End();
return;
}
if (context.Request.Info.TryGet<int?>(DefaultExtensionHelper.SourceIDKey) is not { } sourceID)
{
context.ValidationResult.AddError(this,
$"Parameter request.Info[\"{DefaultExtensionHelper.SourceIDKey}\"] is not specified."
+ " Please, use method DefaultExtensionHelper.SetSourceID(...)");
return;
}
CardFileSourceType targetSourceType = new(sourceID);
Guid[]? requestFileIDs = context.Request.FileID is { } fileID ? new[] { fileID } : null;
// Получение контекстов для существующих файлов в карточке
var contentContexts = await CardComponentHelper.GetContentContextsAsync(
requestCardID,
context.ValidationResult,
this.versionStrategy,
requestFileIDs,
context.CancellationToken);
if (!context.ValidationResult.IsSuccessful())
{
return;
}
List<CardContentContext> contentsToMove = contentContexts
.Where(c => c.Source != targetSourceType)
.ToList();
// Получение контекстов для удалённых файлов в карточке
List<CardContentContext> deletedContentContexts = await CardComponentHelper.GetContentContextsAsync(
requestCardID,
context.ValidationResult,
this.deletedVersionStrategy,
requestFileIDs,
context.CancellationToken);
if (!context.ValidationResult.IsSuccessful())
{
return;
}
List<CardContentContext> deletedContentsToMove = deletedContentContexts
.Where(c => c.Source != targetSourceType)
.ToList();
// Выход, если ни одного контента не нашлось
if (!contentsToMove.Any() && !deletedContentsToMove.Any())
{
return;
}
// Группировка контентов по таблицам, к которым они относятся
Dictionary<string, IEnumerable<CardContentContext>>? groupedContentsToMove = new(2)
{
["FileVersions"] = contentsToMove,
["DeletedFileVersions"] = deletedContentContexts
};
await using IDbScopeInstance _ = context.DbScope!.Create();
DbManager db = context.DbScope!.Db;
IQueryBuilderFactory builderFactory = context.DbScope!.BuilderFactory;
// Транзакция используется только в том случае, если она отсутствовала на момент запуска
bool useTransaction = db.DataConnection.Transaction is null;
if (useTransaction)
{
await db.ExecuteSetXactAbortOnAsync(context.CancellationToken);
}
foreach ((string tableName, IEnumerable<CardContentContext> contents) in groupedContentsToMove)
{
foreach (var content in contents)
{
CardContentContext newContent = new(
content.CardID,
content.FileID,
content.VersionRowID,
targetSourceType,
content.ValidationResult);
await using (Stream contentStream = await this.contentStrategy.GetAsync(content, context.CancellationToken))
{
await this.contentStrategy.StoreAsync(newContent, contentStream, context.CancellationToken);
}
async Task ExecuteAsync(CancellationToken cancellationToken)
{
await this.contentStrategy.DeleteAsync(content, cancellationToken);
await db
.SetCommand(
builderFactory
.Update(tableName).C("SourceID").Assign().P("SourceID")
.Where().C("RowID").Equals().P("RowID")
.Build(),
db.Parameter("RowID", content.VersionRowID, DataType.Guid),
db.Parameter("SourceID", (short) targetSourceType, DataType.Int16))
.LogCommand()
.ExecuteNonQueryAsync(cancellationToken);
}
if (useTransaction)
{
await this.transactionStrategy.ExecuteInTransactionAsync(
context.ValidationResult,
p => ExecuteAsync(p.CancellationToken),
context.CancellationToken);
}
else
{
await ExecuteAsync(CancellationToken.None);
}
}
}
}
#endregion
}
Регистрация расширения:
[Registrator]
public sealed class Registrator : RegistratorBase
{
public override void RegisterUnity() =>
this.UnityContainer.RegisterSingleton<MoveFileRequestExtension>();
public override void RegisterExtensions(IExtensionContainer extensionContainer) =>
extensionContainer
.RegisterExtension<ICardRequestExtension, MoveFileRequestExtension>(x => x
.WithOrder(ExtensionStage.BeforePlatform, 1)
.WithUnity(this.UnityContainer)
.WhenRequestTypes(DefaultRequestTypes.MoveFiles));
}
В общей для клиента и сервера сборке Tessa.Extensions.Shared
нужен хэлпер с идентификатором запроса:
public static class CardExtensionRequestTypes
{
public static readonly Guid MoveFilesToDisk =
new Guid(0x64565091, 0x2c00, 0x4ecd, 0xb6, 0x41, 0x1c, 0x8c, 0x8f, 0x91, 0x2c, 0x58);
}
Чтобы переместить на диск все файлы карточки со всеми версиями, на клиенте можно сделать следующий вызов:
Guid cardID = ... ; // идентификатор карточки
var request = new CardRequest { CardID = cardID, RequestType = CardExtensionRequestTypes.MoveFilesToDisk };
CardResponse response = cardRepository.Request(request);
ValidationResult result = response.ValidationResult.Build();
if (!result.IsSuccessful)
{
CardUIHelper.ShowResult(result);
}
Чтобы переместить все версии конкретного файла, не затрагивая остальные файлы карточки, вызов с клиента будет выглядеть так:
Guid cardID = ... ; // идентификатор карточки
Guid fileID = ... ; // идентификатор файла
var request = new CardRequest { CardID = cardID, FileID = fileID, RequestType = CardExtensionRequestTypes.MoveFilesToDisk };
CardResponse response = cardRepository.Request(request);
ValidationResult result = response.ValidationResult.Build();
if (!result.IsSuccessful)
{
CardUIHelper.ShowResult(result);
}