Хелпер Support/TestFileStorage: словари объектов/типов + журнал удалений, подставка читает состояние на момент вызова (позиция потока при Put — по контракту порта). Потребители (9 файлов) перетипизированы на .Storage, фейк удалён, тесты 1340 зелёные.
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
using Deal.Contracts.Integrations.Models;
|
using Deal.Contracts.Integrations.Models;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Modules.Cards.Application.Sources;
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Deal.Modules.Kanban.Application.Services;
|
using Deal.Modules.Kanban.Application.Services;
|
||||||
@@ -715,7 +716,7 @@ public sealed class CardsServiceTests
|
|||||||
var store = new FakeKanjStore();
|
var store = new FakeKanjStore();
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new FakeSettingsStore();
|
||||||
var ml = new FakeMlClient();
|
var ml = new FakeMlClient();
|
||||||
return (new CardsService(store, settings, ml, new FakeFileStorage()), store, settings, ml);
|
return (new CardsService(store, settings, ml, new TestFileStorage().Storage), store, settings, ml);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Доска с правилами (ContainerRulesDto) либо без них.
|
// Доска с правилами (ContainerRulesDto) либо без них.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Deal.Modules.Cards.Application.Dtos;
|
|||||||
using Deal.Modules.Cards.Application.Models;
|
using Deal.Modules.Cards.Application.Models;
|
||||||
using Deal.Modules.Cards.Application.Sources;
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Modules.Kanban.Application.Services;
|
using Deal.Modules.Kanban.Application.Services;
|
||||||
using Deal.Tests.Unit.Contracts;
|
using Deal.Tests.Unit.Contracts;
|
||||||
using Deal.Tests.Unit.Modules.Cards;
|
using Deal.Tests.Unit.Modules.Cards;
|
||||||
@@ -81,7 +82,7 @@ public sealed class CardMoverTests
|
|||||||
private static (CardMover Mover, FakeKanjStore Store) Create()
|
private static (CardMover Mover, FakeKanjStore Store) Create()
|
||||||
{
|
{
|
||||||
var store = new FakeKanjStore();
|
var store = new FakeKanjStore();
|
||||||
var cardsService = new CardsService(store, new FakeSettingsStore(), new FakeMlClient(), new FakeFileStorage());
|
var cardsService = new CardsService(store, new FakeSettingsStore(), new FakeMlClient(), new TestFileStorage().Storage);
|
||||||
return (new CardMover(cardsService), store);
|
return (new CardMover(cardsService), store);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Modules.Kanban.Application.Services;
|
using Deal.Modules.Kanban.Application.Services;
|
||||||
using Deal.Modules.Settings.Application.Models;
|
using Deal.Modules.Settings.Application.Models;
|
||||||
using Deal.Tests.Unit.Contracts;
|
using Deal.Tests.Unit.Contracts;
|
||||||
@@ -194,7 +195,7 @@ public sealed class CardsServiceRemindersTests
|
|||||||
settings.Preload(SettingsKeys.RemindersEnabled, "false");
|
settings.Preload(SettingsKeys.RemindersEnabled, "false");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (new CardsService(store, settings, new FakeMlClient(), new FakeFileStorage()), store, settings);
|
return (new CardsService(store, settings, new FakeMlClient(), new TestFileStorage().Storage), store, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Текущее время в epoch-мс (UTC) — для посева напоминаний в прошлом/будущем.
|
// Текущее время в epoch-мс (UTC) — для посева напоминаний в прошлом/будущем.
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public sealed class MlReviewServiceTests
|
|||||||
out CardsService cards)
|
out CardsService cards)
|
||||||
{
|
{
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new FakeSettingsStore();
|
||||||
cards = new CardsService(kanj, settings, ml, new FakeFileStorage());
|
cards = new CardsService(kanj, settings, ml, new TestFileStorage().Storage);
|
||||||
var processing = new PipelineProcessingService(pipeline, ml, new PipelineIngestService(pipeline));
|
var processing = new PipelineProcessingService(pipeline, ml, new PipelineIngestService(pipeline));
|
||||||
return new MlReviewService(pipeline, kanj, cards, processing, ml);
|
return new MlReviewService(pipeline, kanj, cards, processing, ml);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ public sealed class AdminTickOrchestratorTests
|
|||||||
var toastPublisher = new StorageToastPublisher(broker);
|
var toastPublisher = new StorageToastPublisher(broker);
|
||||||
var pumpGate = new PipelinePumpGate();
|
var pumpGate = new PipelinePumpGate();
|
||||||
FakeKanjStore reminderStore = withThrowingReminderCheck ? new ThrowingDueKanjStore() : kanjStore;
|
FakeKanjStore reminderStore = withThrowingReminderCheck ? new ThrowingDueKanjStore() : kanjStore;
|
||||||
var cardsService = new CardsService(reminderStore, settings, mlClient, new FakeFileStorage());
|
var cardsService = new CardsService(reminderStore, settings, mlClient, new TestFileStorage().Storage);
|
||||||
var orchestrator = new AdminTickOrchestrator(
|
var orchestrator = new AdminTickOrchestrator(
|
||||||
tickService, processing, worker, cardsService, toastPublisher, broker, pumpGate,
|
tickService, processing, worker, cardsService, toastPublisher, broker, pumpGate,
|
||||||
NullLogger<AdminTickOrchestrator>.Instance);
|
NullLogger<AdminTickOrchestrator>.Instance);
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ public sealed class CardReclassifierTests
|
|||||||
var aiClassifier = new TestAiClassifier();
|
var aiClassifier = new TestAiClassifier();
|
||||||
var fieldsParser = new LocalFieldsParser(settings);
|
var fieldsParser = new LocalFieldsParser(settings);
|
||||||
var composer = new CardComposer(store, settings);
|
var composer = new CardComposer(store, settings);
|
||||||
var cardsService = new CardsService(store, settings, mlClient, new FakeFileStorage());
|
var cardsService = new CardsService(store, settings, mlClient, new TestFileStorage().Storage);
|
||||||
var gate = new ReclassifyGate();
|
var gate = new ReclassifyGate();
|
||||||
var reclassifier = new CardReclassifier(
|
var reclassifier = new CardReclassifier(
|
||||||
store, settings, aiClassifier.Classifier, fieldsParser, composer, cardsService, mlClient, gate);
|
store, settings, aiClassifier.Classifier, fieldsParser, composer, cardsService, mlClient, gate);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public sealed class CardsServiceFilesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Add_ImageMime_DetectsKindByMimeWritesObjectAndMeta()
|
public async Task Add_ImageMime_DetectsKindByMimeWritesObjectAndMeta()
|
||||||
{
|
{
|
||||||
(CardsService service, FakeKanjStore store, FakeFileStorage storage) = Create();
|
(CardsService service, FakeKanjStore store, TestFileStorage storage) = Create();
|
||||||
store.SeedCard(Card("c_1", updatedAtMs: 1));
|
store.SeedCard(Card("c_1", updatedAtMs: 1));
|
||||||
byte[] content = Encoding.UTF8.GetBytes("данные-картинки");
|
byte[] content = Encoding.UTF8.GetBytes("данные-картинки");
|
||||||
using MemoryStream stream = new(content);
|
using MemoryStream stream = new(content);
|
||||||
@@ -57,7 +57,7 @@ public sealed class CardsServiceFilesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Add_TwoFiles_AppendsPreservingOrderAndBothObjects()
|
public async Task Add_TwoFiles_AppendsPreservingOrderAndBothObjects()
|
||||||
{
|
{
|
||||||
(CardsService service, FakeKanjStore store, FakeFileStorage storage) = Create();
|
(CardsService service, FakeKanjStore store, TestFileStorage storage) = Create();
|
||||||
store.SeedCard(Card("c_1"));
|
store.SeedCard(Card("c_1"));
|
||||||
|
|
||||||
CardFileDto? first = await service.AddFileAsync(
|
CardFileDto? first = await service.AddFileAsync(
|
||||||
@@ -76,7 +76,7 @@ public sealed class CardsServiceFilesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Add_CardMissing_ReturnsNullAndDoesNotWriteObject()
|
public async Task Add_CardMissing_ReturnsNullAndDoesNotWriteObject()
|
||||||
{
|
{
|
||||||
(CardsService service, FakeKanjStore store, FakeFileStorage storage) = Create();
|
(CardsService service, FakeKanjStore store, TestFileStorage storage) = Create();
|
||||||
|
|
||||||
CardFileDto? entry = await service.AddFileAsync(
|
CardFileDto? entry = await service.AddFileAsync(
|
||||||
"c_missing", "photo.png", "image/png", new MemoryStream("x"u8.ToArray()), 1, CancellationToken.None);
|
"c_missing", "photo.png", "image/png", new MemoryStream("x"u8.ToArray()), 1, CancellationToken.None);
|
||||||
@@ -105,7 +105,7 @@ public sealed class CardsServiceFilesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Add_FileNameWithPathAndQuoteChars_SanitizesObjectKeyButKeepsMetaName()
|
public async Task Add_FileNameWithPathAndQuoteChars_SanitizesObjectKeyButKeepsMetaName()
|
||||||
{
|
{
|
||||||
(CardsService service, FakeKanjStore store, FakeFileStorage storage) = Create();
|
(CardsService service, FakeKanjStore store, TestFileStorage storage) = Create();
|
||||||
store.SeedCard(Card("c_1"));
|
store.SeedCard(Card("c_1"));
|
||||||
string rawName = "..\\файл\"отчёта v2.pdf";
|
string rawName = "..\\файл\"отчёта v2.pdf";
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ public sealed class CardsServiceFilesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Add_StreamPositionNotZero_StoresWholeContent()
|
public async Task Add_StreamPositionNotZero_StoresWholeContent()
|
||||||
{
|
{
|
||||||
(CardsService service, FakeKanjStore store, FakeFileStorage storage) = Create();
|
(CardsService service, FakeKanjStore store, TestFileStorage storage) = Create();
|
||||||
store.SeedCard(Card("c_1"));
|
store.SeedCard(Card("c_1"));
|
||||||
byte[] content = Encoding.UTF8.GetBytes("полное-содержимое-файла");
|
byte[] content = Encoding.UTF8.GetBytes("полное-содержимое-файла");
|
||||||
using MemoryStream stream = new(content) { Position = 5 }; // эндпоинт мог прочитать поток раньше
|
using MemoryStream stream = new(content) { Position = 5 }; // эндпоинт мог прочитать поток раньше
|
||||||
@@ -177,12 +177,12 @@ public sealed class CardsServiceFilesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Remove_Existing_DeletesObjectRemovesMetaAndReturnsCard()
|
public async Task Remove_Existing_DeletesObjectRemovesMetaAndReturnsCard()
|
||||||
{
|
{
|
||||||
(CardsService service, FakeKanjStore store, FakeFileStorage storage) = Create();
|
(CardsService service, FakeKanjStore store, TestFileStorage storage) = Create();
|
||||||
var first = new CardFileDto("pf_1", "tz.pdf", 100, "document", "Документ", "projects/c_1/1710000000001_tz.pdf");
|
var first = new CardFileDto("pf_1", "tz.pdf", 100, "document", "Документ", "projects/c_1/1710000000001_tz.pdf");
|
||||||
var second = new CardFileDto("pf_2", "photo.png", 200, "image", "Изображение", "projects/c_1/1710000000002_photo.png");
|
var second = new CardFileDto("pf_2", "photo.png", 200, "image", "Изображение", "projects/c_1/1710000000002_photo.png");
|
||||||
store.SeedCard(Card("c_1", updatedAtMs: 1) with { Files = new[] { first, second } });
|
store.SeedCard(Card("c_1", updatedAtMs: 1) with { Files = new[] { first, second } });
|
||||||
await storage.PutAsync(first.ObjectKey, new MemoryStream("t"u8.ToArray()), "application/pdf", CancellationToken.None);
|
await storage.Storage.PutAsync(first.ObjectKey, new MemoryStream("t"u8.ToArray()), "application/pdf", CancellationToken.None);
|
||||||
await storage.PutAsync(second.ObjectKey, new MemoryStream("p"u8.ToArray()), "image/png", CancellationToken.None);
|
await storage.Storage.PutAsync(second.ObjectKey, new MemoryStream("p"u8.ToArray()), "image/png", CancellationToken.None);
|
||||||
|
|
||||||
CardDto? card = await service.RemoveFileAsync("c_1", "pf_1", CancellationToken.None);
|
CardDto? card = await service.RemoveFileAsync("c_1", "pf_1", CancellationToken.None);
|
||||||
|
|
||||||
@@ -199,7 +199,7 @@ public sealed class CardsServiceFilesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Remove_UnknownFileId_ReturnsCardUnchangedWithoutStorageDelete()
|
public async Task Remove_UnknownFileId_ReturnsCardUnchangedWithoutStorageDelete()
|
||||||
{
|
{
|
||||||
(CardsService service, FakeKanjStore store, FakeFileStorage storage) = Create();
|
(CardsService service, FakeKanjStore store, TestFileStorage storage) = Create();
|
||||||
var file = new CardFileDto("pf_1", "tz.pdf", 100, "document", "Документ", "projects/c_1/1710000000001_tz.pdf");
|
var file = new CardFileDto("pf_1", "tz.pdf", 100, "document", "Документ", "projects/c_1/1710000000001_tz.pdf");
|
||||||
store.SeedCard(Card("c_1") with { Files = new[] { file } });
|
store.SeedCard(Card("c_1") with { Files = new[] { file } });
|
||||||
|
|
||||||
@@ -214,7 +214,7 @@ public sealed class CardsServiceFilesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Remove_EntryWithEmptyObjectKey_SkipsStorageDelete()
|
public async Task Remove_EntryWithEmptyObjectKey_SkipsStorageDelete()
|
||||||
{
|
{
|
||||||
(CardsService service, FakeKanjStore store, FakeFileStorage storage) = Create();
|
(CardsService service, FakeKanjStore store, TestFileStorage storage) = Create();
|
||||||
store.SeedCard(Card("c_1")
|
store.SeedCard(Card("c_1")
|
||||||
with { Files = new[] { new CardFileDto("pf_mock", "meta-only.pdf", 10, "document", "Документ", ObjectKey: string.Empty) } });
|
with { Files = new[] { new CardFileDto("pf_mock", "meta-only.pdf", 10, "document", "Документ", ObjectKey: string.Empty) } });
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ public sealed class CardsServiceFilesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Remove_CardMissing_ReturnsNullWithoutStorageDelete()
|
public async Task Remove_CardMissing_ReturnsNullWithoutStorageDelete()
|
||||||
{
|
{
|
||||||
(CardsService service, _, FakeFileStorage storage) = Create();
|
(CardsService service, _, TestFileStorage storage) = Create();
|
||||||
|
|
||||||
CardDto? card = await service.RemoveFileAsync("c_missing", "pf_1", CancellationToken.None);
|
CardDto? card = await service.RemoveFileAsync("c_missing", "pf_1", CancellationToken.None);
|
||||||
|
|
||||||
@@ -238,11 +238,11 @@ public sealed class CardsServiceFilesTests
|
|||||||
|
|
||||||
// ─── Хелперы ──────────────────────────────────────────────────────────
|
// ─── Хелперы ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private static (CardsService Service, FakeKanjStore Store, FakeFileStorage Storage) Create()
|
private static (CardsService Service, FakeKanjStore Store, TestFileStorage Storage) Create()
|
||||||
{
|
{
|
||||||
var store = new FakeKanjStore();
|
var store = new FakeKanjStore();
|
||||||
var storage = new FakeFileStorage();
|
var storage = new TestFileStorage();
|
||||||
return (new CardsService(store, new FakeSettingsStore(), new FakeMlClient(), storage), store, storage);
|
return (new CardsService(store, new FakeSettingsStore(), new FakeMlClient(), storage.Storage), store, storage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Карточка с полями по умолчанию (planned, CreatedAtMs=1, UpdatedAtMs=1; Files пуст).
|
// Карточка с полями по умолчанию (planned, CreatedAtMs=1, UpdatedAtMs=1; Files пуст).
|
||||||
|
|||||||
@@ -536,7 +536,7 @@ public sealed class CardsServiceSelectedTests
|
|||||||
var store = new FakeKanjStore();
|
var store = new FakeKanjStore();
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new FakeSettingsStore();
|
||||||
var ml = new FakeMlClient();
|
var ml = new FakeMlClient();
|
||||||
return (new CardsService(store, settings, ml, new FakeFileStorage()), store, settings, ml);
|
return (new CardsService(store, settings, ml, new TestFileStorage().Storage), store, settings, ml);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Тело PATCH из пар «ключ → значение» (presence = наличие пары; null — явный JSON-null).
|
// Тело PATCH из пар «ключ → значение» (presence = наличие пары; null — явный JSON-null).
|
||||||
|
|||||||
@@ -323,7 +323,7 @@ public sealed class StorageTickSchedulerTests
|
|||||||
services.AddScoped<ISettingsStore>(provider => settingsByTenant[TenantOf(provider)]);
|
services.AddScoped<ISettingsStore>(provider => settingsByTenant[TenantOf(provider)]);
|
||||||
services.AddScoped<IPipelineStore>(provider => pipelineStoresByTenant[TenantOf(provider)]);
|
services.AddScoped<IPipelineStore>(provider => pipelineStoresByTenant[TenantOf(provider)]);
|
||||||
services.AddSingleton<IMlClient>(new FakeMlClient());
|
services.AddSingleton<IMlClient>(new FakeMlClient());
|
||||||
services.AddSingleton<IFileStorage>(new FakeFileStorage());
|
services.AddSingleton<IFileStorage>(new TestFileStorage().Storage);
|
||||||
services.AddScoped<PipelineIngestService>();
|
services.AddScoped<PipelineIngestService>();
|
||||||
services.AddScoped<PipelineProcessingService>();
|
services.AddScoped<PipelineProcessingService>();
|
||||||
services.AddScoped<StorageTickService>();
|
services.AddScoped<StorageTickService>();
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
using Deal.Contracts.Integrations.Abstractions;
|
||||||
|
using Deal.Contracts.Integrations.Models;
|
||||||
|
using NSubstitute;
|
||||||
|
|
||||||
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Подставка <see cref="IFileStorage"/> на словарях: сервисы получают NSubstitute-подставку
|
||||||
|
/// (<see cref="Storage"/>), тесты сеют/проверяют объекты через <see cref="PutObject"/>,
|
||||||
|
/// <see cref="ContentOf"/>, <see cref="StoredObjectKeys"/> и <see cref="DeletedKeys"/>.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TestFileStorage
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, byte[]> _objects = new(StringComparer.Ordinal);
|
||||||
|
private readonly Dictionary<string, string> _contentTypes = new(StringComparer.Ordinal);
|
||||||
|
private readonly List<string> _deletedKeys = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Подставка порта файлового хранилища (создаётся в конструкторе).
|
||||||
|
/// </summary>
|
||||||
|
public IFileStorage Storage { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// objectKey всех объектов, сохранённых на данный момент
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string> StoredObjectKeys => _objects.Keys.ToList();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// objectKey всех удалений в порядке вызовов DeleteAsync
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string> DeletedKeys => _deletedKeys.ToList();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт подставку с пустым хранилищем.
|
||||||
|
/// </summary>
|
||||||
|
public TestFileStorage()
|
||||||
|
{
|
||||||
|
Storage = Substitute.For<IFileStorage>();
|
||||||
|
Storage.PutAsync(Arg.Any<string>(), Arg.Any<Stream>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => Put(ci.ArgAt<string>(0), ci.ArgAt<Stream>(1), ci.ArgAt<string>(2)));
|
||||||
|
Storage.GetAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => Get(ci.ArgAt<string>(0)));
|
||||||
|
Storage.StatAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => Stat(ci.ArgAt<string>(0)));
|
||||||
|
Storage.When(s => s.DeleteAsync(Arg.Any<string>(), Arg.Any<CancellationToken>()))
|
||||||
|
.Do(ci => Delete(ci.ArgAt<string>(0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Кладёт объект напрямую, минуя порт
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="objectKey">Ключ объекта (opaque).</param>
|
||||||
|
/// <param name="content">Содержимое.</param>
|
||||||
|
/// <param name="contentType">MIME-тип.</param>
|
||||||
|
public void PutObject(string objectKey, byte[] content, string contentType = "application/octet-stream")
|
||||||
|
{
|
||||||
|
_objects[objectKey] = content;
|
||||||
|
_contentTypes[objectKey] = contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Содержимое сохранённого объекта по ключу либо null — объекта нет
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="objectKey">Ключ объекта (opaque).</param>
|
||||||
|
public byte[]? ContentOf(string objectKey)
|
||||||
|
{
|
||||||
|
return _objects.TryGetValue(objectKey, out byte[]? content) ? content : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string Put(string objectKey, Stream content, string contentType)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(content);
|
||||||
|
|
||||||
|
using MemoryStream buffer = new();
|
||||||
|
if (content.CanSeek && content.Position != 0)
|
||||||
|
{
|
||||||
|
content.Position = 0; // контракт порта: Put читает всё содержимое с позиции 0 (Ruling T6)
|
||||||
|
}
|
||||||
|
|
||||||
|
content.CopyTo(buffer);
|
||||||
|
_objects[objectKey] = buffer.ToArray();
|
||||||
|
_contentTypes[objectKey] = contentType;
|
||||||
|
return objectKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Stream? Get(string objectKey)
|
||||||
|
{
|
||||||
|
return _objects.TryGetValue(objectKey, out byte[]? content) ? new MemoryStream(content) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FileMeta? Stat(string objectKey)
|
||||||
|
{
|
||||||
|
if (!_objects.TryGetValue(objectKey, out byte[]? content))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
string contentType = _contentTypes.TryGetValue(objectKey, out string? stored) ? stored : string.Empty;
|
||||||
|
return new FileMeta(objectKey, content.Length, contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Delete(string objectKey)
|
||||||
|
{
|
||||||
|
_deletedKeys.Add(objectKey);
|
||||||
|
_objects.Remove(objectKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user