[{"data":1,"prerenderedAt":32281},["ShallowReactive",2],{"navigation_docs":3,"-aws-s3":3338,"-aws-s3-surround":32276},[4,1707,1904,2358,2539,2608,2815,2937,2987,3044,3078,3204,3281,3334],{"title":5,"icon":6,"path":7,"stem":8,"children":9},"C#","i-devicon-csharp","\u002Fcsharp","01.csharp",[10,13,60,90,120,202,219,253,379,404,457,650,1364,1654,1703],{"title":11,"path":7,"stem":12},"C# та .NET","01.csharp\u002Findex",{"title":14,"icon":15,"path":16,"stem":17,"children":18,"page":59},"Fundamentals","i-lucide-book-open","\u002Fcsharp\u002Ffundamentals","01.csharp\u002F01.fundamentals",[19,23,27,31,35,39,43,47,51,55],{"title":20,"path":21,"stem":22},"Вступ до екосистеми .NET","\u002Fcsharp\u002Ffundamentals\u002Fintroduction-to-ecosystem","01.csharp\u002F01.fundamentals\u002F01.introduction-to-ecosystem",{"title":24,"path":25,"stem":26},"Структура програми на C#","\u002Fcsharp\u002Ffundamentals\u002Fprogram-structure","01.csharp\u002F01.fundamentals\u002F02.program-structure",{"title":28,"path":29,"stem":30},"Змінні та Типи Даних","\u002Fcsharp\u002Ffundamentals\u002Fvariables-data-types","01.csharp\u002F01.fundamentals\u002F03.variables-data-types",{"title":32,"path":33,"stem":34},"Масиви","\u002Fcsharp\u002Ffundamentals\u002Farrays","01.csharp\u002F01.fundamentals\u002F04.arrays",{"title":36,"path":37,"stem":38},"Strings & Text Handling","\u002Fcsharp\u002Ffundamentals\u002Fstrings-text-handling","01.csharp\u002F01.fundamentals\u002F05.strings-text-handling",{"title":40,"path":41,"stem":42},"Дати і Час","\u002Fcsharp\u002Ffundamentals\u002Fdates-time-handling","01.csharp\u002F01.fundamentals\u002F06.dates-time-handling",{"title":44,"path":45,"stem":46},"Потік Керування","\u002Fcsharp\u002Ffundamentals\u002Fcontrol-flow","01.csharp\u002F01.fundamentals\u002F07.control-flow",{"title":48,"path":49,"stem":50},"Методи","\u002Fcsharp\u002Ffundamentals\u002Fmethods","01.csharp\u002F01.fundamentals\u002F08.methods",{"title":52,"path":53,"stem":54},"Основи Відлагодження","\u002Fcsharp\u002Ffundamentals\u002Fdebugging-basics","01.csharp\u002F01.fundamentals\u002F09.debugging-basics",{"title":56,"path":57,"stem":58},"Інтерактивна Консоль (Classic)","\u002Fcsharp\u002Ffundamentals\u002Finteractive-console","01.csharp\u002F01.fundamentals\u002F10.interactive-console",false,{"title":61,"icon":62,"path":63,"stem":64,"children":65,"page":59},"OOP","i-lucide-box","\u002Fcsharp\u002Foop","01.csharp\u002F02.oop",[66,70,74,78,82,86],{"title":67,"path":68,"stem":69},"Package Management (Управління Пакетами)","\u002Fcsharp\u002Foop\u002Fpackage-management","01.csharp\u002F02.oop\u002F01.package-management",{"title":71,"path":72,"stem":73},"Класи та Об'єкти","\u002Fcsharp\u002Foop\u002Fclasses-objects","01.csharp\u002F02.oop\u002F02.classes-objects",{"title":75,"path":76,"stem":77},"Властивості та Поля","\u002Fcsharp\u002Foop\u002Fproperties-fields","01.csharp\u002F02.oop\u002F03.properties-fields",{"title":79,"path":80,"stem":81},"Стовпи ООП","\u002Fcsharp\u002Foop\u002Foop-pillars","01.csharp\u002F02.oop\u002F04.oop-pillars",{"title":83,"path":84,"stem":85},"Advanced Types","\u002Fcsharp\u002Foop\u002Fadvanced-types","01.csharp\u002F02.oop\u002F05.advanced-types",{"title":87,"path":88,"stem":89},"Namespaces (Простори Імен)","\u002Fcsharp\u002Foop\u002Fnamespaces","01.csharp\u002F02.oop\u002F06.namespaces",{"title":91,"icon":92,"path":93,"stem":94,"children":95,"page":59},"Advanced Core","i-lucide-zap","\u002Fcsharp\u002Fadvanced-core","01.csharp\u002F03.advanced-core",[96,100,104,108,112,116],{"title":97,"path":98,"stem":99},"Generics (Узагальнення)","\u002Fcsharp\u002Fadvanced-core\u002Fgenerics","01.csharp\u002F03.advanced-core\u002F01.generics",{"title":101,"path":102,"stem":103},"Делегати, Події та Лямбда-вирази","\u002Fcsharp\u002Fadvanced-core\u002Fdelegates-events-lambdas","01.csharp\u002F03.advanced-core\u002F02.delegates-events-lambdas",{"title":105,"path":106,"stem":107},"Interfaces Deep Dive (Інтерфейси: Поглиблений Розгляд)","\u002Fcsharp\u002Fadvanced-core\u002Finterfaces-deep-dive","01.csharp\u002F03.advanced-core\u002F03.interfaces-deep-dive",{"title":109,"path":110,"stem":111},"Обробка Винятків","\u002Fcsharp\u002Fadvanced-core\u002Fexception-handling","01.csharp\u002F03.advanced-core\u002F04.exception-handling",{"title":113,"path":114,"stem":115},"Pattern Matching","\u002Fcsharp\u002Fadvanced-core\u002Fpattern-matching","01.csharp\u002F03.advanced-core\u002F05.pattern-matching",{"title":117,"path":118,"stem":119},"Додаткові Можливості C#","\u002Fcsharp\u002Fadvanced-core\u002Fadditional-features","01.csharp\u002F03.advanced-core\u002F06.additional-features",{"title":121,"icon":122,"path":123,"stem":124,"children":125,"page":59},"Architecture Best Practices","i-lucide-building-2","\u002Fcsharp\u002Farchitecture-best-practices","01.csharp\u002F04.architecture-best-practices",[126,130,149,153,157,161,165,169],{"title":127,"path":128,"stem":129},"Software Design Principles (Частина 1)","\u002Fcsharp\u002Farchitecture-best-practices\u002Fsoftware-design-principles","01.csharp\u002F04.architecture-best-practices\u002F01.software-design-principles",{"title":131,"icon":132,"path":133,"stem":134,"children":135,"page":59},"Design Patterns","i-lucide-folder","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdesign-patterns","01.csharp\u002F04.architecture-best-practices\u002F02.design-patterns",[136],{"title":137,"icon":132,"path":138,"stem":139,"children":140,"page":59},"Creational","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdesign-patterns\u002Fcreational","01.csharp\u002F04.architecture-best-practices\u002F02.design-patterns\u002Fcreational",[141,145],{"title":142,"path":143,"stem":144},"Singleton (Одинак)","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdesign-patterns\u002Fcreational\u002Fsingleton","01.csharp\u002F04.architecture-best-practices\u002F02.design-patterns\u002Fcreational\u002F01.singleton",{"title":146,"path":147,"stem":148},"Builder (Будівельник)","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdesign-patterns\u002Fcreational\u002Fbuilder","01.csharp\u002F04.architecture-best-practices\u002F02.design-patterns\u002Fcreational\u002F02.builder",{"title":150,"path":151,"stem":152},"Building Professional CLIs","\u002Fcsharp\u002Farchitecture-best-practices\u002Fbuilding-professional-clis","01.csharp\u002F04.architecture-best-practices\u002F03.building-professional-clis",{"title":154,"path":155,"stem":156},"Validation & Flow Control","\u002Fcsharp\u002Farchitecture-best-practices\u002Fvalidation-flow-control","01.csharp\u002F04.architecture-best-practices\u002F04.validation-flow-control",{"title":158,"path":159,"stem":160},"The Modern .NET Host (Microsoft.Extensions)","\u002Fcsharp\u002Farchitecture-best-practices\u002Fmodern-dotnet-host","01.csharp\u002F04.architecture-best-practices\u002F05.modern-dotnet-host",{"title":162,"path":163,"stem":164},"Data Mapper: Repository та DAO патерни (Частина 1)","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdata-mapper-part1","01.csharp\u002F04.architecture-best-practices\u002F06.data-mapper-part1",{"title":166,"path":167,"stem":168},"Data Mapper: Repository та DAO патерни (Частина 2)","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdata-mapper-part2","01.csharp\u002F04.architecture-best-practices\u002F07.data-mapper-part2",{"title":170,"icon":132,"path":171,"stem":172,"children":173,"page":59},"Di Ioc","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdi-ioc","01.csharp\u002F04.architecture-best-practices\u002F08.di-ioc",[174,178,182,186,190,194,198],{"title":175,"path":176,"stem":177},"Проблема залежностей та Інверсія Контролю","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdi-ioc\u002Fthe-dependency-problem","01.csharp\u002F04.architecture-best-practices\u002F08.di-ioc\u002F01.the-dependency-problem",{"title":179,"path":180,"stem":181},"Будуємо власний Service Container","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdi-ioc\u002Fbuild-your-own-container","01.csharp\u002F04.architecture-best-practices\u002F08.di-ioc\u002F02.build-your-own-container",{"title":183,"path":184,"stem":185},"Service Locator: Паттерн та Анти-паттерн","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdi-ioc\u002Fservice-locator-pattern","01.csharp\u002F04.architecture-best-practices\u002F08.di-ioc\u002F03.service-locator-pattern",{"title":187,"path":188,"stem":189},"Паттерни Dependency Injection","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdi-ioc\u002Fdependency-injection-patterns","01.csharp\u002F04.architecture-best-practices\u002F08.di-ioc\u002F04.dependency-injection-patterns",{"title":191,"path":192,"stem":193},"Microsoft DI: IServiceCollection та IServiceProvider","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdi-ioc\u002Fmicrosoft-di-deep-dive","01.csharp\u002F04.architecture-best-practices\u002F08.di-ioc\u002F05.microsoft-di-deep-dive",{"title":195,"path":196,"stem":197},"Service Lifetimes та Scopes","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdi-ioc\u002Fservice-lifetimes-and-scopes","01.csharp\u002F04.architecture-best-practices\u002F08.di-ioc\u002F06.service-lifetimes-and-scopes",{"title":199,"path":200,"stem":201},"DI Анти-паттерни та Найкращі Практики","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdi-ioc\u002Fdi-anti-patterns-and-best-practices","01.csharp\u002F04.architecture-best-practices\u002F08.di-ioc\u002F07.di-anti-patterns-and-best-practices",{"title":203,"icon":132,"path":204,"stem":205,"children":206,"page":59},"Standard Library","\u002Fcsharp\u002Fstandard-library","01.csharp\u002F05.standard-library",[207,211,215],{"title":208,"path":209,"stem":210},"Collections (Колекції)","\u002Fcsharp\u002Fstandard-library\u002Fcollections","01.csharp\u002F05.standard-library\u002F01.collections",{"title":212,"path":213,"stem":214},"High Performance Types (Високопродуктивні Типи)","\u002Fcsharp\u002Fstandard-library\u002Fhigh-performance-types","01.csharp\u002F05.standard-library\u002F02.high-performance-types",{"title":216,"path":217,"stem":218},"LINQ (Language Integrated Query)","\u002Fcsharp\u002Fstandard-library\u002Flinq","01.csharp\u002F05.standard-library\u002F03.linq",{"title":220,"icon":221,"path":222,"stem":223,"children":224,"page":59},"System Internals Concurrency","i-lucide-server","\u002Fcsharp\u002Fsystem-internals-concurrency","01.csharp\u002F06.system-internals-concurrency",[225,229,233,237,241,245,249],{"title":226,"path":227,"stem":228},"Memory Management","\u002Fcsharp\u002Fsystem-internals-concurrency\u002Fmemory-management","01.csharp\u002F06.system-internals-concurrency\u002F01.memory-management",{"title":230,"path":231,"stem":232},"Reflection API: System.Type та Метадані","\u002Fcsharp\u002Fsystem-internals-concurrency\u002Freflection-fundamentals","01.csharp\u002F06.system-internals-concurrency\u002F02.reflection-fundamentals",{"title":234,"path":235,"stem":236},"Attributes та Dynamic Language Runtime","\u002Fcsharp\u002Fsystem-internals-concurrency\u002Fattributes-dynamic","01.csharp\u002F06.system-internals-concurrency\u002F03.attributes-dynamic",{"title":238,"path":239,"stem":240},"Expression Trees: Швидка Альтернатива Рефлексії","\u002Fcsharp\u002Fsystem-internals-concurrency\u002Fexpression-trees-compiled","01.csharp\u002F06.system-internals-concurrency\u002F04.expression-trees-compiled",{"title":242,"path":243,"stem":244},"Source Generators: Compile-Time Code Generation","\u002Fcsharp\u002Fsystem-internals-concurrency\u002Fsource-generators","01.csharp\u002F06.system-internals-concurrency\u002F05.source-generators",{"title":246,"path":247,"stem":248},"Multithreading Fundamentals","\u002Fcsharp\u002Fsystem-internals-concurrency\u002Fmultithreading-fundamentals","01.csharp\u002F06.system-internals-concurrency\u002F06.multithreading-fundamentals",{"title":250,"path":251,"stem":252},"Synchronization Primitives","\u002Fcsharp\u002Fsystem-internals-concurrency\u002Fsynchronization-primitives","01.csharp\u002F06.system-internals-concurrency\u002F07.synchronization-primitives",{"title":254,"icon":255,"path":256,"stem":257,"children":258,"page":59},"System Programming Windows","i-lucide-cpu","\u002Fcsharp\u002Fsystem-programming-windows","01.csharp\u002F07.system-programming-windows",[259,263,267,271,275,279,283,287,291,295,299,303,307,311,315,319,323,327,331,335,339,343,347,351,355,359,363,367,371,375],{"title":260,"path":261,"stem":262},"Як Працює Операційна Система","\u002Fcsharp\u002Fsystem-programming-windows\u002Fhow-os-works","01.csharp\u002F07.system-programming-windows\u002F01.how-os-works",{"title":264,"path":265,"stem":266},"Процеси в .NET — API та Запуск","\u002Fcsharp\u002Fsystem-programming-windows\u002Fprocesses-in-dotnet","01.csharp\u002F07.system-programming-windows\u002F02.processes-in-dotnet",{"title":268,"path":269,"stem":270},"Процеси в .NET — IPC та Міжпроцесна Комунікація","\u002Fcsharp\u002Fsystem-programming-windows\u002F02a.processes-ipc","01.csharp\u002F07.system-programming-windows\u002F02a.processes-ipc",{"title":272,"path":273,"stem":274},"Application Domains та Збірки — AppDomain і AssemblyLoadContext","\u002Fcsharp\u002Fsystem-programming-windows\u002Fappdomains-assemblies","01.csharp\u002F07.system-programming-windows\u002F03.appdomains-assemblies",{"title":276,"path":277,"stem":278},"Application Domains та Збірки — Plug-in Система з Hot-Reload","\u002Fcsharp\u002Fsystem-programming-windows\u002F03a.appdomains-plugin-system","01.csharp\u002F07.system-programming-windows\u002F03a.appdomains-plugin-system",{"title":280,"path":281,"stem":282},"Потоки — Основи та API Thread","\u002Fcsharp\u002Fsystem-programming-windows\u002Fthread-fundamentals","01.csharp\u002F07.system-programming-windows\u002F04.thread-fundamentals",{"title":284,"path":285,"stem":286},"Потоки — Lifecycle, Пріоритети та Безпечне Завершення","\u002Fcsharp\u002Fsystem-programming-windows\u002F04a.thread-lifecycle-priorities","01.csharp\u002F07.system-programming-windows\u002F04a.thread-lifecycle-priorities",{"title":288,"path":289,"stem":290},"Проблеми Спільного Стану — Race Condition та Data Race","\u002Fcsharp\u002Fsystem-programming-windows\u002Fshared-state-problems","01.csharp\u002F07.system-programming-windows\u002F05.shared-state-problems",{"title":292,"path":293,"stem":294},"Проблеми Спільного Стану — Memory Model та volatile","\u002Fcsharp\u002Fsystem-programming-windows\u002F05a.shared-state-memory-model","01.csharp\u002F07.system-programming-windows\u002F05a.shared-state-memory-model",{"title":296,"path":297,"stem":298},"Синхронізація — Monitor, lock та еволюція примітивів","\u002Fcsharp\u002Fsystem-programming-windows\u002Fsynchronization-fundamentals","01.csharp\u002F07.system-programming-windows\u002F06.synchronization-fundamentals",{"title":300,"path":301,"stem":302},"Синхронізація — Наскрізний Приклад та Deadlock Detection","\u002Fcsharp\u002Fsystem-programming-windows\u002F06a.synchronization-walkthrough","01.csharp\u002F07.system-programming-windows\u002F06a.synchronization-walkthrough",{"title":304,"path":305,"stem":306},"Синхронізація — Mutex, Semaphore та Event-Based Primitives","\u002Fcsharp\u002Fsystem-programming-windows\u002Fsynchronization-advanced","01.csharp\u002F07.system-programming-windows\u002F07.synchronization-advanced",{"title":308,"path":309,"stem":310},"Синхронізація — Interlocked, Volatile та Lock-Free Структури","\u002Fcsharp\u002Fsystem-programming-windows\u002F07a.synchronization-advanced-walkthrough","01.csharp\u002F07.system-programming-windows\u002F07a.synchronization-advanced-walkthrough",{"title":312,"path":313,"stem":314},"Interlocked, CAS та Lock-Free Структури","\u002Fcsharp\u002Fsystem-programming-windows\u002Finterlocked-cas-lockfree","01.csharp\u002F07.system-programming-windows\u002F08.interlocked-cas-lockfree",{"title":316,"path":317,"stem":318},"Volatile, Memory Model та Spinning","\u002Fcsharp\u002Fsystem-programming-windows\u002F08a.volatile-memory-model","01.csharp\u002F07.system-programming-windows\u002F08a.volatile-memory-model",{"title":320,"path":321,"stem":322},"ThreadPool — Пул Потоків для Ефективного Виконання","\u002Fcsharp\u002Fsystem-programming-windows\u002Fthread-pool","01.csharp\u002F07.system-programming-windows\u002F09.thread-pool",{"title":324,"path":325,"stem":326},"ThreadPool — Просунуті Сценарії та Внутрішня Будова","\u002Fcsharp\u002Fsystem-programming-windows\u002F09a.thread-pool-advanced","01.csharp\u002F07.system-programming-windows\u002F09a.thread-pool-advanced",{"title":328,"path":329,"stem":330},"Concurrent та Immutable Collections","\u002Fcsharp\u002Fsystem-programming-windows\u002Fconcurrent-collections","01.csharp\u002F07.system-programming-windows\u002F10.concurrent-collections",{"title":332,"path":333,"stem":334},"TPL, Task та Композиція — Від Thread до Task","\u002Fcsharp\u002Fsystem-programming-windows\u002Ftpl-parallel-plinq","01.csharp\u002F07.system-programming-windows\u002F11.tpl-parallel-plinq",{"title":336,"path":337,"stem":338},"Parallel Class та PLINQ — Data Parallelism","\u002Fcsharp\u002Fsystem-programming-windows\u002F11a.tpl-parallel-plinq-advanced","01.csharp\u002F07.system-programming-windows\u002F11a.tpl-parallel-plinq-advanced",{"title":340,"path":341,"stem":342},"Async\u002FAwait — Фундамент Асинхронного Програмування","\u002Fcsharp\u002Fsystem-programming-windows\u002Fasync-fundamentals","01.csharp\u002F07.system-programming-windows\u002F12.async-fundamentals",{"title":344,"path":345,"stem":346},"SynchronizationContext та ConfigureAwait — Контекст Виконання","\u002Fcsharp\u002Fsystem-programming-windows\u002Fasync-context-configureawait","01.csharp\u002F07.system-programming-windows\u002F13.async-context-configureawait",{"title":348,"path":349,"stem":350},"Async — Просунуті Паттерни","\u002Fcsharp\u002Fsystem-programming-windows\u002Fasync-advanced","01.csharp\u002F07.system-programming-windows\u002F14.async-advanced",{"title":352,"path":353,"stem":354},"System.Threading.Channels — Async Producer-Consumer","\u002Fcsharp\u002Fsystem-programming-windows\u002Fchannels","01.csharp\u002F07.system-programming-windows\u002F15.channels",{"title":356,"path":357,"stem":358},"Асинхронна Синхронізація","\u002Fcsharp\u002Fsystem-programming-windows\u002Fasync-synchronization","01.csharp\u002F07.system-programming-windows\u002F16.async-synchronization",{"title":360,"path":361,"stem":362},"Unsafe Code та Вказівники","\u002Fcsharp\u002Fsystem-programming-windows\u002Funsafe-code","01.csharp\u002F07.system-programming-windows\u002F17.unsafe-code",{"title":364,"path":365,"stem":366},"P\u002FInvoke та Windows API — Міст між .NET та Native Code","\u002Fcsharp\u002Fsystem-programming-windows\u002Fpinvoke-winapi","01.csharp\u002F07.system-programming-windows\u002F18.pinvoke-winapi",{"title":368,"path":369,"stem":370},"Реєстр Windows — Центральна База Конфігурації Системи","\u002Fcsharp\u002Fsystem-programming-windows\u002Fwindows-registry","01.csharp\u002F07.system-programming-windows\u002F19.windows-registry",{"title":372,"path":373,"stem":374},"Windows Hooks, Hotkeys та Services — Глибока Інтеграція з ОС","\u002Fcsharp\u002Fsystem-programming-windows\u002Fwindows-hooks-services","01.csharp\u002F07.system-programming-windows\u002F20.windows-hooks-services",{"title":376,"path":377,"stem":378},"Системне Програмування C# (Windows) — 07.system-programming-windows","\u002Fcsharp\u002Fsystem-programming-windows\u002Fimplementation_plan","01.csharp\u002F07.system-programming-windows\u002Fimplementation_plan",{"title":380,"icon":132,"path":381,"stem":382,"children":383,"page":59},"Io","\u002Fcsharp\u002Fio","01.csharp\u002F08.io",[384,388,392,396,400],{"title":385,"path":386,"stem":387},"8.1.1. Основи роботи з файловою системою","\u002Fcsharp\u002Fio\u002Ffile-system-basics","01.csharp\u002F08.io\u002F01.file-system-basics",{"title":389,"path":390,"stem":391},"8.1.2. Потоки (Streams) та Серіалізація Даних","\u002Fcsharp\u002Fio\u002Fstreams-serialization","01.csharp\u002F08.io\u002F02.streams-serialization",{"title":393,"path":394,"stem":395},"8.2.1. JSON Serialization з System.Text.Json","\u002Fcsharp\u002Fio\u002Fjson-serialization","01.csharp\u002F08.io\u002F03.json-serialization",{"title":397,"path":398,"stem":399},"8.2.2. XML Serialization та LINQ to XML","\u002Fcsharp\u002Fio\u002Fxml-serialization","01.csharp\u002F08.io\u002F04.xml-serialization",{"title":401,"path":402,"stem":403},"8.2.3. Binary Serialization: MessagePack та Protocol Buffers","\u002Fcsharp\u002Fio\u002Fbinary-serialization","01.csharp\u002F08.io\u002F05.binary-serialization",{"title":405,"icon":132,"path":406,"stem":407,"children":408,"page":59},"Ado Net","\u002Fcsharp\u002Fado-net","01.csharp\u002F09.ado-net",[409,413,417,421,425,429,433,437,441,445,449,453],{"title":410,"path":411,"stem":412},"9.1. Введення в ADO.NET","\u002Fcsharp\u002Fado-net\u002Fintroduction-to-adonet","01.csharp\u002F09.ado-net\u002F01.introduction-to-adonet",{"title":414,"path":415,"stem":416},"9.2. Клас DbConnection — з'єднання з базою даних","\u002Fcsharp\u002Fado-net\u002Fconnection","01.csharp\u002F09.ado-net\u002F02.connection",{"title":418,"path":419,"stem":420},"9.3. Клас DbCommand — виконання SQL-запитів","\u002Fcsharp\u002Fado-net\u002Fcommand-and-queries","01.csharp\u002F09.ado-net\u002F03.command-and-queries",{"title":422,"path":423,"stem":424},"9.4. Клас DbDataReader — ефективне читання даних","\u002Fcsharp\u002Fado-net\u002Fdatareader","01.csharp\u002F09.ado-net\u002F04.datareader",{"title":426,"path":427,"stem":428},"9.5. Параметризовані запити та захист від SQL Injection","\u002Fcsharp\u002Fado-net\u002Fparameters-and-sql-injection","01.csharp\u002F09.ado-net\u002F05.parameters-and-sql-injection",{"title":430,"path":431,"stem":432},"9.6. Транзакції в ADO.NET","\u002Fcsharp\u002Fado-net\u002Ftransactions","01.csharp\u002F09.ado-net\u002F06.transactions",{"title":434,"path":435,"stem":436},"9.7. DbProviderFactory — провайдер-незалежний код","\u002Fcsharp\u002Fado-net\u002Fprovider-factory","01.csharp\u002F09.ado-net\u002F07.provider-factory",{"title":438,"path":439,"stem":440},"9.8. Асинхронний доступ до даних","\u002Fcsharp\u002Fado-net\u002Fasync-data-access","01.csharp\u002F09.ado-net\u002F08.async-data-access",{"title":442,"path":443,"stem":444},"9.9. Від'єднаний режим: DataSet, DataTable, DataRow","\u002Fcsharp\u002Fado-net\u002Fdisconnected-mode-dataset","01.csharp\u002F09.ado-net\u002F09.disconnected-mode-dataset",{"title":446,"path":447,"stem":448},"9.10. DataAdapter — міст між DataSet та базою даних","\u002Fcsharp\u002Fado-net\u002Fdata-adapter","01.csharp\u002F09.ado-net\u002F10.data-adapter",{"title":450,"path":451,"stem":452},"9.11. Data Mapper та Repository: Архітектура доступу до даних","\u002Fcsharp\u002Fado-net\u002Fdata-mapper-repository","01.csharp\u002F09.ado-net\u002F11.data-mapper-repository",{"title":454,"path":455,"stem":456},"9.12. Identity Map, Unit of Work та Specification Pattern","\u002Fcsharp\u002Fado-net\u002Fadvanced-patterns","01.csharp\u002F09.ado-net\u002F12.advanced-patterns",{"title":458,"icon":255,"path":459,"stem":460,"children":461,"page":59},"Ef Core","\u002Fcsharp\u002Fef-core","01.csharp\u002F10.ef-core",[462,466,470,474,478,482,486,490,494,498,502,506,510,514,518,522,526,532,538,542,546,550,554,558,562,566,570,574,578,582,586,590,594,598,602,606,610,614,618,622,626,630,634,638,642,646],{"title":463,"path":464,"stem":465},"Що таке ORM? Від SQL до об'єктів","\u002Fcsharp\u002Fef-core\u002Fwhat-is-orm","01.csharp\u002F10.ef-core\u002F01.what-is-orm",{"title":467,"path":468,"stem":469},"Перший проєкт — від нуля до CRUD","\u002Fcsharp\u002Fef-core\u002Ffirst-project","01.csharp\u002F10.ef-core\u002F02.first-project",{"title":471,"path":472,"stem":473},"DbContext — Серце EF Core","\u002Fcsharp\u002Fef-core\u002Fdbcontext-deep-dive","01.csharp\u002F10.ef-core\u002F03.dbcontext-deep-dive",{"title":475,"path":476,"stem":477},"Провайдери баз даних — Архітектура та Вибір СУБД","\u002Fcsharp\u002Fef-core\u002Fdatabase-providers","01.csharp\u002F10.ef-core\u002F04.database-providers",{"title":479,"path":480,"stem":481},"Конвенції EF Core — Магія без конфігурації","\u002Fcsharp\u002Fef-core\u002Fconventions","01.csharp\u002F10.ef-core\u002F05.conventions",{"title":483,"path":484,"stem":485},"Fluent API та Data Annotations — Явна конфігурація моделі","\u002Fcsharp\u002Fef-core\u002Ffluent-api-vs-annotations","01.csharp\u002F10.ef-core\u002F06.fluent-api-vs-annotations",{"title":487,"path":488,"stem":489},"Зв'язки — One-to-One та One-to-Many","\u002Fcsharp\u002Fef-core\u002Frelationships-basics","01.csharp\u002F10.ef-core\u002F07.relationships-basics",{"title":491,"path":492,"stem":493},"Зв'язки Advanced — Many-to-Many та Складні Сценарії","\u002Fcsharp\u002Fef-core\u002Frelationships-advanced","01.csharp\u002F10.ef-core\u002F08.relationships-advanced",{"title":495,"path":496,"stem":497},"Властивості — Типи, Конвертери, Компаратори (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fproperty-configuration-part1","01.csharp\u002F10.ef-core\u002F09.property-configuration-part1",{"title":499,"path":500,"stem":501},"Властивості — Value Comparers, Generators, Shadow Properties (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fproperty-configuration-part2","01.csharp\u002F10.ef-core\u002F09.property-configuration-part2",{"title":503,"path":504,"stem":505},"Складні типи — Owned Types та Complex Types (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fcomplex-types-owned-part1","01.csharp\u002F10.ef-core\u002F10.complex-types-owned-part1",{"title":507,"path":508,"stem":509},"Складні типи — Complex Types, Keyless Entities, Порівняння (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fcomplex-types-owned-part2","01.csharp\u002F10.ef-core\u002F10.complex-types-owned-part2",{"title":511,"path":512,"stem":513},"JSON Columns — Складні дані у JSON (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fjson-columns-part1","01.csharp\u002F10.ef-core\u002F11.json-columns-part1",{"title":515,"path":516,"stem":517},"JSON Columns — Value Comparers, Індекси, Провайдери (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fjson-columns-part2","01.csharp\u002F10.ef-core\u002F11.json-columns-part2",{"title":519,"path":520,"stem":521},"Успадкування — Абстрактні класи та TPH (Частина 1)","\u002Fcsharp\u002Fef-core\u002Finheritance-part1","01.csharp\u002F10.ef-core\u002F12.inheritance-part1",{"title":523,"path":524,"stem":525},"Успадкування — TPT, TPC та Порівняння Стратегій (Частина 2)","\u002Fcsharp\u002Fef-core\u002Finheritance-part2","01.csharp\u002F10.ef-core\u002F12.inheritance-part2",{"title":527,"path":528,"stem":529,"children":530},"Індекси, Обмеження та Схема (Частина 1)","\u002Fcsharp\u002Fef-core\u002Findexes-constraints-part1","01.csharp\u002F10.ef-core\u002F13.indexes-constraints-part1",[531],{"title":527,"path":528,"stem":529},{"title":533,"path":534,"stem":535,"children":536},"Індекси, Обмеження та Схема (Частина 2)","\u002Fcsharp\u002Fef-core\u002Findexes-constraints-part2","01.csharp\u002F10.ef-core\u002F13.indexes-constraints-part2",[537],{"title":533,"path":534,"stem":535},{"title":539,"path":540,"stem":541},"Seed Data — Початкові Дані (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fseeding-part1","01.csharp\u002F10.ef-core\u002F14.seeding-part1",{"title":543,"path":544,"stem":545},"Seed Data — SQL-скрипти, Bogus та Стратегії (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fseeding-part2","01.csharp\u002F10.ef-core\u002F14.seeding-part2",{"title":547,"path":548,"stem":549},"Global Query Filters — Глобальні Фільтри (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fglobal-query-filters-part1","01.csharp\u002F10.ef-core\u002F15.global-query-filters-part1",{"title":551,"path":552,"stem":553},"Global Query Filters — Підводні камені та Інтеграція (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fglobal-query-filters-part2","01.csharp\u002F10.ef-core\u002F15.global-query-filters-part2",{"title":555,"path":556,"stem":557},"LINQ-запити в EF Core (Частина 1)","\u002Fcsharp\u002Fef-core\u002Flinq-queries-part1","01.csharp\u002F10.ef-core\u002F16.linq-queries-part1",{"title":559,"path":560,"stem":561},"LINQ-запити в EF Core (Частина 2)","\u002Fcsharp\u002Fef-core\u002Flinq-queries-part2","01.csharp\u002F10.ef-core\u002F16.linq-queries-part2",{"title":563,"path":564,"stem":565},"Завантаження Пов'язаних Даних (Частина 1)","\u002Fcsharp\u002Fef-core\u002Floading-related-data-part1","01.csharp\u002F10.ef-core\u002F17.loading-related-data-part1",{"title":567,"path":568,"stem":569},"Завантаження Пов'язаних Даних (Частина 2)","\u002Fcsharp\u002Fef-core\u002Floading-related-data-part2","01.csharp\u002F10.ef-core\u002F17.loading-related-data-part2",{"title":571,"path":572,"stem":573},"Raw SQL, Views та Stored Procedures (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fraw-sql-part1","01.csharp\u002F10.ef-core\u002F18.raw-sql-part1",{"title":575,"path":576,"stem":577},"Raw SQL — Stored Procedures, DbFunction та Bulk Operations (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fraw-sql-part2","01.csharp\u002F10.ef-core\u002F18.raw-sql-part2",{"title":579,"path":580,"stem":581},"Продвинуті Запити — Compiled Queries, Bulk та Оптимізація (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fadvanced-queries-part1","01.csharp\u002F10.ef-core\u002F19.advanced-queries-part1",{"title":583,"path":584,"stem":585},"Продвинуті Запити — Query Tags, Bulk та Interceptors (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fadvanced-queries-part2","01.csharp\u002F10.ef-core\u002F19.advanced-queries-part2",{"title":587,"path":588,"stem":589},"Change Tracker — Відстеження Змін (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fchange-tracking-part1","01.csharp\u002F10.ef-core\u002F20.change-tracking-part1",{"title":591,"path":592,"stem":593},"Change Tracker — Графи Об'єктів та Disconnected (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fchange-tracking-part2","01.csharp\u002F10.ef-core\u002F20.change-tracking-part2",{"title":595,"path":596,"stem":597},"Збереження Даних та Транзакції (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fsaving-data-part1","01.csharp\u002F10.ef-core\u002F21.saving-data-part1",{"title":599,"path":600,"stem":601},"Збереження Даних — Concurrency та Outbox (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fsaving-data-part2","01.csharp\u002F10.ef-core\u002F21.saving-data-part2",{"title":603,"path":604,"stem":605},"Конкурентність та Блокування (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fconcurrency-part1","01.csharp\u002F10.ef-core\u002F22.concurrency-part1",{"title":607,"path":608,"stem":609},"Конкурентність — Дедлоки та Queue Processing (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fconcurrency-part2","01.csharp\u002F10.ef-core\u002F22.concurrency-part2",{"title":611,"path":612,"stem":613},"Міграції в EF Core — Основи (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fmigrations-basics-part1","01.csharp\u002F10.ef-core\u002F23.migrations-basics-part1",{"title":615,"path":616,"stem":617},"Міграції в EF Core — Основи (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fmigrations-basics-part2","01.csharp\u002F10.ef-core\u002F23.migrations-basics-part2",{"title":619,"path":620,"stem":621},"Міграції — Просунуті Сценарії (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fmigrations-advanced-part1","01.csharp\u002F10.ef-core\u002F24.migrations-advanced-part1",{"title":623,"path":624,"stem":625},"Міграції — Просунуті Сценарії (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fmigrations-advanced-part2","01.csharp\u002F10.ef-core\u002F24.migrations-advanced-part2",{"title":627,"path":628,"stem":629},"Управління Схемою та Database-First (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fschema-management-part1","01.csharp\u002F10.ef-core\u002F25.schema-management-part1",{"title":631,"path":632,"stem":633},"Управління Схемою та Database-First (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fschema-management-part2","01.csharp\u002F10.ef-core\u002F25.schema-management-part2",{"title":635,"path":636,"stem":637},"Продуктивність EF Core — Основи (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fperformance-fundamentals-part1","01.csharp\u002F10.ef-core\u002F26.performance-fundamentals-part1",{"title":639,"path":640,"stem":641},"Interceptors в EF Core (Частина 1)","\u002Fcsharp\u002Fef-core\u002Finterceptors-part1","01.csharp\u002F10.ef-core\u002F29.interceptors-part1",{"title":643,"path":644,"stem":645},"Interceptors в EF Core — Connection, Transaction та Materialization (Частина 2)","\u002Fcsharp\u002Fef-core\u002Finterceptors-part2","01.csharp\u002F10.ef-core\u002F29.interceptors-part2",{"title":647,"path":648,"stem":649},"План вивчення Entity Framework Core — Повний курс","\u002Fcsharp\u002Fef-core\u002Fimplementation_plan","01.csharp\u002F10.ef-core\u002Fimplementation_plan",{"title":651,"icon":652,"path":653,"stem":654,"children":655,"page":59},"ASP.NET","i-devicon-dotnetcore","\u002Fcsharp\u002Faspnet","01.csharp\u002F11.aspnet",[656,730,791,869,927,941,967,1057,1111,1182,1212,1289,1346],{"title":657,"icon":658,"path":659,"stem":660,"children":661,"page":59},"Minimal API","i-lucide-network","\u002Fcsharp\u002Faspnet\u002Fminimal-api","01.csharp\u002F11.aspnet\u002F01.minimal-api",[662,666,670,674,678,682,686,690,694,698,702,706,710,714,718,722,726],{"title":663,"path":664,"stem":665},"Вступ до ASP.NET та еволюція фреймворку","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fintroduction","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F01.introduction",{"title":667,"path":668,"stem":669},"Перший додаток на ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Ffirst-application","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F02.first-application",{"title":671,"path":672,"stem":673},"WebApplication, Builder та Dependency Injection","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fwebapplication-builder","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F03.webapplication-builder",{"title":675,"path":676,"stem":677},"Конвеєр запитів та Middleware","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Frequest-pipeline-middleware","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F04.request-pipeline-middleware",{"title":679,"path":680,"stem":681},"Маршрутизація в ASP.NET Core: Основи","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Frouting-basics","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F05.routing-basics",{"title":683,"path":684,"stem":685},"Маршрутизація в ASP.NET Core: Розширені можливості","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Frouting-advanced","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F06.routing-advanced",{"title":687,"path":688,"stem":689},"Статичні файли в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fstatic-files","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F07.static-files",{"title":691,"path":692,"stem":693},"Статичні Активи: MapStaticAssets (ASP.NET Core 9.0)","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fstatic-assets","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F08.static-assets",{"title":695,"path":696,"stem":697},"Конфігурація в ASP.NET Core: Основи","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fconfiguration-fundamentals","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F09.configuration-fundamentals",{"title":699,"path":700,"stem":701},"Конфігурація: Паттерн Options","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fconfiguration-options","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F10.configuration-options",{"title":703,"path":704,"stem":705},"Логування в ASP.NET Core: Основи","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Flogging-basics","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F11.logging-basics",{"title":707,"path":708,"stem":709},"Логування: Serilog та Middleware","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Flogging-advanced","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F12.logging-advanced",{"title":711,"path":712,"stem":713},"Управління станом: HttpContext.Items та Cookies","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fstate-management","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F13.state-management",{"title":715,"path":716,"stem":717},"Стан сесії: Sessions","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fsession-state","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F14.session-state",{"title":719,"path":720,"stem":721},"Структура проєкту: від хаосу до архітектури","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fproject-structure","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F15.project-structure",{"title":723,"path":724,"stem":725},"Scalar у Minimal API: повний проєкт і Fluent OpenAPI","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fscalar-openapi-fluent","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F16.scalar-openapi-fluent",{"title":727,"path":728,"stem":729},"Swagger \u002F Swashbuckle у Minimal API: окремий класичний шлях","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fswagger-swashbuckle","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F17.swagger-swashbuckle",{"title":731,"icon":658,"path":732,"stem":733,"children":734,"page":59},"API","\u002Fcsharp\u002Faspnet\u002Fapi","01.csharp\u002F11.aspnet\u002F02.api",[735,739,743,747,751,755,759,763,767,771,775,779,783,787],{"title":736,"path":737,"stem":738},"Що таке API. Клієнт-серверна архітектура","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fwhat-is-api","01.csharp\u002F11.aspnet\u002F02.api\u002F01.what-is-api",{"title":740,"path":741,"stem":742},"Формати даних: JSON, XML, TOML та бінарні формати","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fdata-formats","01.csharp\u002F11.aspnet\u002F02.api\u002F02.data-formats",{"title":744,"path":745,"stem":746},"Парадигми API та концепція REST","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fapi-paradigms-rest","01.csharp\u002F11.aspnet\u002F02.api\u002F03.api-paradigms-rest",{"title":748,"path":749,"stem":750},"HTTP-методи, статус-коди та заголовки","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fhttp-methods-status-codes","01.csharp\u002F11.aspnet\u002F02.api\u002F04.http-methods-status-codes",{"title":752,"path":753,"stem":754},"Організація HTTP API за принципами REST","\u002Fcsharp\u002Faspnet\u002Fapi\u002Frest-organizing","01.csharp\u002F11.aspnet\u002F02.api\u002F05.rest-organizing",{"title":756,"path":757,"stem":758},"Номенклатура URL та CRUD-операції","\u002Fcsharp\u002Faspnet\u002Fapi\u002Furl-nomenclature-crud","01.csharp\u002F11.aspnet\u002F02.api\u002F06.url-nomenclature-crud",{"title":760,"path":761,"stem":762},"Правила дизайну: іменування та стандарти","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fapi-design-naming","01.csharp\u002F11.aspnet\u002F02.api\u002F07.api-design-naming",{"title":764,"path":765,"stem":766},"Валідація, ліміти та обробка помилок","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fapi-design-validation","01.csharp\u002F11.aspnet\u002F02.api\u002F08.api-design-validation",{"title":768,"path":769,"stem":770},"Обробка помилок у Minimal API","\u002Fcsharp\u002Faspnet\u002Fapi\u002Ferror-handling-http","01.csharp\u002F11.aspnet\u002F02.api\u002F09.error-handling-http",{"title":772,"path":773,"stem":774},"Ідемпотентність та синхронізація стану","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fidempotency-sync","01.csharp\u002F11.aspnet\u002F02.api\u002F10.idempotency-sync",{"title":776,"path":777,"stem":778},"Пагінація та організація списків","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fpagination-lists","01.csharp\u002F11.aspnet\u002F02.api\u002F11.pagination-lists",{"title":780,"path":781,"stem":782},"Безпека API, кешування та інтернаціоналізація","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fsecurity-auth","01.csharp\u002F11.aspnet\u002F02.api\u002F12.security-auth",{"title":784,"path":785,"stem":786},"Процес проєктування API та документування","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fapi-design-process","01.csharp\u002F11.aspnet\u002F02.api\u002F13.api-design-process",{"title":788,"path":789,"stem":790},"OpenAPI: контракт, специфікація та документація API","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fopenapi","01.csharp\u002F11.aspnet\u002F02.api\u002F14.openapi",{"title":792,"icon":793,"path":794,"stem":795,"children":796,"page":59},"Auth","i-lucide-shield-check","\u002Fcsharp\u002Faspnet\u002Fauth","01.csharp\u002F11.aspnet\u002F03.auth",[797,801,805,809,813,817,821,825,829,833,837,841,845,849,853,857,861,865],{"title":798,"path":799,"stem":800},"Основи аутентифікації та авторизації","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fauth-fundamentals","01.csharp\u002F11.aspnet\u002F03.auth\u002F01.auth-fundamentals",{"title":802,"path":803,"stem":804},"JWT-аутентифікація","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fjwt-authentication","01.csharp\u002F11.aspnet\u002F03.auth\u002F02.jwt-authentication",{"title":806,"path":807,"stem":808},"Авторизація: ролі, політики та resource-based доступ","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fauthorization-policies","01.csharp\u002F11.aspnet\u002F03.auth\u002F03.authorization-policies",{"title":810,"path":811,"stem":812},"Cookie-аутентифікація та ASP.NET Core Identity","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fcookie-auth-identity","01.csharp\u002F11.aspnet\u002F03.auth\u002F04.cookie-auth-identity",{"title":814,"path":815,"stem":816},"JWT + Refresh Tokens (HttpOnly Cookie)","\u002Fcsharp\u002Faspnet\u002Fauth\u002F04b.identity-auth-jwt","01.csharp\u002F11.aspnet\u002F03.auth\u002F04b.identity-auth-jwt",{"title":818,"path":819,"stem":820},"Identity: Підтвердження Email та Скидання Пароля","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fidentity-email-confirmation","01.csharp\u002F11.aspnet\u002F03.auth\u002F05.identity-email-confirmation",{"title":822,"path":823,"stem":824},"Identity: Двофакторна Аутентифікація (2FA)","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fidentity-two-factor","01.csharp\u002F11.aspnet\u002F03.auth\u002F06.identity-two-factor",{"title":826,"path":827,"stem":828},"Identity: Внутрішня Архітектура та Кастомізація","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fidentity-internals","01.csharp\u002F11.aspnet\u002F03.auth\u002F07.identity-internals",{"title":830,"path":831,"stem":832},"OAuth 2.0 та зовнішні провайдери","\u002Fcsharp\u002Faspnet\u002Fauth\u002Foauth-external-providers","01.csharp\u002F11.aspnet\u002F03.auth\u002F08.oauth-external-providers",{"title":834,"path":835,"stem":836},"Безпека на практиці: CORS, HTTPS та захист від атак","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fsecurity-hardening","01.csharp\u002F11.aspnet\u002F03.auth\u002F09.security-hardening",{"title":838,"path":839,"stem":840},"Теорія OAuth 2.0: Поняття, Аналогії та Флоу","\u002Fcsharp\u002Faspnet\u002Fauth\u002Foauth-theory","01.csharp\u002F11.aspnet\u002F03.auth\u002F10.oauth-theory",{"title":842,"path":843,"stem":844},"OIDC, OAuth 2.0 та Keycloak в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fauth\u002Foidc-keycloak","01.csharp\u002F11.aspnet\u002F03.auth\u002F10.oidc-keycloak",{"title":846,"path":847,"stem":848},"API Keys аутентифікація в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fapi-keys","01.csharp\u002F11.aspnet\u002F03.auth\u002F11.api-keys",{"title":850,"path":851,"stem":852},"Rate Limiting та Throttling в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fauth\u002Frate-limiting","01.csharp\u002F11.aspnet\u002F03.auth\u002F12.rate-limiting",{"title":854,"path":855,"stem":856},"Refresh Token Rotation в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fauth\u002Frefresh-token-rotation","01.csharp\u002F11.aspnet\u002F03.auth\u002F13.refresh-token-rotation",{"title":858,"path":859,"stem":860},"Certificate Authentication та mTLS в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fcertificate-auth","01.csharp\u002F11.aspnet\u002F03.auth\u002F14.certificate-auth",{"title":862,"path":863,"stem":864},"RBAC, ABAC та ReBAC в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fauth\u002Frbac-abac-rebac","01.csharp\u002F11.aspnet\u002F03.auth\u002F15.rbac-abac-rebac",{"title":866,"path":867,"stem":868},"Multi-tenancy та ізоляція даних в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fmulti-tenancy","01.csharp\u002F11.aspnet\u002F03.auth\u002F16.multi-tenancy",{"title":870,"icon":871,"path":872,"stem":873,"children":874,"page":59},"Нотифікації","i-lucide-bell","\u002Fcsharp\u002Faspnet\u002Fnotifications","01.csharp\u002F11.aspnet\u002F04.notifications",[875,879,883,887,891,895,899,903,907,911,915,919,923],{"title":876,"path":877,"stem":878},"In-App нотифікації через базу даних","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fin-app-database-notifications","01.csharp\u002F11.aspnet\u002F04.notifications\u002F01.in-app-database-notifications",{"title":880,"path":881,"stem":882},"Polling: Регулярний запит оновлень","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fpolling","01.csharp\u002F11.aspnet\u002F04.notifications\u002F02.polling",{"title":884,"path":885,"stem":886},"Server-Sent Events: Однострімовий push від сервера","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fserver-sent-events","01.csharp\u002F11.aspnet\u002F04.notifications\u002F03.server-sent-events",{"title":888,"path":889,"stem":890},"WebSockets: Двостороннє з'єднання в реальному часі","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fwebsockets","01.csharp\u002F11.aspnet\u002F04.notifications\u002F04.websockets",{"title":892,"path":893,"stem":894},"SignalR: Абстракція над транспортами реального часу","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fsignalr","01.csharp\u002F11.aspnet\u002F04.notifications\u002F05.signalr",{"title":896,"path":897,"stem":898},"Background Services: Фонові задачі в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fbackground-services","01.csharp\u002F11.aspnet\u002F04.notifications\u002F06.background-services",{"title":900,"path":901,"stem":902},"Web Push нотифікації","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fweb-push","01.csharp\u002F11.aspnet\u002F04.notifications\u002F07.web-push",{"title":904,"path":905,"stem":906},"Email нотифікації","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Femail-notifications","01.csharp\u002F11.aspnet\u002F04.notifications\u002F08.email-notifications",{"title":908,"path":909,"stem":910},"Порівняння підходів: Як вибрати правильну технологію нотифікацій","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fchoosing-the-right-approach","01.csharp\u002F11.aspnet\u002F04.notifications\u002F09.choosing-the-right-approach",{"title":912,"path":913,"stem":914},"Hangfire: Надійне планування фонових задач","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fhangfire","01.csharp\u002F11.aspnet\u002F04.notifications\u002F10.hangfire",{"title":916,"path":917,"stem":918},"Практика: Конвертація зображень у WebP через Hangfire","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fhangfire-image-webp","01.csharp\u002F11.aspnet\u002F04.notifications\u002F11.hangfire-image-webp",{"title":920,"path":921,"stem":922},"Практика: Підготовка відео до HLS-стрімінгу через Hangfire","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fhangfire-video-hls","01.csharp\u002F11.aspnet\u002F04.notifications\u002F12.hangfire-video-hls",{"title":924,"path":925,"stem":926},"Telegram-нотифікації: від одного повідомлення до масових розсилок і мульти-канального підходу","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Ftelegram-notifications","01.csharp\u002F11.aspnet\u002F04.notifications\u002F13.telegram-notifications",{"title":928,"icon":929,"path":930,"stem":931,"children":932,"page":59},"Інтернаціоналізація","i-lucide-languages","\u002Fcsharp\u002Faspnet\u002Fi18n","01.csharp\u002F11.aspnet\u002F05.i18n",[933,937],{"title":934,"path":935,"stem":936},"Інтернаціоналізація (i18n) у Minimal API: від A до Я","\u002Fcsharp\u002Faspnet\u002Fi18n\u002Finternationalization","01.csharp\u002F11.aspnet\u002F05.i18n\u002F01.internationalization",{"title":938,"path":939,"stem":940},"Humanizer: людиномовні рядки у .NET","\u002Fcsharp\u002Faspnet\u002Fi18n\u002Fhumanizer","01.csharp\u002F11.aspnet\u002F05.i18n\u002F02.humanizer",{"title":942,"icon":943,"path":944,"stem":945,"children":946,"page":59},"Кешування","i-lucide-layers","\u002Fcsharp\u002Faspnet\u002Fcaching","01.csharp\u002F11.aspnet\u002F06.caching",[947,951,955,959,963],{"title":948,"path":949,"stem":950},"Огляд кешування: чотири рівні і коли що обирати","\u002Fcsharp\u002Faspnet\u002Fcaching\u002Fcaching","01.csharp\u002F11.aspnet\u002F06.caching\u002F01.caching",{"title":952,"path":953,"stem":954},"IMemoryCache: кеш в оперативній пам'яті","\u002Fcsharp\u002Faspnet\u002Fcaching\u002Fmemory-cache","01.csharp\u002F11.aspnet\u002F06.caching\u002F02.memory-cache",{"title":956,"path":957,"stem":958},"IDistributedCache і Redis: розподілений кеш","\u002Fcsharp\u002Faspnet\u002Fcaching\u002Fdistributed-cache","01.csharp\u002F11.aspnet\u002F06.caching\u002F03.distributed-cache",{"title":960,"path":961,"stem":962},"Response Cache: HTTP-кешування через Cache-Control","\u002Fcsharp\u002Faspnet\u002Fcaching\u002Fresponse-cache","01.csharp\u002F11.aspnet\u002F06.caching\u002F04.response-cache",{"title":964,"path":965,"stem":966},"Output Cache: серверний кеш HTTP-відповідей (.NET 7+)","\u002Fcsharp\u002Faspnet\u002Fcaching\u002Foutput-cache","01.csharp\u002F11.aspnet\u002F06.caching\u002F05.output-cache",{"title":968,"icon":969,"path":970,"stem":971,"children":972,"page":59},"Тестування","i-lucide-test-tube","\u002Fcsharp\u002Faspnet\u002Ftesting","01.csharp\u002F11.aspnet\u002F07.testing",[973,977,981,985,989,993,997,1001,1005,1009,1013,1017,1021,1025,1029,1033,1037,1041,1045,1049,1053],{"title":974,"path":975,"stem":976},"Що таке тестування? Від інтуїції до науки","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fwhat-is-testing","01.csharp\u002F11.aspnet\u002F07.testing\u002F01.what-is-testing",{"title":978,"path":979,"stem":980},"Піраміда тестування — Стратегія, а не Догма","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Ftesting-pyramid","01.csharp\u002F11.aspnet\u002F07.testing\u002F02.testing-pyramid",{"title":982,"path":983,"stem":984},"Дві Школи Тестування — Лондон проти Детройту","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Ftesting-schools","01.csharp\u002F11.aspnet\u002F07.testing\u002F03.testing-schools",{"title":986,"path":987,"stem":988},"TDD та BDD — Тести як Дизайн-інструмент","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Ftdd-and-bdd","01.csharp\u002F11.aspnet\u002F07.testing\u002F04.tdd-and-bdd",{"title":990,"path":991,"stem":992},"Що саме тестувати — Техніки аналізу та Циклomatична складність","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fwhat-to-test","01.csharp\u002F11.aspnet\u002F07.testing\u002F05.what-to-test",{"title":994,"path":995,"stem":996},"Тестові Фреймворки — Навіщо вони і що всередині","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Ftest-frameworks","01.csharp\u002F11.aspnet\u002F07.testing\u002F06.test-frameworks",{"title":998,"path":999,"stem":1000},"xUnit — Факти, Теорії та Lifecycle тестів","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fxunit-basics","01.csharp\u002F11.aspnet\u002F07.testing\u002F07.xunit-basics",{"title":1002,"path":1003,"stem":1004},"xUnit Advanced — Fixtures, Кастомізація та Розширення","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fxunit-advanced","01.csharp\u002F11.aspnet\u002F07.testing\u002F08.xunit-advanced",{"title":1006,"path":1007,"stem":1008},"Moq — Глибоке занурення в мокування","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fmocking-with-moq","01.csharp\u002F11.aspnet\u002F07.testing\u002F09.mocking-with-moq",{"title":1010,"path":1011,"stem":1012},"Тестування Баз Даних — EF Core, SQLite та Testcontainers","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fdatabase-testing","01.csharp\u002F11.aspnet\u002F07.testing\u002F10.database-testing",{"title":1014,"path":1015,"stem":1016},"Integration Testing — Частина 1 [Теорія та WebApplicationFactory]","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fintegration-testing","01.csharp\u002F11.aspnet\u002F07.testing\u002F11.integration-testing",{"title":1018,"path":1019,"stem":1020},"Інтеграційне тестування — Практика","\u002Fcsharp\u002Faspnet\u002Ftesting\u002F11a.integration-testing-practice","01.csharp\u002F11.aspnet\u002F07.testing\u002F11a.integration-testing-practice",{"title":1022,"path":1023,"stem":1024},"Integration Testing — Частина 2 [Просунуті Сценарії та Testcontainers]","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fintegration-testing-advanced","01.csharp\u002F11.aspnet\u002F07.testing\u002F12.integration-testing-advanced",{"title":1026,"path":1027,"stem":1028},"Професійний Postman: Колекції, Змінні та GitHub Інтеграція","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fpostman-professional","01.csharp\u002F11.aspnet\u002F07.testing\u002F13.postman-professional",{"title":1030,"path":1031,"stem":1032},"HttpClient у Тестах Частина 1: Архітектура та MockHttpMessageHandler","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fhttpclient-testing","01.csharp\u002F11.aspnet\u002F07.testing\u002F14.httpclient-testing",{"title":1034,"path":1035,"stem":1036},"HttpClient у Тестах Частина 2: WireMock.Net та Resilience","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fwiremock-net","01.csharp\u002F11.aspnet\u002F07.testing\u002F15.wiremock-net",{"title":1038,"path":1039,"stem":1040},"Патерни та Анти-патерни Тестування: Test Smells","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Ftesting-patterns","01.csharp\u002F11.aspnet\u002F07.testing\u002F16.testing-patterns",{"title":1042,"path":1043,"stem":1044},"Просунуті інструменти: Time, Snapshots та Властивості","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fadvanced-testing-tools","01.csharp\u002F11.aspnet\u002F07.testing\u002F17.advanced-testing-tools",{"title":1046,"path":1047,"stem":1048},"Тестування Архітектури з NetArchTest","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Farchitecture-testing","01.csharp\u002F11.aspnet\u002F07.testing\u002F18.architecture-testing",{"title":1050,"path":1051,"stem":1052},"Тестування Продуктивності: BenchmarkDotNet, NBomber та k6","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fperformance-testing","01.csharp\u002F11.aspnet\u002F07.testing\u002F19.performance-testing",{"title":1054,"path":1055,"stem":1056},"Залишок плану для курсу \"Тестування ASP.NET Minimal API\"","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fremaining_plan","01.csharp\u002F11.aspnet\u002F07.testing\u002Fremaining_plan",{"title":1058,"icon":1059,"path":1060,"stem":1061,"children":1062,"page":59},"Платежі","i-lucide-credit-card","\u002Fcsharp\u002Faspnet\u002Fpayments","01.csharp\u002F11.aspnet\u002F08.payments",[1063,1067,1071,1075,1079,1083,1087,1091,1095,1099,1103,1107],{"title":1064,"path":1065,"stem":1066},"Основи платіжної інфраструктури","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fpayment-fundamentals","01.csharp\u002F11.aspnet\u002F08.payments\u002F01.payment-fundamentals",{"title":1068,"path":1069,"stem":1070},"Методи оплати в Україні","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fpayment-methods-ukraine","01.csharp\u002F11.aspnet\u002F08.payments\u002F02.payment-methods-ukraine",{"title":1072,"path":1073,"stem":1074},"PCI DSS та безпека платежів","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fpci-dss-security","01.csharp\u002F11.aspnet\u002F08.payments\u002F03.pci-dss-security",{"title":1076,"path":1077,"stem":1078},"Архітектура платіжної підсистеми","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fpayment-architecture","01.csharp\u002F11.aspnet\u002F08.payments\u002F04.payment-architecture",{"title":1080,"path":1081,"stem":1082},"Інтеграція LiqPay (ПриватБанк)","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fliqpay-integration","01.csharp\u002F11.aspnet\u002F08.payments\u002F05.liqpay-integration",{"title":1084,"path":1085,"stem":1086},"Інтеграція Monobank Acquiring API","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fmonobank-acquiring","01.csharp\u002F11.aspnet\u002F08.payments\u002F06.monobank-acquiring",{"title":1088,"path":1089,"stem":1090},"Інтеграція Stripe","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fstripe-integration","01.csharp\u002F11.aspnet\u002F08.payments\u002F07.stripe-integration",{"title":1092,"path":1093,"stem":1094},"Webhooks — глибоке занурення","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fwebhooks-deep-dive","01.csharp\u002F11.aspnet\u002F08.payments\u002F08.webhooks-deep-dive",{"title":1096,"path":1097,"stem":1098},"Підписки та рекурентні платежі","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fsubscriptions-recurring","01.csharp\u002F11.aspnet\u002F08.payments\u002F09.subscriptions-recurring",{"title":1100,"path":1101,"stem":1102},"Повернення коштів та диспути","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Frefunds-disputes","01.csharp\u002F11.aspnet\u002F08.payments\u002F10.refunds-disputes",{"title":1104,"path":1105,"stem":1106},"Тестування платіжних інтеграцій","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Ftesting-payments","01.csharp\u002F11.aspnet\u002F08.payments\u002F11.testing-payments",{"title":1108,"path":1109,"stem":1110},"Чекліст виходу в Production","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fproduction-checklist","01.csharp\u002F11.aspnet\u002F08.payments\u002F12.production-checklist",{"title":1112,"icon":1113,"items":1114,"path":1127,"stem":1128,"children":1129,"page":59},"Популярні бібліотеки","lucide:box",[1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126],"01.fluent-validation","02.mapster","03.erroror-result-pattern","04.serilog","05.mediatr","06.polly","07.health-checks","08.feature-management","09.fluent-email","10.quest-pdf","11.bogus","12.humanizer-guard","\u002Fcsharp\u002Faspnet\u002Flibraries","01.csharp\u002F11.aspnet\u002F09.libraries",[1130,1134,1138,1142,1146,1150,1154,1158,1162,1166,1170,1174,1178],{"title":1131,"path":1132,"stem":1133},"Валідація з FluentValidation в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Ffluent-validation","01.csharp\u002F11.aspnet\u002F09.libraries\u002F01.fluent-validation",{"title":1135,"path":1136,"stem":1137},"Маппінг об","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fmapster","01.csharp\u002F11.aspnet\u002F09.libraries\u002F02.mapster",{"title":1139,"path":1140,"stem":1141},"Обробка помилок з ErrorOr та Result Pattern в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Ferroror-result-pattern","01.csharp\u002F11.aspnet\u002F09.libraries\u002F03.erroror-result-pattern",{"title":1143,"path":1144,"stem":1145},"Структуроване логування з Serilog в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fserilog","01.csharp\u002F11.aspnet\u002F09.libraries\u002F04.serilog",{"title":1147,"path":1148,"stem":1149},"CQRS та Mediator з MediatR в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fmediatr","01.csharp\u002F11.aspnet\u002F09.libraries\u002F05.mediatr",{"title":1151,"path":1152,"stem":1153},"Відмовостійкість з Polly в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fpolly","01.csharp\u002F11.aspnet\u002F09.libraries\u002F06.polly",{"title":1155,"path":1156,"stem":1157},"Health Checks в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fhealth-checks","01.csharp\u002F11.aspnet\u002F09.libraries\u002F07.health-checks",{"title":1159,"path":1160,"stem":1161},"Feature Management та Feature Flags в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Ffeature-management","01.csharp\u002F11.aspnet\u002F09.libraries\u002F08.feature-management",{"title":1163,"path":1164,"stem":1165},"Відправка Email з FluentEmail в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Ffluent-email","01.csharp\u002F11.aspnet\u002F09.libraries\u002F09.fluent-email",{"title":1167,"path":1168,"stem":1169},"Генерація PDF з QuestPDF в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fquest-pdf","01.csharp\u002F11.aspnet\u002F09.libraries\u002F10.quest-pdf",{"title":1171,"path":1172,"stem":1173},"Генерація тестових даних з Bogus в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fbogus","01.csharp\u002F11.aspnet\u002F09.libraries\u002F11.bogus",{"title":1175,"path":1176,"stem":1177},"Humanizer та Guard Clauses в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fhumanizer-guard","01.csharp\u002F11.aspnet\u002F09.libraries\u002F12.humanizer-guard",{"title":1179,"path":1180,"stem":1181},"План модуля 10.libraries — Популярні бібліотеки ASP.NET","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fplan","01.csharp\u002F11.aspnet\u002F09.libraries\u002Fplan",{"title":1183,"icon":1184,"path":1185,"stem":1186,"children":1187,"page":59},"Razor Pages","i-lucide-layout-template","\u002Fcsharp\u002Faspnet\u002Frazor-pages","01.csharp\u002F11.aspnet\u002F10.razor-pages",[1188,1192,1196,1200,1204,1208],{"title":1189,"path":1190,"stem":1191},"Від Minimal API до Razor Pages: концептуальний перехід","\u002Fcsharp\u002Faspnet\u002Frazor-pages\u002Ffrom-minimal-api","01.csharp\u002F11.aspnet\u002F10.razor-pages\u002F01.from-minimal-api",{"title":1193,"path":1194,"stem":1195},"PageModel: логіка сторінки Razor Pages","\u002Fcsharp\u002Faspnet\u002Frazor-pages\u002Fpage-model","01.csharp\u002F11.aspnet\u002F10.razor-pages\u002F02.page-model",{"title":1197,"path":1198,"stem":1199},"Razor синтаксис: шаблонізатор у .cshtml","\u002Fcsharp\u002Faspnet\u002Frazor-pages\u002Frazor-syntax","01.csharp\u002F11.aspnet\u002F10.razor-pages\u002F03.razor-syntax",{"title":1201,"path":1202,"stem":1203},"Tag Helpers: типізований HTML","\u002Fcsharp\u002Faspnet\u002Frazor-pages\u002Ftag-helpers","01.csharp\u002F11.aspnet\u002F10.razor-pages\u002F04.tag-helpers",{"title":1205,"path":1206,"stem":1207},"Форми і валідація: повний цикл обробки даних","\u002Fcsharp\u002Faspnet\u002Frazor-pages\u002Fforms-validation","01.csharp\u002F11.aspnet\u002F10.razor-pages\u002F05.forms-validation",{"title":1209,"path":1210,"stem":1211},"Практичний проєкт: TaskManager на Razor Pages","\u002Fcsharp\u002Faspnet\u002Frazor-pages\u002Fproject-task-manager","01.csharp\u002F11.aspnet\u002F10.razor-pages\u002F06.project-task-manager",{"title":1213,"path":1214,"stem":1215,"children":1216,"page":59},"ASP.NET Core MVC","\u002Fcsharp\u002Faspnet\u002Fmvc","01.csharp\u002F11.aspnet\u002F11.mvc",[1217,1221,1225,1229,1233,1237,1241,1245,1249,1253,1257,1261,1265,1269,1273,1277,1281,1285],{"title":1218,"path":1219,"stem":1220},"Патерн MVC: архітектура, що змінила веб","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fmvc-pattern","01.csharp\u002F11.aspnet\u002F11.mvc\u002F01.mvc-pattern",{"title":1222,"path":1223,"stem":1224},"Від Razor Pages до MVC: концептуальний перехід","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Ffrom-razor-pages","01.csharp\u002F11.aspnet\u002F11.mvc\u002F02.from-razor-pages",{"title":1226,"path":1227,"stem":1228},"Controllers та Actions: серце MVC","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fcontrollers-actions","01.csharp\u002F11.aspnet\u002F11.mvc\u002F03.controllers-actions",{"title":1230,"path":1231,"stem":1232},"Маршрутизація в MVC: Convention vs Attribute Routing","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Frouting-mvc","01.csharp\u002F11.aspnet\u002F11.mvc\u002F04.routing-mvc",{"title":1234,"path":1235,"stem":1236},"Model Binding: від HTTP до C#","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fmodel-binding","01.csharp\u002F11.aspnet\u002F11.mvc\u002F05.model-binding",{"title":1238,"path":1239,"stem":1240},"Views, ViewData, ViewBag, TempData і ViewModel","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fviews-viewdata-tempdata","01.csharp\u002F11.aspnet\u002F11.mvc\u002F06.views-viewdata-tempdata",{"title":1242,"path":1243,"stem":1244},"Filters: аспектно-орієнтоване програмування в MVC","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Ffilters","01.csharp\u002F11.aspnet\u002F11.mvc\u002F07.filters",{"title":1246,"path":1247,"stem":1248},"Areas: структурування великих застосунків","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fareas","01.csharp\u002F11.aspnet\u002F11.mvc\u002F08.areas",{"title":1250,"path":1251,"stem":1252},"View Components: повторювані незалежні блоки UI","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fview-components","01.csharp\u002F11.aspnet\u002F11.mvc\u002F09.view-components",{"title":1254,"path":1255,"stem":1256},"Display та Editor Templates","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fdisplay-editor-templates","01.csharp\u002F11.aspnet\u002F11.mvc\u002F10.display-editor-templates",{"title":1258,"path":1259,"stem":1260},"Валідація: IValidatableObject та FluentValidation","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fvalidation-advanced","01.csharp\u002F11.aspnet\u002F11.mvc\u002F11.validation-advanced",{"title":1262,"path":1263,"stem":1264},"HTMX: інтерактивність через HTML-атрибути","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fhtmx","01.csharp\u002F11.aspnet\u002F11.mvc\u002F12.htmx",{"title":1266,"path":1267,"stem":1268},"HTMX у ASP.NET Core MVC: серверна інтеграція","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fajax-htmx-mvc","01.csharp\u002F11.aspnet\u002F11.mvc\u002F13.ajax-htmx-mvc",{"title":1270,"path":1271,"stem":1272},"Практичний проєкт: Каталог товарів з HTMX","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fhtmx-project","01.csharp\u002F11.aspnet\u002F11.mvc\u002F14.htmx-project",{"title":1274,"path":1275,"stem":1276},"Завантаження та обробка файлів","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Ffile-upload","01.csharp\u002F11.aspnet\u002F11.mvc\u002F15.file-upload",{"title":1278,"path":1279,"stem":1280},"Глобалізація та Локалізація MVC","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fglobalization-localization","01.csharp\u002F11.aspnet\u002F11.mvc\u002F16.globalization-localization",{"title":1282,"path":1283,"stem":1284},"Підсумковий проєкт: Блог-платформа","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fmvc-project","01.csharp\u002F11.aspnet\u002F11.mvc\u002F17.mvc-project",{"title":1286,"path":1287,"stem":1288},"План курсу: ASP.NET Core MVC","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fplan","01.csharp\u002F11.aspnet\u002F11.mvc\u002Fplan",{"title":1290,"path":1291,"stem":1292,"children":1293,"page":59},"Web Api","\u002Fcsharp\u002Faspnet\u002Fweb-api","01.csharp\u002F11.aspnet\u002F12.web-api",[1294,1298,1302,1306,1310,1314,1318,1322,1326,1330,1334,1338,1342],{"title":1295,"path":1296,"stem":1297},"Від Minimal API до Controller-based API","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Ffrom-minimal-api-to-controllers","01.csharp\u002F11.aspnet\u002F12.web-api\u002F01.from-minimal-api-to-controllers",{"title":1299,"path":1300,"stem":1301},"ControllerBase, ActionResult\u003CT> та Response Types","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fcontroller-base-actionresult","01.csharp\u002F11.aspnet\u002F12.web-api\u002F02.controller-base-actionresult",{"title":1303,"path":1304,"stem":1305},"Content Negotiation - JSON, XML та власні форматери","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fcontent-negotiation","01.csharp\u002F11.aspnet\u002F12.web-api\u002F03.content-negotiation",{"title":1307,"path":1308,"stem":1309},"Версіонування API","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fapi-versioning","01.csharp\u002F11.aspnet\u002F12.web-api\u002F04.api-versioning",{"title":1311,"path":1312,"stem":1313},"ProblemDetails та структурована обробка помилок","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fproblemdetails-error-handling","01.csharp\u002F11.aspnet\u002F12.web-api\u002F05.problemdetails-error-handling",{"title":1315,"path":1316,"stem":1317},"Фільтри у Web API контексті","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Ffilters-for-api","01.csharp\u002F11.aspnet\u002F12.web-api\u002F06.filters-for-api",{"title":1319,"path":1320,"stem":1321},"Пагінація, фільтрація та сортування","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fpagination-filtering-sorting","01.csharp\u002F11.aspnet\u002F12.web-api\u002F07.pagination-filtering-sorting",{"title":1323,"path":1324,"stem":1325},"HATEOAS та Resource Expansion","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fhateoas-resource-expansion","01.csharp\u002F11.aspnet\u002F12.web-api\u002F08.hateoas-resource-expansion",{"title":1327,"path":1328,"stem":1329},"Гібридна архітектура - Minimal API + Controllers","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fminimal-api-vs-controllers-hybrid","01.csharp\u002F11.aspnet\u002F12.web-api\u002F09.minimal-api-vs-controllers-hybrid",{"title":1331,"path":1332,"stem":1333},"Документація API - Swashbuckle, NSwag та генерація клієнтів","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fapi-documentation-generation","01.csharp\u002F11.aspnet\u002F12.web-api\u002F10.api-documentation-generation",{"title":1335,"path":1336,"stem":1337},"Health Checks та моніторинг API","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fhealth-checks-monitoring","01.csharp\u002F11.aspnet\u002F12.web-api\u002F11.health-checks-monitoring",{"title":1339,"path":1340,"stem":1341},"Підсумковий проєкт - Production-Ready REST API","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fweb-api-project","01.csharp\u002F11.aspnet\u002F12.web-api\u002F12.web-api-project",{"title":1343,"path":1344,"stem":1345},"План курсу: ASP.NET Core Web API (Controllers)","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fplan","01.csharp\u002F11.aspnet\u002F12.web-api\u002Fplan",{"title":1347,"icon":1348,"path":1349,"stem":1350,"children":1351,"page":59},"Моніторинг","i-lucide-activity","\u002Fcsharp\u002Faspnet\u002Fmonitoring","01.csharp\u002F11.aspnet\u002F13.monitoring",[1352,1356,1360],{"title":1353,"path":1354,"stem":1355},"Спостережуваність: від console.log до production-систем","\u002Fcsharp\u002Faspnet\u002Fmonitoring\u002Fobservability-intro","01.csharp\u002F11.aspnet\u002F13.monitoring\u002F01.observability-intro",{"title":1357,"path":1358,"stem":1359},"Health Checks: перший рівень observability","\u002Fcsharp\u002Faspnet\u002Fmonitoring\u002Fhealth-checks","01.csharp\u002F11.aspnet\u002F13.monitoring\u002F02.health-checks",{"title":1361,"path":1362,"stem":1363},"Вбудовані метрики .NET 10 та System.Diagnostics.Metrics","\u002Fcsharp\u002Faspnet\u002Fmonitoring\u002Fdotnet-metrics","01.csharp\u002F11.aspnet\u002F13.monitoring\u002F03.dotnet-metrics",{"title":1365,"icon":1366,"path":1367,"stem":1368,"children":1369,"page":59},"Desktop UI","i-lucide-app-window","\u002Fcsharp\u002Fdesktop-ui","01.csharp\u002F12.desktop-ui",[1370,1374,1378,1382,1386,1390,1394,1398,1402,1406,1410,1414,1418,1422,1426,1430,1434,1438,1442,1446,1450,1454,1458,1462,1466,1470,1474,1478,1482,1486,1490,1494,1498,1502,1506,1510,1514,1518,1522,1526,1530,1534,1538,1542,1546,1550,1554,1558,1562,1566,1570,1574,1578,1582,1586,1590,1594,1598,1602,1606,1610,1614,1618,1622,1626,1630,1634,1638,1642,1646,1650],{"title":1371,"path":1372,"stem":1373},"Що таке десктопна розробка?","\u002Fcsharp\u002Fdesktop-ui\u002Fwhat-is-desktop-dev","01.csharp\u002F12.desktop-ui\u002F01.what-is-desktop-dev",{"title":1375,"path":1376,"stem":1377},"Архітектура WPF — як влаштований графічний інтерфейс","\u002Fcsharp\u002Fdesktop-ui\u002Fwpf-architecture","01.csharp\u002F12.desktop-ui\u002F02.wpf-architecture",{"title":1379,"path":1380,"stem":1381},"Перший WPF-проєкт — від нуля до вікна","\u002Fcsharp\u002Fdesktop-ui\u002Ffirst-wpf-app","01.csharp\u002F12.desktop-ui\u002F03.first-wpf-app",{"title":1383,"path":1384,"stem":1385},"Перший Avalonia-проєкт: WPF для всіх платформ","\u002Fcsharp\u002Fdesktop-ui\u002F03a.first-avalonia-app","01.csharp\u002F12.desktop-ui\u002F03a.first-avalonia-app",{"title":1387,"path":1388,"stem":1389},"XAML: декларативний інтерфейс","\u002Fcsharp\u002Fdesktop-ui\u002Fxaml-basics","01.csharp\u002F12.desktop-ui\u002F04.xaml-basics",{"title":1391,"path":1392,"stem":1393},"Fluent UI у WPF — сучасний дизайн Windows 11","\u002Fcsharp\u002Fdesktop-ui\u002F04a.wpf-fluent-ui","01.csharp\u002F12.desktop-ui\u002F04a.wpf-fluent-ui",{"title":1395,"path":1396,"stem":1397},"WPF UI — сучасна бібліотека Fluent контролів","\u002Fcsharp\u002Fdesktop-ui\u002F04b.wpf-ui-library","01.csharp\u002F12.desktop-ui\u002F04b.wpf-ui-library",{"title":1399,"path":1400,"stem":1401},"HandyControl — велика бібліотека UI контролів для WPF","\u002Fcsharp\u002Fdesktop-ui\u002F04c.handycontrol-library","01.csharp\u002F12.desktop-ui\u002F04c.handycontrol-library",{"title":1403,"path":1404,"stem":1405},"Простори імен та ресурси XAML","\u002Fcsharp\u002Fdesktop-ui\u002Fxaml-namespaces-resources","01.csharp\u002F12.desktop-ui\u002F05.xaml-namespaces-resources",{"title":1407,"path":1408,"stem":1409},"XAML в Avalonia: ключові відмінності від WPF","\u002Fcsharp\u002Fdesktop-ui\u002F05a.avalonia-xaml-differences","01.csharp\u002F12.desktop-ui\u002F05a.avalonia-xaml-differences",{"title":1411,"path":1412,"stem":1413},"Розширення розмітки XAML (Markup Extensions)","\u002Fcsharp\u002Fdesktop-ui\u002Fxaml-markup-extensions","01.csharp\u002F12.desktop-ui\u002F06.xaml-markup-extensions",{"title":1415,"path":1416,"stem":1417},"Панелі Layout: StackPanel, WrapPanel, DockPanel","\u002Fcsharp\u002Fdesktop-ui\u002Flayout-panels-part1","01.csharp\u002F12.desktop-ui\u002F07.layout-panels-part1",{"title":1419,"path":1420,"stem":1421},"Grid, Canvas, UniformGrid","\u002Fcsharp\u002Fdesktop-ui\u002Flayout-panels-part2","01.csharp\u002F12.desktop-ui\u002F07.layout-panels-part2",{"title":1423,"path":1424,"stem":1425},"Просунуті техніки Layout","\u002Fcsharp\u002Fdesktop-ui\u002Flayout-advanced","01.csharp\u002F12.desktop-ui\u002F08.layout-advanced",{"title":1427,"path":1428,"stem":1429},"Адаптивний Layout та найкращі практики","\u002Fcsharp\u002Fdesktop-ui\u002Flayout-responsive","01.csharp\u002F12.desktop-ui\u002F09.layout-responsive",{"title":1431,"path":1432,"stem":1433},"Layout в Avalonia: відмінності та нові можливості","\u002Fcsharp\u002Fdesktop-ui\u002F09a.layout-avalonia","01.csharp\u002F12.desktop-ui\u002F09a.layout-avalonia",{"title":1435,"path":1436,"stem":1437},"Button, Image, ProgressBar та інші базові контроли","\u002Fcsharp\u002Fdesktop-ui\u002Fbasic-controls","01.csharp\u002F12.desktop-ui\u002F10.basic-controls",{"title":1439,"path":1440,"stem":1441},"Контроли в Avalonia: відмінності від WPF","\u002Fcsharp\u002Fdesktop-ui\u002F10a.controls-avalonia","01.csharp\u002F12.desktop-ui\u002F10a.controls-avalonia",{"title":1443,"path":1444,"stem":1445},"Текстові контроли — TextBlock, TextBox, RichTextBox","\u002Fcsharp\u002Fdesktop-ui\u002Ftext-controls","01.csharp\u002F12.desktop-ui\u002F11.text-controls",{"title":1447,"path":1448,"stem":1449},"Контроли вибору — CheckBox, RadioButton, ComboBox, ListBox, DatePicker","\u002Fcsharp\u002Fdesktop-ui\u002Fselection-controls","01.csharp\u002F12.desktop-ui\u002F12.selection-controls",{"title":1451,"path":1452,"stem":1453},"Content Model — GroupBox, Expander, TabControl, StatusBar","\u002Fcsharp\u002Fdesktop-ui\u002Fcontent-controls","01.csharp\u002F12.desktop-ui\u002F13.content-controls",{"title":1455,"path":1456,"stem":1457},"UI\u002FUX принципи десктопних застосунків","\u002Fcsharp\u002Fdesktop-ui\u002F13a.ui-ux-principles","01.csharp\u002F12.desktop-ui\u002F13a.ui-ux-principles",{"title":1459,"path":1460,"stem":1461},"Dependency Properties — Концепція та Value Resolution","\u002Fcsharp\u002Fdesktop-ui\u002Fdependency-properties-part1","01.csharp\u002F12.desktop-ui\u002F14.dependency-properties-part1",{"title":1463,"path":1464,"stem":1465},"Avalonia Property System — StyledProperty та DirectProperty","\u002Fcsharp\u002Fdesktop-ui\u002F14a.avalonia-property-system","01.csharp\u002F12.desktop-ui\u002F14a.avalonia-property-system",{"title":1467,"path":1468,"stem":1469},"Attached Properties — Властивості без меж","\u002Fcsharp\u002Fdesktop-ui\u002Fattached-properties","01.csharp\u002F12.desktop-ui\u002F15.attached-properties",{"title":1471,"path":1472,"stem":1473},"Routed Events — Маршрутизація подій у WPF","\u002Fcsharp\u002Fdesktop-ui\u002Frouted-events","01.csharp\u002F12.desktop-ui\u002F16.routed-events",{"title":1475,"path":1476,"stem":1477},"Data Binding — Від Code-Behind до Декларативності","\u002Fcsharp\u002Fdesktop-ui\u002Fdata-binding-basics-part1","01.csharp\u002F12.desktop-ui\u002F17.data-binding-basics-part1",{"title":1479,"path":1480,"stem":1481},"INotifyPropertyChanged — Живе оновлення UI","\u002Fcsharp\u002Fdesktop-ui\u002Fdata-binding-basics-part2","01.csharp\u002F12.desktop-ui\u002F17.data-binding-basics-part2",{"title":1483,"path":1484,"stem":1485},"Compiled Bindings в Avalonia — Безпека на етапі компіляції","\u002Fcsharp\u002Fdesktop-ui\u002F17a.avalonia-compiled-bindings","01.csharp\u002F12.desktop-ui\u002F17a.avalonia-compiled-bindings",{"title":1487,"path":1488,"stem":1489},"Просунутий Data Binding — ElementName, RelativeSource, MultiBinding","\u002Fcsharp\u002Fdesktop-ui\u002Fdata-binding-advanced","01.csharp\u002F12.desktop-ui\u002F18.data-binding-advanced",{"title":1491,"path":1492,"stem":1493},"Value Converters — Перетворення типів даних у Data Binding","\u002Fcsharp\u002Fdesktop-ui\u002Fvalue-converters","01.csharp\u002F12.desktop-ui\u002F19.value-converters",{"title":1495,"path":1496,"stem":1497},"Data Templates — Візуалізація об'єктів у WPF","\u002Fcsharp\u002Fdesktop-ui\u002Fdata-templates","01.csharp\u002F12.desktop-ui\u002F20.data-templates",{"title":1499,"path":1500,"stem":1501},"Collections Binding Part 1 — ObservableCollection та ItemsControl","\u002Fcsharp\u002Fdesktop-ui\u002Fcollections-binding-part1","01.csharp\u002F12.desktop-ui\u002F21.collections-binding-part1",{"title":1503,"path":1504,"stem":1505},"Collections Binding Part 2 — ICollectionView, Filtering, Sorting та Virtualization","\u002Fcsharp\u002Fdesktop-ui\u002Fcollections-binding-part2","01.csharp\u002F12.desktop-ui\u002F21.collections-binding-part2",{"title":1507,"path":1508,"stem":1509},"MVVM Pattern — Від Spaghetti Code до архітектури","\u002Fcsharp\u002Fdesktop-ui\u002Fmvvm-pattern","01.csharp\u002F12.desktop-ui\u002F22.mvvm-pattern",{"title":1511,"path":1512,"stem":1513},"ViewModel Implementation — Від BaseViewModel до валідації","\u002Fcsharp\u002Fdesktop-ui\u002Fviewmodel-implementation","01.csharp\u002F12.desktop-ui\u002F23.viewmodel-implementation",{"title":1515,"path":1516,"stem":1517},"Commands — Від event handlers до декларативних команд","\u002Fcsharp\u002Fdesktop-ui\u002Fcommands","01.csharp\u002F12.desktop-ui\u002F24.commands",{"title":1519,"path":1520,"stem":1521},"MVVM Toolkit — MVVM без boilerplate через Source Generators","\u002Fcsharp\u002Fdesktop-ui\u002Fmvvm-toolkit","01.csharp\u002F12.desktop-ui\u002F25.mvvm-toolkit",{"title":1523,"path":1524,"stem":1525},"Messenger Pattern — Комунікація між ViewModel без прямих посилань","\u002Fcsharp\u002Fdesktop-ui\u002Fmessenger-pattern","01.csharp\u002F12.desktop-ui\u002F26.messenger-pattern",{"title":1527,"path":1528,"stem":1529},"Стилі WPF — CSS для десктопу","\u002Fcsharp\u002Fdesktop-ui\u002Fstyles-basics","01.csharp\u002F12.desktop-ui\u002F27.styles-basics",{"title":1531,"path":1532,"stem":1533},"CSS-like стилі Avalonia","\u002Fcsharp\u002Fdesktop-ui\u002F27a.avalonia-css-styling","01.csharp\u002F12.desktop-ui\u002F27a.avalonia-css-styling",{"title":1535,"path":1536,"stem":1537},"Control Templates — Частина 1. Концепція та TemplateBinding","\u002Fcsharp\u002Fdesktop-ui\u002Fcontrol-templates-part1","01.csharp\u002F12.desktop-ui\u002F28.control-templates-part1",{"title":1539,"path":1540,"stem":1541},"Control Templates — Частина 2. Named Parts та ContentPresenter","\u002Fcsharp\u002Fdesktop-ui\u002Fcontrol-templates-part2","01.csharp\u002F12.desktop-ui\u002F28.control-templates-part2",{"title":1543,"path":1544,"stem":1545},"Control Themes в Avalonia — нова ера стилізації","\u002Fcsharp\u002Fdesktop-ui\u002F28a.avalonia-control-themes","01.csharp\u002F12.desktop-ui\u002F28a.avalonia-control-themes",{"title":1547,"path":1548,"stem":1549},"Triggers та Visual State Manager у WPF","\u002Fcsharp\u002Fdesktop-ui\u002Ftriggers-visual-states","01.csharp\u002F12.desktop-ui\u002F29.triggers-visual-states",{"title":1551,"path":1552,"stem":1553},"Pseudo-classes в Avalonia — замість WPF Triggers","\u002Fcsharp\u002Fdesktop-ui\u002F29a.avalonia-pseudo-classes","01.csharp\u002F12.desktop-ui\u002F29a.avalonia-pseudo-classes",{"title":1555,"path":1556,"stem":1557},"Теми та ресурсні словники у WPF","\u002Fcsharp\u002Fdesktop-ui\u002Fresources-themes","01.csharp\u002F12.desktop-ui\u002F30.resources-themes",{"title":1559,"path":1560,"stem":1561},"Avalonia Themes — Fluent Design та система тематизації","\u002Fcsharp\u002Fdesktop-ui\u002F30a.avalonia-themes-fluent","01.csharp\u002F12.desktop-ui\u002F30a.avalonia-themes-fluent",{"title":1563,"path":1564,"stem":1565},"Контроли колекцій — глибоке занурення","\u002Fcsharp\u002Fdesktop-ui\u002Fcollection-controls","01.csharp\u002F12.desktop-ui\u002F31.collection-controls",{"title":1567,"path":1568,"stem":1569},"DataGrid — колонки та базове відображення","\u002Fcsharp\u002Fdesktop-ui\u002Fdatagrid-part1","01.csharp\u002F12.desktop-ui\u002F32.datagrid-part1",{"title":1571,"path":1572,"stem":1573},"DataGrid — сортування, фільтрація, редагування","\u002Fcsharp\u002Fdesktop-ui\u002Fdatagrid-part2","01.csharp\u002F12.desktop-ui\u002F32.datagrid-part2",{"title":1575,"path":1576,"stem":1577},"TreeView та GridView","\u002Fcsharp\u002Fdesktop-ui\u002Ftreeview-listview","01.csharp\u002F12.desktop-ui\u002F33.treeview-listview",{"title":1579,"path":1580,"stem":1581},"Меню, Toolbar, ContextMenu, StatusBar","\u002Fcsharp\u002Fdesktop-ui\u002Fmenus-toolbars","01.csharp\u002F12.desktop-ui\u002F34.menus-toolbars",{"title":1583,"path":1584,"stem":1585},"Навігація та керування вікнами. Частина 1: вікна та сторінки","\u002Fcsharp\u002Fdesktop-ui\u002Fnavigation-windows-part1","01.csharp\u002F12.desktop-ui\u002F35.navigation-windows-part1",{"title":1587,"path":1588,"stem":1589},"Навігація та керування вікнами. Частина 2: MVVM-навігація","\u002Fcsharp\u002Fdesktop-ui\u002Fnavigation-windows-part2","01.csharp\u002F12.desktop-ui\u002F35.navigation-windows-part2",{"title":1591,"path":1592,"stem":1593},"Avalonia — Навігація та діалоги","\u002Fcsharp\u002Fdesktop-ui\u002F35a.avalonia-navigation-dialogs","01.csharp\u002F12.desktop-ui\u002F35a.avalonia-navigation-dialogs",{"title":1595,"path":1596,"stem":1597},"Діалоги та File Pickers у WPF","\u002Fcsharp\u002Fdesktop-ui\u002Fdialogs-file-pickers","01.csharp\u002F12.desktop-ui\u002F36.dialogs-file-pickers",{"title":1599,"path":1600,"stem":1601},"UserControl: компонентний підхід у WPF","\u002Fcsharp\u002Fdesktop-ui\u002Fuser-controls","01.csharp\u002F12.desktop-ui\u002F37.user-controls",{"title":1603,"path":1604,"stem":1605},"Custom Controls: Lookless Controls у WPF","\u002Fcsharp\u002Fdesktop-ui\u002Fcustom-controls","01.csharp\u002F12.desktop-ui\u002F38.custom-controls",{"title":1607,"path":1608,"stem":1609},"Avalonia TemplatedControl — Lookless Controls","\u002Fcsharp\u002Fdesktop-ui\u002F38a.avalonia-templated-controls","01.csharp\u002F12.desktop-ui\u002F38a.avalonia-templated-controls",{"title":1611,"path":1612,"stem":1613},"Анімації у WPF: Storyboard та Easing Functions","\u002Fcsharp\u002Fdesktop-ui\u002Fanimations-transitions","01.csharp\u002F12.desktop-ui\u002F39.animations-transitions",{"title":1615,"path":1616,"stem":1617},"Анімації в Avalonia","\u002Fcsharp\u002Fdesktop-ui\u002F39a.avalonia-animations","01.csharp\u002F12.desktop-ui\u002F39a.avalonia-animations",{"title":1619,"path":1620,"stem":1621},"2D Графіка та Мультимедіа у WPF","\u002Fcsharp\u002Fdesktop-ui\u002Fmedia-graphics","01.csharp\u002F12.desktop-ui\u002F40.media-graphics",{"title":1623,"path":1624,"stem":1625},"Dependency Injection у WPF та Avalonia","\u002Fcsharp\u002Fdesktop-ui\u002Fdi-integration","01.csharp\u002F12.desktop-ui\u002F41.di-integration",{"title":1627,"path":1628,"stem":1629},"SQLite та EF Core у десктопних додатках","\u002Fcsharp\u002Fdesktop-ui\u002Fdata-persistence-part1","01.csharp\u002F12.desktop-ui\u002F42.data-persistence-part1",{"title":1631,"path":1632,"stem":1633},"Repository Pattern та Unit of Work","\u002Fcsharp\u002Fdesktop-ui\u002Fdata-persistence-part2","01.csharp\u002F12.desktop-ui\u002F43.data-persistence-part2",{"title":1635,"path":1636,"stem":1637},"Тестування ViewModels","\u002Fcsharp\u002Fdesktop-ui\u002Fviewmodel-testing","01.csharp\u002F12.desktop-ui\u002F44.viewmodel-testing",{"title":1639,"path":1640,"stem":1641},"Avalonia Headless Testing — тестування UI без вікон","\u002Fcsharp\u002Fdesktop-ui\u002F44a.avalonia-headless-testing","01.csharp\u002F12.desktop-ui\u002F44a.avalonia-headless-testing",{"title":1643,"path":1644,"stem":1645},"Кросплатформна розробка з Avalonia","\u002Fcsharp\u002Fdesktop-ui\u002Favalonia-cross-platform","01.csharp\u002F12.desktop-ui\u002F45.avalonia-cross-platform",{"title":1647,"path":1648,"stem":1649},"Пакування та розгортання Avalonia додатків","\u002Fcsharp\u002Fdesktop-ui\u002Favalonia-packaging-deployment","01.csharp\u002F12.desktop-ui\u002F46.avalonia-packaging-deployment",{"title":1651,"path":1652,"stem":1653},"Розгортання WPF застосунків","\u002Fcsharp\u002Fdesktop-ui\u002Fwpf-packaging-deployment","01.csharp\u002F12.desktop-ui\u002F47.wpf-packaging-deployment",{"title":1655,"icon":658,"path":1656,"stem":1657,"children":1658,"page":59},"Network Programming","\u002Fcsharp\u002Fnetwork-programming","01.csharp\u002F13.network-programming",[1659,1663,1667,1671,1675,1679,1683,1687,1691,1695,1699],{"title":1660,"path":1661,"stem":1662},"Основи комп'ютерних мереж","\u002Fcsharp\u002Fnetwork-programming\u002Ffoundations","01.csharp\u002F13.network-programming\u002F01.foundations",{"title":1664,"path":1665,"stem":1666},"Модель OSI та стек TCP\u002FIP","\u002Fcsharp\u002Fnetwork-programming\u002Fosi-model","01.csharp\u002F13.network-programming\u002F02.osi-model",{"title":1668,"path":1669,"stem":1670},"IP-протокол та адресація","\u002Fcsharp\u002Fnetwork-programming\u002Fip-addressing","01.csharp\u002F13.network-programming\u002F03.ip-addressing",{"title":1672,"path":1673,"stem":1674},"UDP — протокол без з'єднання","\u002Fcsharp\u002Fnetwork-programming\u002Fudp","01.csharp\u002F13.network-programming\u002F05.udp",{"title":1676,"path":1677,"stem":1678},"UDP Broadcast та Multicast","\u002Fcsharp\u002Fnetwork-programming\u002Fudp-broadcast-multicast","01.csharp\u002F13.network-programming\u002F06.udp-broadcast-multicast",{"title":1680,"path":1681,"stem":1682},"HTTP — протокол вебу","\u002Fcsharp\u002Fnetwork-programming\u002Fhttp-fundamentals","01.csharp\u002F13.network-programming\u002F07.http-fundamentals",{"title":1684,"path":1685,"stem":1686},"HttpListener — вбудований HTTP-сервер .NET","\u002Fcsharp\u002Fnetwork-programming\u002F07a.http-listener","01.csharp\u002F13.network-programming\u002F07a.http-listener",{"title":1688,"path":1689,"stem":1690},"HTTP Advanced — cookies, аутентифікація та HTTPS","\u002Fcsharp\u002Fnetwork-programming\u002Fhttp-advanced","01.csharp\u002F13.network-programming\u002F08.http-advanced",{"title":1692,"path":1693,"stem":1694},"SMTP та протоколи електронної пошти","\u002Fcsharp\u002Fnetwork-programming\u002Fsmtp","01.csharp\u002F13.network-programming\u002F09.smtp",{"title":1696,"path":1697,"stem":1698},"WebSocket — повнодуплексний протокол реального часу","\u002Fcsharp\u002Fnetwork-programming\u002Fwebsockets","01.csharp\u002F13.network-programming\u002F10.websockets",{"title":1700,"path":1701,"stem":1702},"TLS\u002FSSL — криптографічний захист мережевих з'єднань","\u002Fcsharp\u002Fnetwork-programming\u002Ftls-ssl","01.csharp\u002F13.network-programming\u002F11.tls-ssl",{"title":1704,"path":1705,"stem":1706},"C# & .NET: The Ultimate Roadmap","\u002Fcsharp\u002Froadmap","01.csharp\u002Froadmap",{"title":1708,"icon":1709,"path":1710,"stem":1711,"children":1712,"page":59},"C++","i-devicon-cplusplus","\u002Fcpp","02.cpp",[1713,1717,1721,1725,1729,1733,1737,1741,1745,1748,1752,1756,1760,1764,1768,1772,1776,1780,1784,1788,1792,1796,1800,1804,1808,1812,1816,1820,1824,1828,1832,1836,1840,1844,1848,1852,1856,1860,1864,1868,1872,1876,1880,1884,1888,1892,1896,1900],{"title":1714,"path":1715,"stem":1716},"Вступ у програмування та алгоритми","\u002Fcpp\u002Fintro-algorithms","02.cpp\u002F01.intro-algorithms",{"title":1718,"path":1719,"stem":1720},"Code Style: угоди про оформлення коду","\u002Fcpp\u002Fcode-style","02.cpp\u002F02.code-style",{"title":1722,"path":1723,"stem":1724},"Середовище розробки та перший проєкт","\u002Fcpp\u002Fide-setup","02.cpp\u002F03.ide-setup",{"title":1726,"path":1727,"stem":1728},"Вивід даних на екран","\u002Fcpp\u002Fdata-output","02.cpp\u002F04.data-output",{"title":1730,"path":1731,"stem":1732},"Типи даних, змінні та константи","\u002Fcpp\u002Fdata-types-variables","02.cpp\u002F05.data-types-variables",{"title":1734,"path":1735,"stem":1736},"Ввід даних з клавіатури","\u002Fcpp\u002Fdata-input","02.cpp\u002F06.data-input",{"title":1738,"path":1739,"stem":1740},"Оператори, перетворення типів та логічні операції","\u002Fcpp\u002Foperators-type-conversion","02.cpp\u002F07.operators-type-conversion",{"title":1742,"path":1743,"stem":1744},"Цикли","\u002Fcpp\u002Floops","02.cpp\u002F08.loops",{"title":32,"path":1746,"stem":1747},"\u002Fcpp\u002Farrays","02.cpp\u002F09.arrays",{"title":1749,"path":1750,"stem":1751},"Алгоритми сортування та аналіз складності","\u002Fcpp\u002Fsorting","02.cpp\u002F10.sorting",{"title":1753,"path":1754,"stem":1755},"Алгоритми пошуку","\u002Fcpp\u002Fsearching","02.cpp\u002F11.searching",{"title":1757,"path":1758,"stem":1759},"Функції: основи","\u002Fcpp\u002Ffunctions-basics","02.cpp\u002F12.functions-basics",{"title":1761,"path":1762,"stem":1763},"Функції: прототипи, область видимості та додаткові можливості","\u002Fcpp\u002Ffunctions-scope","02.cpp\u002F13.functions-scope",{"title":1765,"path":1766,"stem":1767},"Функції: перевантаження та шаблони","\u002Fcpp\u002Ffunctions-overloading-templates","02.cpp\u002F14.functions-overloading-templates",{"title":1769,"path":1770,"stem":1771},"Вказівники: основи","\u002Fcpp\u002Fpointers-basics","02.cpp\u002F15.pointers-basics",{"title":1773,"path":1774,"stem":1775},"Посилання (References)","\u002Fcpp\u002Freferences","02.cpp\u002F16.references",{"title":1777,"path":1778,"stem":1779},"Вказівники, const і масиви","\u002Fcpp\u002Fpointers-const-arrays","02.cpp\u002F17.pointers-const-arrays",{"title":1781,"path":1782,"stem":1783},"Адресна арифметика","\u002Fcpp\u002Fpointer-arithmetic","02.cpp\u002F18.pointer-arithmetic",{"title":1785,"path":1786,"stem":1787},"Динамічна пам'ять","\u002Fcpp\u002Fdynamic-memory","02.cpp\u002F19.dynamic-memory",{"title":1789,"path":1790,"stem":1791},"Вказівники типу void","\u002Fcpp\u002Fvoid-pointers","02.cpp\u002F20.void-pointers",{"title":1793,"path":1794,"stem":1795},"Вказівники на вказівники","\u002Fcpp\u002Fpointers-to-pointers","02.cpp\u002F21.pointers-to-pointers",{"title":1797,"path":1798,"stem":1799},"Оператор доступу до членів через вказівник (->)","\u002Fcpp\u002Fmember-access-operator","02.cpp\u002F22.member-access-operator",{"title":1801,"path":1802,"stem":1803},"Цикл for-each (Range-based for)","\u002Fcpp\u002Fforeach-loop","02.cpp\u002F23.foreach-loop",{"title":1805,"path":1806,"stem":1807},"Вказівники на функції","\u002Fcpp\u002Ffunction-pointers","02.cpp\u002F24.function-pointers",{"title":1809,"path":1810,"stem":1811},"Лямбда-вирази","\u002Fcpp\u002Flambdas","02.cpp\u002F25.lambdas",{"title":1813,"path":1814,"stem":1815},"Лямбда-захоплення","\u002Fcpp\u002Flambda-captures","02.cpp\u002F26.lambda-captures",{"title":1817,"path":1818,"stem":1819},"Еліпсис","\u002Fcpp\u002Fellipsis","02.cpp\u002F27.ellipsis",{"title":1821,"path":1822,"stem":1823},"Безпечні альтернативи еліпсису","\u002Fcpp\u002F27a.ellipsis","02.cpp\u002F27a.ellipsis",{"title":1825,"path":1826,"stem":1827},"Аргументи командного рядка","\u002Fcpp\u002Fcommand-line-arguments","02.cpp\u002F28.command-line-arguments",{"title":1829,"path":1830,"stem":1831},"Перерахування (enum)","\u002Fcpp\u002Fenum","02.cpp\u002F29.enum",{"title":1833,"path":1834,"stem":1835},"Класи-перерахування (enum class)","\u002Fcpp\u002Fenum-class","02.cpp\u002F30.enum-class",{"title":1837,"path":1838,"stem":1839},"Псевдоніми типів (typedef і using)","\u002Fcpp\u002Ftype-aliases","02.cpp\u002F31.type-aliases",{"title":1841,"path":1842,"stem":1843},"Системи числення та двійкова арифметика","\u002Fcpp\u002Fnumber-systems","02.cpp\u002F32.number-systems",{"title":1845,"path":1846,"stem":1847},"Структури (struct): агрегування даних","\u002Fcpp\u002Fstruct","02.cpp\u002F33.struct",{"title":1849,"path":1850,"stem":1851},"Структури у функціях","\u002Fcpp\u002Fstruct-functions","02.cpp\u002F34.struct-functions",{"title":1853,"path":1854,"stem":1855},"Масиви структур і вкладені структури","\u002Fcpp\u002Fstruct-arrays","02.cpp\u002F35.struct-arrays",{"title":1857,"path":1858,"stem":1859},"Патерни struct та межі застосування","\u002Fcpp\u002Fstruct-patterns","02.cpp\u002F36.struct-patterns",{"title":1861,"path":1862,"stem":1863},"Символи та таблиця ASCII","\u002Fcpp\u002Fascii-characters","02.cpp\u002F37.ascii-characters",{"title":1865,"path":1866,"stem":1867},"Unicode та кодування UTF","\u002Fcpp\u002Funicode-utf","02.cpp\u002F38.unicode-utf",{"title":1869,"path":1870,"stem":1871},"C-style рядки","\u002Fcpp\u002Fc-strings","02.cpp\u002F39.c-strings",{"title":1873,"path":1874,"stem":1875},"Вступ до std::string","\u002Fcpp\u002Fstd-string-intro","02.cpp\u002F40.std-string-intro",{"title":1877,"path":1878,"stem":1879},"Довжина, ємність та доступ до символів std::string","\u002Fcpp\u002Fstd-string-capacity-access","02.cpp\u002F41.std-string-capacity-access",{"title":1881,"path":1882,"stem":1883},"Модифікація std::string: присвоювання, додавання, вставка, видалення та заміна","\u002Fcpp\u002Fstd-string-modification","02.cpp\u002F42.std-string-modification",{"title":1885,"path":1886,"stem":1887},"Пошук у std::string: find, npos та практичні патерни","\u002Fcpp\u002Fstd-string-search","02.cpp\u002F43.std-string-search",{"title":1889,"path":1890,"stem":1891},"std::string_view: невласницький погляд на рядок без копіювання","\u002Fcpp\u002Fstd-string-view","02.cpp\u002F44.std-string-view",{"title":1893,"path":1894,"stem":1895},"Об'єднання (union): один блок пам'яті, кілька інтерпретацій","\u002Fcpp\u002Funion","02.cpp\u002F45.union",{"title":1897,"path":1898,"stem":1899},"Організація коду: файли, препроцесор, простори імен","\u002Fcpp\u002Fmultifile-programs","02.cpp\u002F46.multifile-programs",{"title":1901,"path":1902,"stem":1903},"План навчання: Курс C++ — Продовження (Статті 29–60+)","\u002Fcpp\u002Fcurriculum-plan","02.cpp\u002Fcurriculum-plan",{"title":1905,"icon":1906,"path":1907,"stem":1908,"children":1909,"page":59},"JavaScript","i-devicon-javascript","\u002Fjavascript","03.javascript",[1910,1936,1990,2012,2316,2354],{"title":1911,"icon":1912,"path":1913,"stem":1914,"children":1915,"page":59},"Events","i-lucide-mouse-pointer-click","\u002Fjavascript\u002Fevents","03.javascript\u002F01.events",[1916,1920,1924,1928,1932],{"title":1917,"path":1918,"stem":1919},"Вступ до подій браузера","\u002Fjavascript\u002Fevents\u002Fintro","03.javascript\u002F01.events\u002F01.intro",{"title":1921,"path":1922,"stem":1923},"Бульбашковий механізм (Bubbling) та занурення (Capturing)","\u002Fjavascript\u002Fevents\u002Fbubbling-capturing","03.javascript\u002F01.events\u002F02.bubbling-capturing",{"title":1925,"path":1926,"stem":1927},"Делегування подій (Event Delegation)","\u002Fjavascript\u002Fevents\u002Fdelegate-events","03.javascript\u002F01.events\u002F03.delegate-events",{"title":1929,"path":1930,"stem":1931},"Типові дії браузера та preventDefault()","\u002Fjavascript\u002Fevents\u002Fprevent-default","03.javascript\u002F01.events\u002F04.prevent-default",{"title":1933,"path":1934,"stem":1935},"Запуск користувацьких подій (Custom Events)","\u002Fjavascript\u002Fevents\u002Fcustom-events","03.javascript\u002F01.events\u002F05.custom-events",{"title":1937,"icon":1938,"path":1939,"stem":1940,"children":1941,"page":59},"Network","i-lucide-globe","\u002Fjavascript\u002Fnetwork","03.javascript\u002F02.network",[1942,1946,1950,1954,1958,1962,1966,1970,1974,1978,1982,1986],{"title":1943,"path":1944,"stem":1945},"Fetch API - Сучасний підхід до HTTP-запитів","\u002Fjavascript\u002Fnetwork\u002F01-fetch-api","03.javascript\u002F02.network\u002F01-fetch-api",{"title":1947,"path":1948,"stem":1949},"FormData - Робота з формами та файлами","\u002Fjavascript\u002Fnetwork\u002F02-formdata","03.javascript\u002F02.network\u002F02-formdata",{"title":1951,"path":1952,"stem":1953},"Відстеження прогресу завантаження","\u002Fjavascript\u002Fnetwork\u002F03-download-progress","03.javascript\u002F02.network\u002F03-download-progress",{"title":1955,"path":1956,"stem":1957},"Переривання fetch-запитів","\u002Fjavascript\u002Fnetwork\u002F04-abort-requests","03.javascript\u002F02.network\u002F04-abort-requests",{"title":1959,"path":1960,"stem":1961},"CORS - Запити між різними джерелами","\u002Fjavascript\u002Fnetwork\u002F05-cors","03.javascript\u002F02.network\u002F05-cors",{"title":1963,"path":1964,"stem":1965},"Fetch API - Повний довідник опцій","\u002Fjavascript\u002Fnetwork\u002F06-fetch-options","03.javascript\u002F02.network\u002F06-fetch-options",{"title":1967,"path":1968,"stem":1969},"URL Objects - Робота з посиланнями","\u002Fjavascript\u002Fnetwork\u002F07-url-objects","03.javascript\u002F02.network\u002F07-url-objects",{"title":1971,"path":1972,"stem":1973},"XMLHttpRequest - AJAX та низькорівневі запити","\u002Fjavascript\u002Fnetwork\u002F08-xmlhttprequest","03.javascript\u002F02.network\u002F08-xmlhttprequest",{"title":1975,"path":1976,"stem":1977},"Відновлюване завантаження файлів","\u002Fjavascript\u002Fnetwork\u002F09-resumable-upload","03.javascript\u002F02.network\u002F09-resumable-upload",{"title":1979,"path":1980,"stem":1981},"Cookies, document.cookie та світ після \"Cookiepocalypse\"","\u002Fjavascript\u002Fnetwork\u002F10-cookies","03.javascript\u002F02.network\u002F10-cookies",{"title":1983,"path":1984,"stem":1985},"js-cookie: Керування Cookies без Болю","\u002Fjavascript\u002Fnetwork\u002F11-js-cookie","03.javascript\u002F02.network\u002F11-js-cookie",{"title":1987,"path":1988,"stem":1989},"Axios: Потужний HTTP-клієнт для JavaScript","\u002Fjavascript\u002Fnetwork\u002F12-axios","03.javascript\u002F02.network\u002F12-axios",{"title":1991,"icon":1992,"path":1993,"stem":1994,"children":1995,"page":59},"Bom","i-lucide-monitor","\u002Fjavascript\u002Fbom","03.javascript\u002F03.bom",[1996,2000,2004,2008],{"title":1997,"path":1998,"stem":1999},"LocalStorage, SessionStorage та patterns збереження даних","\u002Fjavascript\u002Fbom\u002F01-localstorage","03.javascript\u002F03.bom\u002F01-localstorage",{"title":2001,"path":2002,"stem":2003},"Location Object - Керування адресою сторінки","\u002Fjavascript\u002Fbom\u002F02-location-object","03.javascript\u002F03.bom\u002F02-location-object",{"title":2005,"path":2006,"stem":2007},"History API - Керування історією браузера","\u002Fjavascript\u002Fbom\u002F03-history-api","03.javascript\u002F03.bom\u002F03-history-api",{"title":2009,"path":2010,"stem":2011},"Navigator Object - Ідентифікація та Можливості Пристрою","\u002Fjavascript\u002Fbom\u002F04-navigator-object","03.javascript\u002F03.bom\u002F04-navigator-object",{"title":2013,"icon":2014,"path":2015,"stem":2016,"children":2017},"React","i-devicon-react","\u002Fjavascript\u002Freact","03.javascript\u002F04.react\u002Findex",[2018,2019,2023,2027,2031,2035,2098,2133,2285],{"title":2013,"path":2015,"stem":2016},{"title":2020,"path":2021,"stem":2022},"Робота з Формами в React","\u002Fjavascript\u002Freact\u002Freact-forms","03.javascript\u002F04.react\u002F01.react-forms",{"title":2024,"path":2025,"stem":2026},"React Hook Form: Професійна Робота з Формами","\u002Fjavascript\u002Freact\u002Freact-hook-form","03.javascript\u002F04.react\u002F02.react-hook-form",{"title":2028,"path":2029,"stem":2030},"React Hook Form: Глибоке Розуміння Архітектури та Оптимізації","\u002Fjavascript\u002Freact\u002Freact-hook-form-new","03.javascript\u002F04.react\u002F02.react-hook-form-new",{"title":2032,"path":2033,"stem":2034},"Axios та React: Професійна Архітектура Запитів","\u002Fjavascript\u002Freact\u002Fdata-fetching-axios","03.javascript\u002F04.react\u002F03.data-fetching-axios",{"title":2036,"icon":132,"path":2037,"stem":2038,"children":2039},"Tanstack Query","\u002Fjavascript\u002Freact\u002Ftanstack-query","03.javascript\u002F04.react\u002F04.tanstack-query\u002Findex",[2040,2042,2046,2050,2054,2058,2062,2066,2070,2074,2078,2082,2086,2090,2094],{"title":2041,"path":2037,"stem":2038},"TanStack Query: Майстерність Керування Станом Сервера",{"title":2043,"path":2044,"stem":2045},"Парадигма Server State: Чому useEffect недостатньо","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fserver-state-paradigm","03.javascript\u002F04.react\u002F04.tanstack-query\u002F01.server-state-paradigm",{"title":2047,"path":2048,"stem":2049},"Встановлення та Налаштування: Фундамент","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Finstallation-and-devtools","03.javascript\u002F04.react\u002F04.tanstack-query\u002F02.installation-and-devtools",{"title":2051,"path":2052,"stem":2053},"Основи Запитів та Магія Ключів","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fquery-basics-and-keys","03.javascript\u002F04.react\u002F04.tanstack-query\u002F03.query-basics-and-keys",{"title":2055,"path":2056,"stem":2057},"Синхронізація Даних: Життєвий Цикл Запиту","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fdata-synchronization","03.javascript\u002F04.react\u002F04.tanstack-query\u002F04.data-synchronization",{"title":2059,"path":2060,"stem":2061},"Мутації та Інвалідація: Зміна Даних","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fmutations-and-invalidation","03.javascript\u002F04.react\u002F04.tanstack-query\u002F05.mutations-and-invalidation",{"title":2063,"path":2064,"stem":2065},"Оптимістичні Оновлення: Швидше за Світло","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Foptimistic-updates","03.javascript\u002F04.react\u002F04.tanstack-query\u002F06.optimistic-updates",{"title":2067,"path":2068,"stem":2069},"Пагінація та Infinite Scroll","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fpagination-and-load-more","03.javascript\u002F04.react\u002F04.tanstack-query\u002F07.pagination-and-load-more",{"title":2071,"path":2072,"stem":2073},"Просунуті Патерни та Оптимізація","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fadvanced-patterns","03.javascript\u002F04.react\u002F04.tanstack-query\u002F08.advanced-patterns",{"title":2075,"path":2076,"stem":2077},"Архітектура та Best Practices","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Farchitecture-and-best-practices","03.javascript\u002F04.react\u002F04.tanstack-query\u002F09.architecture-and-best-practices",{"title":2079,"path":2080,"stem":2081},"Server-Side Rendering (SSR) та Гідратація","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fserver-side-rendering","03.javascript\u002F04.react\u002F04.tanstack-query\u002F10.server-side-rendering",{"title":2083,"path":2084,"stem":2085},"Стратегії Тестування","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Ftesting-strategies","03.javascript\u002F04.react\u002F04.tanstack-query\u002F11.testing-strategies",{"title":2087,"path":2088,"stem":2089},"Аутентифікація та Обробка Помилок","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fauthentication-and-errors","03.javascript\u002F04.react\u002F04.tanstack-query\u002F12.authentication-and-errors",{"title":2091,"path":2092,"stem":2093},"React Suspense та Майбутнє","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Freact-suspense","03.javascript\u002F04.react\u002F04.tanstack-query\u002F13.react-suspense",{"title":2095,"path":2096,"stem":2097},"Глибоке Занурення в Продуктивність","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fperformance-deep-dive","03.javascript\u002F04.react\u002F04.tanstack-query\u002F14.performance-deep-dive",{"title":2099,"icon":2014,"path":2100,"stem":2101,"children":2102},"React Router","\u002Fjavascript\u002Freact\u002Freact-router","03.javascript\u002F04.react\u002F05.react-router\u002Findex",[2103,2105,2109,2113,2117,2121,2125,2129],{"title":2104,"path":2100,"stem":2101},"React Router: Навігаційна система сучасного вебу",{"title":2106,"path":2107,"stem":2108},"Налаштування та Базовий Роутинг","\u002Fjavascript\u002Freact\u002Freact-router\u002Fsetup-and-basic-routing","03.javascript\u002F04.react\u002F05.react-router\u002F01.setup-and-basic-routing",{"title":2110,"path":2111,"stem":2112},"Динамічна Навігація","\u002Fjavascript\u002Freact\u002Freact-router\u002Fnavigation-and-links","03.javascript\u002F04.react\u002F05.react-router\u002F02.navigation-and-links",{"title":2114,"path":2115,"stem":2116},"Вкладені Маршрути та Макети","\u002Fjavascript\u002Freact\u002Freact-router\u002Fnested-routes-and-layouts","03.javascript\u002F04.react\u002F05.react-router\u002F03.nested-routes-and-layouts",{"title":2118,"path":2119,"stem":2120},"Динамічні Маршрути та Параметри","\u002Fjavascript\u002Freact\u002Freact-router\u002Fdynamic-routing","03.javascript\u002F04.react\u002F05.react-router\u002F04.dynamic-routing",{"title":2122,"path":2123,"stem":2124},"Data APIs: Loaders та Actions","\u002Fjavascript\u002Freact\u002Freact-router\u002Fdata-loading","03.javascript\u002F04.react\u002F05.react-router\u002F05.data-loading",{"title":2126,"path":2127,"stem":2128},"Просунуті Патерни","\u002Fjavascript\u002Freact\u002Freact-router\u002Fadvanced-patterns","03.javascript\u002F04.react\u002F05.react-router\u002F06.advanced-patterns",{"title":2130,"path":2131,"stem":2132},"Legacy Routing: Компонентний підхід","\u002Fjavascript\u002Freact\u002Freact-router\u002Flegacy-routing","03.javascript\u002F04.react\u002F05.react-router\u002F07.legacy-routing",{"title":2134,"icon":132,"path":2135,"stem":2136,"children":2137},"Redux","\u002Fjavascript\u002Freact\u002Fredux","03.javascript\u002F04.react\u002F06.redux\u002Findex",[2138,2140,2156,2185,2194,2215,2231,2260],{"title":2139,"path":2135,"stem":2136},"Redux: Еволюція управління станом",{"title":14,"icon":15,"path":2141,"stem":2142,"children":2143,"page":59},"\u002Fjavascript\u002Freact\u002Fredux\u002Ffundamentals","03.javascript\u002F04.react\u002F06.redux\u002F01.fundamentals",[2144,2148,2152],{"title":2145,"path":2146,"stem":2147},"Вступ до State Management","\u002Fjavascript\u002Freact\u002Fredux\u002Ffundamentals\u002Fintro-state-management","03.javascript\u002F04.react\u002F06.redux\u002F01.fundamentals\u002F01.intro-state-management",{"title":2149,"path":2150,"stem":2151},"Філософія Redux та Три Принципи","\u002Fjavascript\u002Freact\u002Fredux\u002Ffundamentals\u002Fredux-philosophy","03.javascript\u002F04.react\u002F06.redux\u002F01.fundamentals\u002F02.redux-philosophy",{"title":2153,"path":2154,"stem":2155},"Чисті функції та Іммутабельність","\u002Fjavascript\u002Freact\u002Fredux\u002Ffundamentals\u002Fpure-functions-immutability","03.javascript\u002F04.react\u002F06.redux\u002F01.fundamentals\u002F03.pure-functions-immutability",{"title":2157,"icon":132,"path":2158,"stem":2159,"children":2160,"page":59},"Classic Redux","\u002Fjavascript\u002Freact\u002Fredux\u002Fclassic-redux","03.javascript\u002F04.react\u002F06.redux\u002F02.classic-redux",[2161,2165,2169,2173,2177,2181],{"title":2162,"path":2163,"stem":2164},"Створення Store (Classic Redux)","\u002Fjavascript\u002Freact\u002Fredux\u002Fclassic-redux\u002Fstore-setup","03.javascript\u002F04.react\u002F06.redux\u002F02.classic-redux\u002F01.store-setup",{"title":2166,"path":2167,"stem":2168},"Actions, Constants та Action Creators","\u002Fjavascript\u002Freact\u002Fredux\u002Fclassic-redux\u002Factions-constants","03.javascript\u002F04.react\u002F06.redux\u002F02.classic-redux\u002F02.actions-constants",{"title":2170,"path":2171,"stem":2172},"Логіка Reducers","\u002Fjavascript\u002Freact\u002Fredux\u002Fclassic-redux\u002Freducers","03.javascript\u002F04.react\u002F06.redux\u002F02.classic-redux\u002F03.reducers",{"title":2174,"path":2175,"stem":2176},"Комбінування Reducers (Root Reducer)","\u002Fjavascript\u002Freact\u002Fredux\u002Fclassic-redux\u002Fdata-flow","03.javascript\u002F04.react\u002F06.redux\u002F02.classic-redux\u002F04.data-flow",{"title":2178,"path":2179,"stem":2180},"Підключення до React (React-Redux)","\u002Fjavascript\u002Freact\u002Fredux\u002Fclassic-redux\u002Freact-redux-connection","03.javascript\u002F04.react\u002F06.redux\u002F02.classic-redux\u002F05.react-redux-connection",{"title":2182,"path":2183,"stem":2184},"Middleware та Асинхронність (Redux Thunk)","\u002Fjavascript\u002Freact\u002Fredux\u002Fclassic-redux\u002Fmiddleware-thunk","03.javascript\u002F04.react\u002F06.redux\u002F02.classic-redux\u002F06.middleware-thunk",{"title":2186,"icon":132,"path":2187,"stem":2188,"children":2189,"page":59},"Transition To Rtk","\u002Fjavascript\u002Freact\u002Fredux\u002Ftransition-to-rtk","03.javascript\u002F04.react\u002F06.redux\u002F03.transition-to-rtk",[2190],{"title":2191,"path":2192,"stem":2193},"Проблеми класичного Redux","\u002Fjavascript\u002Freact\u002Fredux\u002Ftransition-to-rtk\u002Fproblems-with-classic","03.javascript\u002F04.react\u002F06.redux\u002F03.transition-to-rtk\u002F01.problems-with-classic",{"title":2195,"icon":132,"path":2196,"stem":2197,"children":2198,"page":59},"Redux Toolkit","\u002Fjavascript\u002Freact\u002Fredux\u002Fredux-toolkit","03.javascript\u002F04.react\u002F06.redux\u002F04.redux-toolkit",[2199,2203,2207,2211],{"title":2200,"path":2201,"stem":2202},"Налаштування Store з configureStore","\u002Fjavascript\u002Freact\u002Fredux\u002Fredux-toolkit\u002Fconfigure-store","03.javascript\u002F04.react\u002F06.redux\u002F04.redux-toolkit\u002F01.configure-store",{"title":2204,"path":2205,"stem":2206},"createSlice: Революція в Redux","\u002Fjavascript\u002Freact\u002Fredux\u002Fredux-toolkit\u002Fcreate-slice","03.javascript\u002F04.react\u002F06.redux\u002F04.redux-toolkit\u002F02.create-slice",{"title":2208,"path":2209,"stem":2210},"Асинхронність з createAsyncThunk","\u002Fjavascript\u002Freact\u002Fredux\u002Fredux-toolkit\u002Fasync-thunks","03.javascript\u002F04.react\u002F06.redux\u002F04.redux-toolkit\u002F03.async-thunks",{"title":2212,"path":2213,"stem":2214},"04. Entity Adapter: Керування нормалізованим станом","\u002Fjavascript\u002Freact\u002Fredux\u002Fredux-toolkit\u002Fentity-adapter","03.javascript\u002F04.react\u002F06.redux\u002F04.redux-toolkit\u002F04.entity-adapter",{"title":2216,"icon":92,"path":2217,"stem":2218,"children":2219,"page":59},"Advanced","\u002Fjavascript\u002Freact\u002Fredux\u002Fadvanced","03.javascript\u002F04.react\u002F06.redux\u002F05.advanced",[2220,2224,2228],{"title":2221,"path":2222,"stem":2223},"Мемоізація та Селектори: Повний Гайд по Reselect","\u002Fjavascript\u002Freact\u002Fredux\u002Fadvanced\u002Fselectors-reselect","03.javascript\u002F04.react\u002F06.redux\u002F05.advanced\u002F01.selectors-reselect",{"title":2225,"path":2226,"stem":2227},"RTK Query: Архітектура Серверного Кешу","\u002Fjavascript\u002Freact\u002Fredux\u002Fadvanced\u002Frtk-query-intro","03.javascript\u002F04.react\u002F06.redux\u002F05.advanced\u002F02.rtk-query-intro",{"title":2075,"path":2229,"stem":2230},"\u002Fjavascript\u002Freact\u002Fredux\u002Fadvanced\u002Farchitecture-best-practices","03.javascript\u002F04.react\u002F06.redux\u002F05.advanced\u002F03.architecture-best-practices",{"title":2232,"icon":132,"path":2233,"stem":2234,"children":2235,"page":59},"Project Kanban","\u002Fjavascript\u002Freact\u002Fredux\u002Fproject-kanban","03.javascript\u002F04.react\u002F06.redux\u002F06.project-kanban",[2236,2240,2244,2248,2252,2256],{"title":2237,"path":2238,"stem":2239},"Проєкт: Kanban Board (Trello Clone)","\u002Fjavascript\u002Freact\u002Fredux\u002Fproject-kanban\u002Fproject-overview","03.javascript\u002F04.react\u002F06.redux\u002F06.project-kanban\u002F01.project-overview",{"title":2241,"path":2242,"stem":2243},"Налаштування та Типізація","\u002Fjavascript\u002Freact\u002Fredux\u002Fproject-kanban\u002Fsetup-and-types","03.javascript\u002F04.react\u002F06.redux\u002F06.project-kanban\u002F02.setup-and-types",{"title":2245,"path":2246,"stem":2247},"Board Slice: Серце Дошки","\u002Fjavascript\u002Freact\u002Fredux\u002Fproject-kanban\u002Fboard-slice","03.javascript\u002F04.react\u002F06.redux\u002F06.project-kanban\u002F03.board-slice",{"title":2249,"path":2250,"stem":2251},"Логіка Drag & Drop","\u002Fjavascript\u002Freact\u002Fredux\u002Fproject-kanban\u002Fdrag-and-drop-logic","03.javascript\u002F04.react\u002F06.redux\u002F06.project-kanban\u002F04.drag-and-drop-logic",{"title":2253,"path":2254,"stem":2255},"Інтеграція з RTK Query","\u002Fjavascript\u002Freact\u002Fredux\u002Fproject-kanban\u002Frtk-query-integration","03.javascript\u002F04.react\u002F06.redux\u002F06.project-kanban\u002F05.rtk-query-integration",{"title":2257,"path":2258,"stem":2259},"Optimistic Updates","\u002Fjavascript\u002Freact\u002Fredux\u002Fproject-kanban\u002Foptimistic-updates","03.javascript\u002F04.react\u002F06.redux\u002F06.project-kanban\u002F06.optimistic-updates",{"title":2261,"icon":132,"path":2262,"stem":2263,"children":2264,"page":59},"Testing","\u002Fjavascript\u002Freact\u002Fredux\u002Ftesting","03.javascript\u002F04.react\u002F06.redux\u002F07.testing",[2265,2269,2273,2277,2281],{"title":2266,"path":2267,"stem":2268},"Тестування Redux","\u002Fjavascript\u002Freact\u002Fredux\u002Ftesting\u002Fintro-testing","03.javascript\u002F04.react\u002F06.redux\u002F07.testing\u002F01.intro-testing",{"title":2270,"path":2271,"stem":2272},"Тестування Reducers","\u002Fjavascript\u002Freact\u002Fredux\u002Ftesting\u002Ftesting-reducers","03.javascript\u002F04.react\u002F06.redux\u002F07.testing\u002F02.testing-reducers",{"title":2274,"path":2275,"stem":2276},"Тестування Селекторів","\u002Fjavascript\u002Freact\u002Fredux\u002Ftesting\u002Ftesting-selectors","03.javascript\u002F04.react\u002F06.redux\u002F07.testing\u002F03.testing-selectors",{"title":2278,"path":2279,"stem":2280},"Тестування Компонентів (Integration)","\u002Fjavascript\u002Freact\u002Fredux\u002Ftesting\u002Ftesting-components","03.javascript\u002F04.react\u002F06.redux\u002F07.testing\u002F04.testing-components",{"title":2282,"path":2283,"stem":2284},"Тестування Async Thunks","\u002Fjavascript\u002Freact\u002Fredux\u002Ftesting\u002Ftesting-thunks","03.javascript\u002F04.react\u002F06.redux\u002F07.testing\u002F05.testing-thunks",{"title":2286,"icon":132,"path":2287,"stem":2288,"children":2289},"Ui Libraries","\u002Fjavascript\u002Freact\u002Fui-libraries","03.javascript\u002F04.react\u002F07.ui-libraries\u002Findex",[2290,2292,2296,2300,2304,2308,2312],{"title":2291,"path":2287,"stem":2288},"UI Бібліотеки в React",{"title":2293,"path":2294,"stem":2295},"Вступ до UI Бібліотек: Навіщо Винаходити Велосипед Двічі?","\u002Fjavascript\u002Freact\u002Fui-libraries\u002Fintroduction-to-ui-libraries","03.javascript\u002F04.react\u002F07.ui-libraries\u002F01.introduction-to-ui-libraries",{"title":2297,"path":2298,"stem":2299},"Філософія shadcn\u002Fui: \"Not a Component Library\"","\u002Fjavascript\u002Freact\u002Fui-libraries\u002Fshadcn-philosophy","03.javascript\u002F04.react\u002F07.ui-libraries\u002F02.shadcn-philosophy",{"title":2301,"path":2302,"stem":2303},"Установка та Налаштування shadcn\u002Fui","\u002Fjavascript\u002Freact\u002Fui-libraries\u002Fshadcn-installation","03.javascript\u002F04.react\u002F07.ui-libraries\u002F03.shadcn-installation",{"title":2305,"path":2306,"stem":2307},"Базові Компоненти shadcn\u002Fui: Фундамент Інтерфейсу","\u002Fjavascript\u002Freact\u002Fui-libraries\u002Fshadcn-components-basics","03.javascript\u002F04.react\u002F07.ui-libraries\u002F04.shadcn-components-basics",{"title":2309,"path":2310,"stem":2311},"Компоненти Форм: Побудова Інтерактивних Form","\u002Fjavascript\u002Freact\u002Fui-libraries\u002Fshadcn-components-forms","03.javascript\u002F04.react\u002F07.ui-libraries\u002F05.shadcn-components-forms",{"title":2313,"path":2314,"stem":2315},"Складні Компоненти: Dialog, Dropdown, Table та Command","\u002Fjavascript\u002Freact\u002Fui-libraries\u002Fshadcn-components-advanced","03.javascript\u002F04.react\u002F07.ui-libraries\u002F06.shadcn-components-advanced",{"title":2317,"icon":2318,"path":2319,"stem":2320,"children":2321,"page":59},"TypeScript","i-devicon-typescript","\u002Fjavascript\u002Ftypescript","03.javascript\u002F05.typescript",[2322,2326,2330,2334,2338,2342,2346,2350],{"title":2323,"path":2324,"stem":2325},"TypeScript: Броня для вашого коду","\u002Fjavascript\u002Ftypescript\u002Fintro-and-basic-types","03.javascript\u002F05.typescript\u002F01.intro-and-basic-types",{"title":2327,"path":2328,"stem":2329},"Майстерність Моделювання Даних: Інтерфейси та Просунуті Типи","\u002Fjavascript\u002Ftypescript\u002Finterfaces-and-advanced-types","03.javascript\u002F05.typescript\u002F02.interfaces-and-advanced-types",{"title":2331,"path":2332,"stem":2333},"Алхімія Типів: Generics та Utility Types","\u002Fjavascript\u002Ftypescript\u002Fgenerics-and-utilities","03.javascript\u002F05.typescript\u002F03.generics-and-utilities",{"title":2335,"path":2336,"stem":2337},"Архітектура та Шаблони: Класи в TypeScript","\u002Fjavascript\u002Ftypescript\u002Fclasses-and-oop","03.javascript\u002F05.typescript\u002F04.classes-and-oop",{"title":2339,"path":2340,"stem":2341},"Продакшн та Екосистема: Advanced Config & Workflow","\u002Fjavascript\u002Ftypescript\u002Fadvanced-patterns-and-config","03.javascript\u002F05.typescript\u002F05.advanced-patterns-and-config",{"title":2343,"path":2344,"stem":2345},"TypeScript у світі React","\u002Fjavascript\u002Ftypescript\u002Freact-basics","03.javascript\u002F05.typescript\u002F06.react-basics",{"title":2347,"path":2348,"stem":2349},"React + TypeScript: Продвинуті патерни","\u002Fjavascript\u002Ftypescript\u002Freact-advanced","03.javascript\u002F05.typescript\u002F07.react-advanced",{"title":2351,"path":2352,"stem":2353},"React + TypeScript: Екосистема та бібліотеки","\u002Fjavascript\u002Ftypescript\u002Freact-ecosystem","03.javascript\u002F05.typescript\u002F08.react-ecosystem",{"title":2355,"path":2356,"stem":2357},"Atomic Design","\u002Fjavascript\u002Fatomic-design","03.javascript\u002F2.atomic-design",{"title":2359,"icon":2360,"path":2361,"stem":2362,"children":2363,"page":59},"Java","i-devicon-java","\u002Fjava","04.java",[2364,2367,2370,2374,2378,2382,2386],{"title":162,"path":2365,"stem":2366},"\u002Fjava\u002Fdata-mapper-part1","04.java\u002F01.data-mapper-part1",{"title":166,"path":2368,"stem":2369},"\u002Fjava\u002Fdata-mapper-part2","04.java\u002F02.data-mapper-part2",{"title":2371,"path":2372,"stem":2373},"Service Layer: Організація бізнес-логіки","\u002Fjava\u002Fservice-layer","04.java\u002F03.service-layer",{"title":2375,"path":2376,"stem":2377},"Rich Domain Model та State Pattern","\u002Fjava\u002Frich-domain-model","04.java\u002F04.rich-domain-model",{"title":2379,"path":2380,"stem":2381},"Патерни для складної бізнес-логіки","\u002Fjava\u002Fbusiness-logic-patterns","04.java\u002F05.business-logic-patterns",{"title":2383,"path":2384,"stem":2385},"Обробка помилок та валідація","\u002Fjava\u002Ferror-handling-validation","04.java\u002F06.error-handling-validation",{"title":2387,"path":2388,"stem":2389,"children":2390,"page":59},"Проектування баз даних","\u002Fjava\u002Fpr2","04.java\u002Fpr2",[2391,2395,2399,2403,2407,2411,2415,2419,2423,2427,2431,2435,2439,2443,2447,2451,2455,2459,2463,2467,2471,2475,2479,2483,2487,2491,2495,2499,2503,2507,2511,2515,2519,2523,2527,2531,2535],{"title":2392,"path":2393,"stem":2394},"Концептуальне моделювання: Мистецтво розуміння предметної області","\u002Fjava\u002Fpr2\u002Fconceptual-modeling","04.java\u002Fpr2\u002F01.conceptual-modeling",{"title":2396,"path":2397,"stem":2398},"Логічне моделювання: Від бізнес-ідей до структур даних","\u002Fjava\u002Fpr2\u002Flogical-modeling","04.java\u002Fpr2\u002F02.logical-modeling",{"title":2400,"path":2401,"stem":2402},"Нормалізація: Гігієна даних та боротьба з аномаліями","\u002Fjava\u002Fpr2\u002Fnormalization","04.java\u002Fpr2\u002F03.normalization",{"title":2404,"path":2405,"stem":2406},"Фізична схема: Від абстракції до DDL","\u002Fjava\u002Fpr2\u002Fphysical-schema","04.java\u002Fpr2\u002F04.physical-schema",{"title":2408,"path":2409,"stem":2410},"Архітектурна класифікація таблиць","\u002Fjava\u002Fpr2\u002Ftable-classification","04.java\u002Fpr2\u002F05.table-classification",{"title":2412,"path":2413,"stem":2414},"Database Migrations: Версіонування схеми з Flyway","\u002Fjava\u002Fpr2\u002Fdatabase-migrations","04.java\u002Fpr2\u002F06.database-migrations",{"title":2416,"path":2417,"stem":2418},"А що, якби це була не реляційна БД?","\u002Fjava\u002Fpr2\u002Fbeyond-relational","04.java\u002Fpr2\u002F07.beyond-relational",{"title":2420,"path":2421,"stem":2422},"Object-Relational Impedance Mismatch: Два світи, що не хочуть дружити","\u002Fjava\u002Fpr2\u002Fimpedance-mismatch","04.java\u002Fpr2\u002F09.impedance-mismatch",{"title":2424,"path":2425,"stem":2426},"JDBC: Перший контакт із базою даних","\u002Fjava\u002Fpr2\u002Fjdbc-fundamentals","04.java\u002Fpr2\u002F10.jdbc-fundamentals",{"title":2428,"path":2429,"stem":2430},"Якість коду: Spotless, SpotBugs та SonarQube","\u002Fjava\u002Fpr2\u002F10a.code-quality","04.java\u002Fpr2\u002F10a.code-quality",{"title":2432,"path":2433,"stem":2434},"Connection Pool: Патерн Object Pool для JDBC-з'єднань","\u002Fjava\u002Fpr2\u002Fconnection-pool","04.java\u002Fpr2\u002F11.connection-pool",{"title":2436,"path":2437,"stem":2438},"Row Data Gateway: Об'єкт як обгортка рядка таблиці","\u002Fjava\u002Fpr2\u002Frow-data-gateway","04.java\u002Fpr2\u002F12.row-data-gateway",{"title":2440,"path":2441,"stem":2442},"Table Data Gateway: Фасад таблиці як архітектурний відступ","\u002Fjava\u002Fpr2\u002Ftable-data-gateway","04.java\u002Fpr2\u002F13.table-data-gateway",{"title":2444,"path":2445,"stem":2446},"Repository + Data Mapper: Правильна шарова архітектура з JDBC","\u002Fjava\u002Fpr2\u002Frepository-data-mapper","04.java\u002Fpr2\u002F14.repository-data-mapper",{"title":2448,"path":2449,"stem":2450},"Identity Map: Кешування сутностей у рамках сесії","\u002Fjava\u002Fpr2\u002Fidentity-map","04.java\u002Fpr2\u002F15.identity-map",{"title":2452,"path":2453,"stem":2454},"Unit of Work: Відстеження змін і координація JDBC-транзакцій","\u002Fjava\u002Fpr2\u002Funit-of-work","04.java\u002Fpr2\u002F16.unit-of-work",{"title":2456,"path":2457,"stem":2458},"Strategy: Замінювані SQL-стратегії для підтримки різних СУБД","\u002Fjava\u002Fpr2\u002Fstrategy-sql","04.java\u002Fpr2\u002F17.strategy-sql",{"title":2460,"path":2461,"stem":2462},"Proxy: Lazy Loading для One-To-Many колекцій","\u002Fjava\u002Fpr2\u002Fproxy-lazy-loading","04.java\u002Fpr2\u002F18.proxy-lazy-loading",{"title":2464,"path":2465,"stem":2466},"Generic Repository через Java Reflection: анотації та динамічний SQL","\u002Fjava\u002Fpr2\u002Fgeneric-repository-reflection","04.java\u002Fpr2\u002F19.generic-repository-reflection",{"title":2468,"path":2469,"stem":2470},"Specification Pattern: Композиція бізнес-правил для складних запитів","\u002Fjava\u002Fpr2\u002Fspecification-pattern","04.java\u002Fpr2\u002F20.specification-pattern",{"title":2472,"path":2473,"stem":2474},"Розширені можливості Specification Pattern: підзапити, агрегації та гібридний підхід","\u002Fjava\u002Fpr2\u002F20a.advanced-specifications","04.java\u002Fpr2\u002F20a.advanced-specifications",{"title":2476,"path":2477,"stem":2478},"Асинхронність у JDBC: Від блокуючих викликів до CompletableFuture","\u002Fjava\u002Fpr2\u002Fasynchronous-jdbc","04.java\u002Fpr2\u002F21.asynchronous-jdbc",{"title":2480,"path":2481,"stem":2482},"Інтеграційне тестування JDBC-репозиторіїв: Embedded H2 та патерн AAA","\u002Fjava\u002Fpr2\u002Fintegration-testing-h2","04.java\u002Fpr2\u002F22.integration-testing-h2",{"title":2484,"path":2485,"stem":2486},"Testcontainers: Тестування з реальною PostgreSQL у Docker-контейнерах","\u002Fjava\u002Fpr2\u002Fintegration-testing-testcontainers","04.java\u002Fpr2\u002F23.integration-testing-testcontainers",{"title":2488,"path":2489,"stem":2490},"Google Guice: Впровадження залежностей у JavaFX-проєкті","\u002Fjava\u002Fpr2\u002Fdependency-injection-guice","04.java\u002Fpr2\u002F24.dependency-injection-guice",{"title":2492,"path":2493,"stem":2494},"JavaFX: Основи побудови графічних інтерфейсів","\u002Fjava\u002Fpr2\u002Fjavafx-fundamentals","04.java\u002Fpr2\u002F25.javafx-fundamentals",{"title":2496,"path":2497,"stem":2498},"Properties та Bindings: Реактивність у JavaFX","\u002Fjava\u002Fpr2\u002Fjavafx-properties-bindings","04.java\u002Fpr2\u002F26.javafx-properties-bindings",{"title":2500,"path":2501,"stem":2502},"MVC vs MVP vs MVVM: Еволюція архітектурних патернів UI","\u002Fjava\u002Fpr2\u002Fui-architecture-patterns","04.java\u002Fpr2\u002F27.ui-architecture-patterns",{"title":2504,"path":2505,"stem":2506},"MVVM на практиці: Побудова ViewModel","\u002Fjava\u002Fpr2\u002Fmvvm-viewmodel-implementation","04.java\u002Fpr2\u002F28.mvvm-viewmodel-implementation",{"title":2508,"path":2509,"stem":2510},"View та Controller: Зв'язування з ViewModel через FXML","\u002Fjava\u002Fpr2\u002Fmvvm-view-controller","04.java\u002Fpr2\u002F29.mvvm-view-controller",{"title":2512,"path":2513,"stem":2514},"Інтеграція MVVM з Guice: Автоматична ін'єкція залежностей","\u002Fjava\u002Fpr2\u002Fmvvm-guice-integration","04.java\u002Fpr2\u002F30.mvvm-guice-integration",{"title":2516,"path":2517,"stem":2518},"Валідація та обробка помилок у MVVM","\u002Fjava\u002Fpr2\u002Fmvvm-validation-error-handling","04.java\u002Fpr2\u002F31.mvvm-validation-error-handling",{"title":2520,"path":2521,"stem":2522},"Навігація та управління екранами у JavaFX MVVM","\u002Fjava\u002Fpr2\u002Fmvvm-navigation-screen-management","04.java\u002Fpr2\u002F32.mvvm-navigation-screen-management",{"title":2524,"path":2525,"stem":2526},"Тестування JavaFX MVVM-додатків","\u002Fjava\u002Fpr2\u002Fmvvm-testing","04.java\u002Fpr2\u002F33.mvvm-testing",{"title":2528,"path":2529,"stem":2530},"Стилізація та теми у JavaFX: CSS та User Experience","\u002Fjava\u002Fpr2\u002Fjavafx-styling-themes","04.java\u002Fpr2\u002F34.javafx-styling-themes",{"title":2532,"path":2533,"stem":2534},"AtlantaFX: Сучасні теми для JavaFX додатків","\u002Fjava\u002Fpr2\u002Fatlantafx-modern-themes","04.java\u002Fpr2\u002F35.atlantafx-modern-themes",{"title":2536,"path":2537,"stem":2538},"Пакування та розповсюдження JavaFX-додатків","\u002Fjava\u002Fpr2\u002Fjar-packaging-distribution","04.java\u002Fpr2\u002F36.jar-packaging-distribution",{"title":2540,"icon":2541,"path":2542,"stem":2543,"children":2544,"page":59},"Python","i-devicon-python","\u002Fpython","05.python",[2545,2549,2552,2556,2560,2564,2568,2572,2576,2580,2584,2588,2592,2596,2600,2604],{"title":2546,"path":2547,"stem":2548},"Модулі, Пакети та Віртуальні Середовища","\u002Fpython\u002Fmodules-packages-venv","05.python\u002F00.modules-packages-venv",{"title":71,"path":2550,"stem":2551},"\u002Fpython\u002Fclasses-objects","05.python\u002F01.classes-objects",{"title":2553,"path":2554,"stem":2555},"Інкапсуляція, Керування Доступом та Властивості","\u002Fpython\u002Fencapsulation","05.python\u002F02.encapsulation",{"title":2557,"path":2558,"stem":2559},"Наслідування, MRO та суперсила super()","\u002Fpython\u002Finheritance-mro","05.python\u002F03.inheritance-mro",{"title":2561,"path":2562,"stem":2563},"Абстракція — ABC проти Статичних Протоколів (PEP 544)","\u002Fpython\u002Fabstraction-protocols","05.python\u002F04.abstraction-protocols",{"title":2565,"path":2566,"stem":2567},"Магічні методи (Dunder) та Емуляція протоколів","\u002Fpython\u002Fdunder-methods","05.python\u002F05.dunder-methods",{"title":2569,"path":2570,"stem":2571},"Декоратори та Керування життєвим циклом методів","\u002Fpython\u002Fdecorators-static-class","05.python\u002F06.decorators-static-class",{"title":2573,"path":2574,"stem":2575},"Дескриптори — Магія доступу до атрибутів","\u002Fpython\u002Fdescriptors","05.python\u002F07.descriptors",{"title":2577,"path":2578,"stem":2579},"Метакласи — Динамічне створення класів під капотом CPython","\u002Fpython\u002Fmetaclasses","05.python\u002F08.metaclasses",{"title":2581,"path":2582,"stem":2583},"Dataclasses, NamedTuple та сучасні контейнери Python","\u002Fpython\u002Fmodern-containers","05.python\u002F09.modern-containers",{"title":2585,"path":2586,"stem":2587},"GIL та модель конкурентності CPython — фундамент перед потоками і процесами","\u002Fpython\u002Fgil-concurrency-intro","05.python\u002F11.gil-concurrency-intro",{"title":2589,"path":2590,"stem":2591},"Threading — конкурентність для I\u002FO-bound задач","\u002Fpython\u002Fthreading","05.python\u002F12.threading",{"title":2593,"path":2594,"stem":2595},"Multiprocessing — справжній паралелізм для CPU-bound задач","\u002Fpython\u002Fmultiprocessing","05.python\u002F13.multiprocessing",{"title":2597,"path":2598,"stem":2599},"asyncio — кооперативна конкурентність та event loop","\u002Fpython\u002Fasyncio","05.python\u002F14.asyncio",{"title":2601,"path":2602,"stem":2603},"📦 Повний посібник з модулів, пакетів та віртуальних середовищ у Python","\u002Fpython\u002Flesson_9","05.python\u002Flesson_9",{"title":2605,"path":2606,"stem":2607},"[object Object]","\u002Fpython\u002Foop-plan","05.python\u002Foop-plan",{"title":2609,"icon":2610,"path":2611,"stem":2612,"children":2613,"page":59},"Бази даних","i-lucide-database","\u002Fdatabases","06.databases",[2614,2644,2667,2704,2733,2751,2785,2797,2806],{"title":2615,"icon":2616,"path":2617,"stem":2618,"children":2619,"page":59},"Intro","i-lucide-play","\u002Fdatabases\u002Fintro","06.databases\u002F01.intro",[2620,2624,2628,2632,2636,2640],{"title":2621,"path":2622,"stem":2623},"Введення в теорію баз даних","\u002Fdatabases\u002Fintro\u002Fintroduction-to-databases","06.databases\u002F01.intro\u002F01.introduction-to-databases",{"title":2625,"path":2626,"stem":2627},"Реляційна модель даних","\u002Fdatabases\u002Fintro\u002Frelational-model-theory","06.databases\u002F01.intro\u002F02.relational-model-theory",{"title":2629,"path":2630,"stem":2631},"ER-моделювання","\u002Fdatabases\u002Fintro\u002Fer-modeling","06.databases\u002F01.intro\u002F03.er-modeling",{"title":2633,"path":2634,"stem":2635},"Логічне проектування БД","\u002Fdatabases\u002Fintro\u002Flogical-schema","06.databases\u002F01.intro\u002F04.logical-schema",{"title":2637,"path":2638,"stem":2639},"Класифікація таблиць","\u002Fdatabases\u002Fintro\u002Ftable-classification","06.databases\u002F01.intro\u002F05.table-classification",{"title":2641,"path":2642,"stem":2643},"PlantUML для баз даних","\u002Fdatabases\u002Fintro\u002Fplantuml-diagrams","06.databases\u002F01.intro\u002F06.plantuml-diagrams",{"title":2645,"icon":2610,"path":2646,"stem":2647,"children":2648,"page":59},"MS SQL Server Start","\u002Fdatabases\u002Fms-sql-server-start","06.databases\u002F02.ms-sql-server-start",[2649,2653,2659,2663],{"title":2650,"path":2651,"stem":2652},"Типи даних у MS SQL Server","\u002Fdatabases\u002Fms-sql-server-start\u002Fdata-types","06.databases\u002F02.ms-sql-server-start\u002F01.data-types",{"title":2654,"path":2655,"stem":2656,"children":2657},"Індекси у MS SQL Server","\u002Fdatabases\u002Fms-sql-server-start\u002Fsql-indexes","06.databases\u002F02.ms-sql-server-start\u002F02.sql-indexes",[2658],{"title":2654,"path":2655,"stem":2656},{"title":2660,"path":2661,"stem":2662},"Системні бази даних MS SQL Server","\u002Fdatabases\u002Fms-sql-server-start\u002Fsystem-databases","06.databases\u002F02.ms-sql-server-start\u002F03.system-databases",{"title":2664,"path":2665,"stem":2666},"Огляд мови SQL та запитів","\u002Fdatabases\u002Fms-sql-server-start\u002Fsql-queries-overview","06.databases\u002F02.ms-sql-server-start\u002F04.sql-queries-overview",{"title":2668,"icon":2610,"path":2669,"stem":2670,"children":2671,"page":59},"SQL","\u002Fdatabases\u002Fsql","06.databases\u002F03.sql",[2672,2676,2680,2684,2688,2692,2696,2700],{"title":2673,"path":2674,"stem":2675},"Налаштування демонстраційної бази даних","\u002Fdatabases\u002Fsql\u002Fsample-database-setup","06.databases\u002F03.sql\u002F00.sample-database-setup",{"title":2677,"path":2678,"stem":2679},"DDL - Створення таблиць (CREATE TABLE)","\u002Fdatabases\u002Fsql\u002Fddl-create-table","06.databases\u002F03.sql\u002F01.ddl-create-table",{"title":2681,"path":2682,"stem":2683},"DDL - Зміна та видалення таблиць (ALTER, DROP)","\u002Fdatabases\u002Fsql\u002Fddl-alter-drop-table","06.databases\u002F03.sql\u002F02.ddl-alter-drop-table",{"title":2685,"path":2686,"stem":2687},"SELECT запити - Основи","\u002Fdatabases\u002Fsql\u002Fselect-queries-fundamentals","06.databases\u002F03.sql\u002F03.select-queries-fundamentals",{"title":2689,"path":2690,"stem":2691},"SELECT запити - Розширені можливості","\u002Fdatabases\u002Fsql\u002Fselect-queries-advanced","06.databases\u002F03.sql\u002F04.select-queries-advanced",{"title":2693,"path":2694,"stem":2695},"INSERT запити - Додавання даних","\u002Fdatabases\u002Fsql\u002Finsert-queries","06.databases\u002F03.sql\u002F05.insert-queries",{"title":2697,"path":2698,"stem":2699},"UPDATE та DELETE запити","\u002Fdatabases\u002Fsql\u002Fupdate-delete-queries","06.databases\u002F03.sql\u002F06.update-delete-queries",{"title":2701,"path":2702,"stem":2703},"Транзакції в SQL","\u002Fdatabases\u002Fsql\u002Ftransactions","06.databases\u002F03.sql\u002F07.transactions",{"title":2705,"icon":2610,"path":2706,"stem":2707,"children":2708,"page":59},"Multi Table Databases","\u002Fdatabases\u002Fmulti-table-databases","06.databases\u002F04.multi-table-databases",[2709,2713,2717,2721,2725,2729],{"title":2710,"path":2711,"stem":2712},"Зв'язки та нормалізація БД","\u002Fdatabases\u002Fmulti-table-databases\u002Frelationships-and-normalization","06.databases\u002F04.multi-table-databases\u002F00.relationships-and-normalization",{"title":2714,"path":2715,"stem":2716},"INNER JOIN - З'єднання таблиць","\u002Fdatabases\u002Fmulti-table-databases\u002Finner-join","06.databases\u002F04.multi-table-databases\u002F01.inner-join",{"title":2718,"path":2719,"stem":2720},"OUTER JOINs - LEFT, RIGHT, FULL","\u002Fdatabases\u002Fmulti-table-databases\u002Fouter-joins","06.databases\u002F04.multi-table-databases\u002F02.outer-joins",{"title":2722,"path":2723,"stem":2724},"CROSS та SELF JOINs","\u002Fdatabases\u002Fmulti-table-databases\u002Fcross-self-joins","06.databases\u002F04.multi-table-databases\u002F03.cross-self-joins",{"title":2726,"path":2727,"stem":2728},"Підзапити (Subqueries)","\u002Fdatabases\u002Fmulti-table-databases\u002Fsubqueries","06.databases\u002F04.multi-table-databases\u002F04.subqueries",{"title":2730,"path":2731,"stem":2732},"Агрегації з JOIN","\u002Fdatabases\u002Fmulti-table-databases\u002Faggregations-with-joins","06.databases\u002F04.multi-table-databases\u002F05.aggregations-with-joins",{"title":2734,"icon":2735,"path":2736,"stem":2737,"children":2738,"page":59},"Aggregate Functions","i-lucide-calculator","\u002Fdatabases\u002Faggregate-functions","06.databases\u002F05.aggregate-functions",[2739,2743,2747],{"title":2740,"path":2741,"stem":2742},"Функції агрегування в MS SQL Server","\u002Fdatabases\u002Faggregate-functions\u002Fintroduction-aggregate-functions","06.databases\u002F05.aggregate-functions\u002F01.introduction-aggregate-functions",{"title":2744,"path":2745,"stem":2746},"Групування даних в MS SQL Server","\u002Fdatabases\u002Faggregate-functions\u002Fgrouping-data","06.databases\u002F05.aggregate-functions\u002F02.grouping-data",{"title":2748,"path":2749,"stem":2750},"Підзапити з агрегатними функціями","\u002Fdatabases\u002Faggregate-functions\u002Fsubqueries-aggregates","06.databases\u002F05.aggregate-functions\u002F03.subqueries-aggregates",{"title":2752,"icon":2753,"path":2754,"stem":2755,"children":2756,"page":59},"Тригери та зберігаємі процедури","i-lucide-database-zap","\u002Fdatabases\u002Ftriggers-stored-procedures","06.databases\u002F07.triggers-stored-procedures",[2757,2761,2765,2769,2773,2777,2781],{"title":2758,"path":2759,"stem":2760},"DML-тригери","\u002Fdatabases\u002Ftriggers-stored-procedures\u002Fdml-triggers","06.databases\u002F07.triggers-stored-procedures\u002F01.dml-triggers",{"title":2762,"path":2763,"stem":2764},"DDL-тригери","\u002Fdatabases\u002Ftriggers-stored-procedures\u002Fddl-triggers","06.databases\u002F07.triggers-stored-procedures\u002F02.ddl-triggers",{"title":2766,"path":2767,"stem":2768},"Transact-SQL розширення","\u002Fdatabases\u002Ftriggers-stored-procedures\u002Ftransact-sql-extensions","06.databases\u002F07.triggers-stored-procedures\u002F03.transact-sql-extensions",{"title":2770,"path":2771,"stem":2772},"Транзакції","\u002Fdatabases\u002Ftriggers-stored-procedures\u002Ftransactions","06.databases\u002F07.triggers-stored-procedures\u002F04.transactions",{"title":2774,"path":2775,"stem":2776},"Зберігаємі процедури","\u002Fdatabases\u002Ftriggers-stored-procedures\u002Fstored-procedures","06.databases\u002F07.triggers-stored-procedures\u002F05.stored-procedures",{"title":2778,"path":2779,"stem":2780},"Користувацькі функції","\u002Fdatabases\u002Ftriggers-stored-procedures\u002Fuser-defined-functions","06.databases\u002F07.triggers-stored-procedures\u002F06.user-defined-functions",{"title":2782,"path":2783,"stem":2784},"Безпека баз даних","\u002Fdatabases\u002Ftriggers-stored-procedures\u002Fsecurity","06.databases\u002F07.triggers-stored-procedures\u002F08.security",{"title":2782,"icon":793,"path":2786,"stem":2787,"children":2788,"page":59},"\u002Fdatabases\u002Fsecurity","06.databases\u002F08.security",[2789,2793],{"title":2790,"path":2791,"stem":2792},"Вступ до безпеки баз даних","\u002Fdatabases\u002Fsecurity\u002Fintroduction","06.databases\u002F08.security\u002F01.introduction",{"title":2794,"path":2795,"stem":2796},"Системні представлення та метадані","\u002Fdatabases\u002Fsecurity\u002Fsystem-views","06.databases\u002F08.security\u002F02.system-views",{"title":2798,"icon":2799,"path":2800,"stem":2801,"children":2802,"page":59},"Резервне копіювання та відновлення","i-lucide-database-backup","\u002Fdatabases\u002Fbackup-recovery","06.databases\u002F09.backup-recovery",[2803],{"title":2798,"path":2804,"stem":2805},"\u002Fdatabases\u002Fbackup-recovery\u002Fbackup-restore","06.databases\u002F09.backup-recovery\u002F01.backup-restore",{"title":2807,"icon":2808,"path":2809,"stem":2810,"children":2811,"page":59},"Повнотекстовий пошук","i-lucide-search","\u002Fdatabases\u002Ffull-text-search","06.databases\u002F10.full-text-search",[2812],{"title":2807,"path":2813,"stem":2814},"\u002Fdatabases\u002Ffull-text-search\u002Ffull-text-search","06.databases\u002F10.full-text-search\u002F01.full-text-search",{"title":2816,"icon":2817,"path":2818,"stem":2819,"children":2820,"page":59},"Tools","i-lucide-wrench","\u002Ftools","07.tools",[2821,2897],{"title":2822,"icon":2823,"path":2824,"stem":2825,"children":2826},"Docker","i-simple-icons-docker","\u002Ftools\u002Fdocker","07.tools\u002F01.docker\u002Findex",[2827,2829,2833,2837,2841,2845,2849,2853,2857,2861,2865,2869,2873,2877,2881,2885,2889,2893],{"title":2828,"path":2824,"stem":2825},"Docker: від нуля до production",{"title":2830,"path":2831,"stem":2832},"Контейнеризація — від проблеми до рішення","\u002Ftools\u002Fdocker\u002Fcontainerization-concept","07.tools\u002F01.docker\u002F01.containerization-concept",{"title":2834,"path":2835,"stem":2836},"Docker — що це і навіщо?","\u002Ftools\u002Fdocker\u002Fdocker-what-and-why","07.tools\u002F01.docker\u002F02.docker-what-and-why",{"title":2838,"path":2839,"stem":2840},"Архітектура Docker Engine","\u002Ftools\u002Fdocker\u002Fdocker-architecture","07.tools\u002F01.docker\u002F03.docker-architecture",{"title":2842,"path":2843,"stem":2844},"Встановлення Docker","\u002Ftools\u002Fdocker\u002Finstallation","07.tools\u002F01.docker\u002F04.installation",{"title":2846,"path":2847,"stem":2848},"Перший контейнер — docker run","\u002Ftools\u002Fdocker\u002Ffirst-container","07.tools\u002F01.docker\u002F05.first-container",{"title":2850,"path":2851,"stem":2852},"Життєвий цикл контейнера","\u002Ftools\u002Fdocker\u002Fcontainer-lifecycle","07.tools\u002F01.docker\u002F06.container-lifecycle",{"title":2854,"path":2855,"stem":2856},"Docker Images — фундаментальні концепції","\u002Ftools\u002Fdocker\u002Fdocker-images-fundamentals","07.tools\u002F01.docker\u002F07.docker-images-fundamentals",{"title":2858,"path":2859,"stem":2860},"Dockerfile — основи","\u002Ftools\u002Fdocker\u002Fdockerfile-basics","07.tools\u002F01.docker\u002F08.dockerfile-basics",{"title":2862,"path":2863,"stem":2864},"Dockerfile — просунуті техніки","\u002Ftools\u002Fdocker\u002Fdockerfile-advanced","07.tools\u002F01.docker\u002F09.dockerfile-advanced",{"title":2866,"path":2867,"stem":2868},"Build Context та кешування шарів","\u002Ftools\u002Fdocker\u002Fbuild-context-and-cache","07.tools\u002F01.docker\u002F10.build-context-and-cache",{"title":2870,"path":2871,"stem":2872},"Реєстри Docker-образів","\u002Ftools\u002Fdocker\u002Fimage-registries","07.tools\u002F01.docker\u002F11.image-registries",{"title":2874,"path":2875,"stem":2876},"Контейнеризація .NET додатків","\u002Ftools\u002Fdocker\u002Fdotnet-containerization","07.tools\u002F01.docker\u002F12.dotnet-containerization",{"title":2878,"path":2879,"stem":2880},"Томи та збереження даних","\u002Ftools\u002Fdocker\u002Fvolumes-and-data","07.tools\u002F01.docker\u002F13.volumes-and-data",{"title":2882,"path":2883,"stem":2884},"Основи мережі в Docker","\u002Ftools\u002Fdocker\u002Fnetworking-basics","07.tools\u002F01.docker\u002F14.networking-basics",{"title":2886,"path":2887,"stem":2888},"Змінні оточення та конфігурація","\u002Ftools\u002Fdocker\u002Fenvironment-and-configuration","07.tools\u002F01.docker\u002F15.environment-and-configuration",{"title":2890,"path":2891,"stem":2892},"Docker Compose — оркестрація контейнерів","\u002Ftools\u002Fdocker\u002Fdocker-compose-basics","07.tools\u002F01.docker\u002F16.docker-compose-basics",{"title":2894,"path":2895,"stem":2896},"Docker Compose — Multi-Service застосунки","\u002Ftools\u002Fdocker\u002Fcompose-multi-service","07.tools\u002F01.docker\u002F17.compose-multi-service",{"title":2898,"icon":2899,"path":2900,"stem":2901,"children":2902},"Kubernetes","simple-icons:kubernetes","\u002Ftools\u002Fkubernetes","07.tools\u002F02.kubernetes\u002Findex",[2903,2905,2909,2913,2917,2921,2925,2929,2933],{"title":2904,"path":2900,"stem":2901},"Kubernetes: від розробки до production",{"title":2906,"path":2907,"stem":2908},"Kubernetes — коли Docker Compose більше не вистачає","\u002Ftools\u002Fkubernetes\u002Fwhy-kubernetes","07.tools\u002F02.kubernetes\u002F01.why-kubernetes",{"title":2910,"path":2911,"stem":2912},"Архітектура Kubernetes — анатомія кластера","\u002Ftools\u002Fkubernetes\u002Fkubernetes-architecture","07.tools\u002F02.kubernetes\u002F02.kubernetes-architecture",{"title":2914,"path":2915,"stem":2916},"Локальне середовище — minikube, kind та k3s","\u002Ftools\u002Fkubernetes\u002Flocal-environment","07.tools\u002F02.kubernetes\u002F03.local-environment",{"title":2918,"path":2919,"stem":2920},"Pod — атомарна одиниця Kubernetes","\u002Ftools\u002Fkubernetes\u002Fpods-and-containers","07.tools\u002F02.kubernetes\u002F04.pods-and-containers",{"title":2922,"path":2923,"stem":2924},"Патерни використання Pod","\u002Ftools\u002Fkubernetes\u002Fpod-patterns","07.tools\u002F02.kubernetes\u002F05.pod-patterns",{"title":2926,"path":2927,"stem":2928},"Deployment — декларативне управління Pod","\u002Ftools\u002Fkubernetes\u002Fdeployment-basics","07.tools\u002F02.kubernetes\u002F06.deployment-basics",{"title":2930,"path":2931,"stem":2932},"Rolling Updates та управління життєвим циклом Deployment","\u002Ftools\u002Fkubernetes\u002Fdeployment-rolling-updates","07.tools\u002F02.kubernetes\u002F07.deployment-rolling-updates",{"title":2934,"path":2935,"stem":2936},"Service — мережева абстракція для Pod","\u002Ftools\u002Fkubernetes\u002Fservices-networking","07.tools\u002F02.kubernetes\u002F08.services-networking",{"title":2938,"icon":2939,"path":2940,"stem":2941,"children":2942,"page":59},"Software Engineering","i-lucide-code-2","\u002Fsoftware-engineering","09.software-engineering",[2943,2947,2951,2955,2959,2963,2967,2971,2975,2979,2983],{"title":2944,"path":2945,"stem":2946},"1. Аналіз предметної області. Експертні знання та складність","\u002Fsoftware-engineering\u002Fintro-subdomains","09.software-engineering\u002F01.intro-subdomains",{"title":2948,"path":2949,"stem":2950},"2. Обмежені контексти. Інтеграція обмежених контекстів","\u002Fsoftware-engineering\u002Fintegrating-limited-contexts","09.software-engineering\u002F02.integrating-limited-contexts",{"title":2952,"path":2953,"stem":2954},"3. Реалізація простої бізнес-логіки","\u002Fsoftware-engineering\u002Fsimple","09.software-engineering\u002F03.simple",{"title":2956,"path":2957,"stem":2958},"4. Опрацювання складної бізнес-логіки","\u002Fsoftware-engineering\u002Fcomplex-business-logic","09.software-engineering\u002F04.complex-business-logic",{"title":2960,"path":2961,"stem":2962},"5. Моделювання фактора часу. Подієво-орієнтована архітектура.","\u002Fsoftware-engineering\u002Fmodelling-the-time-factor","09.software-engineering\u002F05.modelling-the-time-factor",{"title":2964,"path":2965,"stem":2966},"6. Архітектурні патерни","\u002Fsoftware-engineering\u002Farchitectural-patterns","09.software-engineering\u002F06.architectural-patterns",{"title":2968,"path":2969,"stem":2970},"Паттерни взаємодії","\u002Fsoftware-engineering\u002Fpatterns-of-interaction","09.software-engineering\u002F07.patterns-of-interaction",{"title":2972,"path":2973,"stem":2974},"Евристика проєктування","\u002Fsoftware-engineering\u002Fdesign-heuristics","09.software-engineering\u002F08.design-heuristics",{"title":2976,"path":2977,"stem":2978},"Еволюція проєктних рішень","\u002Fsoftware-engineering\u002Fevolution-of-design-solutions","09.software-engineering\u002F09.evolution-of-design-solutions",{"title":2980,"path":2981,"stem":2982},"EventStorming","\u002Fsoftware-engineering\u002Feventstorming","09.software-engineering\u002F10.eventstorming",{"title":2984,"path":2985,"stem":2986},"DDD на практиці","\u002Fsoftware-engineering\u002Fddd-in-practice","09.software-engineering\u002F11.ddd-in-practice",{"title":2988,"icon":943,"path":2989,"stem":2990,"children":2991,"page":59},"DDD","\u002Fddd","10.ddd",[2992,2996,3000,3004,3008,3012,3016,3020,3024,3028,3032,3036,3040],{"title":2993,"path":2994,"stem":2995},"Аналіз предметної області","\u002Fddd\u002Fdomain-analysis","10.ddd\u002F01.domain-analysis",{"title":2997,"path":2998,"stem":2999},"Експертні знання про предметну область","\u002Fddd\u002Fdomain-expert-knowledge","10.ddd\u002F02.domain-expert-knowledge",{"title":3001,"path":3002,"stem":3003},"Як осмислити складність предметної області","\u002Fddd\u002Fmanaging-domain-complexity","10.ddd\u002F03.managing-domain-complexity",{"title":3005,"path":3006,"stem":3007},"Інтеграція обмежених контекстів","\u002Fddd\u002Fbounded-context-integration","10.ddd\u002F04.bounded-context-integration",{"title":3009,"path":3010,"stem":3011},"Реалізація простої бізнес-логіки","\u002Fddd\u002Fsimple-business-logic","10.ddd\u002F05.simple-business-logic",{"title":3013,"path":3014,"stem":3015},"Обробка складної бізнес-логіки","\u002Fddd\u002Fcomplex-business-logic","10.ddd\u002F06.complex-business-logic",{"title":3017,"path":3018,"stem":3019},"Моделювання фактора часу","\u002Fddd\u002Ftime-modeling","10.ddd\u002F07.time-modeling",{"title":3021,"path":3022,"stem":3023},"Глава 8. Архітектурні Патерни","\u002Fddd\u002Farchitectural-patterns","10.ddd\u002F08.architectural-patterns",{"title":3025,"path":3026,"stem":3027},"Глава 9. Патерни Взаємодії","\u002Fddd\u002Finteraction-patterns","10.ddd\u002F09.interaction-patterns",{"title":3029,"path":3030,"stem":3031},"Глава 10. Проектні Евристики","\u002Fddd\u002Fdesign-heuristics","10.ddd\u002F10.design-heuristics",{"title":3033,"path":3034,"stem":3035},"Глава 11. Еволюція Проектних Рішень","\u002Fddd\u002Fevolution-of-design-decisions","10.ddd\u002F11.evolution-of-design-decisions",{"title":3037,"path":3038,"stem":3039},"Глава 12. EventStorming","\u002Fddd\u002Fevent-storming","10.ddd\u002F12.event-storming",{"title":3041,"path":3042,"stem":3043},"Глава 13. DDD на Практиці","\u002Fddd\u002Fddd-in-practice","10.ddd\u002F13.ddd-in-practice",{"title":3045,"icon":3046,"path":3047,"stem":3048,"children":3049,"page":59},"Media Streaming","i-lucide-video","\u002Fmedia-streaming","11.media-streaming",[3050,3054,3058,3062,3066,3070,3074],{"title":3051,"path":3052,"stem":3053},"01. Магія Стрімінгу: Що відбувається, коли ви натискаєте \"Play\"","\u002Fmedia-streaming\u002Fintroduction","11.media-streaming\u002F01.introduction",{"title":3055,"path":3056,"stem":3057},"02. Анатомія Медіа: Кодеки, Контейнери та Стиснення","\u002Fmedia-streaming\u002Faudio-video-anatomy","11.media-streaming\u002F02.audio-video-anatomy",{"title":3059,"path":3060,"stem":3061},"03. The Gym: FFmpeg Deep Dive","\u002Fmedia-streaming\u002Fffmpeg-gym","11.media-streaming\u002F03.ffmpeg-gym",{"title":3063,"path":3064,"stem":3065},"04. HLS Protocol: HTTP Live Streaming у Деталях","\u002Fmedia-streaming\u002Fhls-protocol","11.media-streaming\u002F04.hls-protocol",{"title":3067,"path":3068,"stem":3069},"05. DASH Protocol: Відкритий Стандарт","\u002Fmedia-streaming\u002Fdash-protocol","11.media-streaming\u002F05.dash-protocol",{"title":3071,"path":3072,"stem":3073},"06. Масштабування: CDN та Adaptive Bitrate","\u002Fmedia-streaming\u002Fcdn-and-adaptive-bitrate","11.media-streaming\u002F06.cdn-and-adaptive-bitrate",{"title":3075,"path":3076,"stem":3077},"07. Війна із Затримкою (Latency)","\u002Fmedia-streaming\u002Frealtime-latency","11.media-streaming\u002F07.realtime-latency",{"title":3079,"icon":3080,"path":3081,"stem":3082,"children":3083,"page":59},"HTML & CSS","i-devicon-html5","\u002Fhtml-css","12.html-css",[3084,3088,3092,3096,3100,3104,3108,3112,3116,3120,3124,3128,3132,3136,3140,3144,3148,3152,3156,3160,3164,3168,3172,3176,3180,3184,3188,3192,3196,3200],{"title":3085,"path":3086,"stem":3087},"Вступ до HTML. Структура документа","\u002Fhtml-css\u002Fintro-html-structure","12.html-css\u002F01.intro-html-structure",{"title":3089,"path":3090,"stem":3091},"Форматування тексту в HTML","\u002Fhtml-css\u002Fhtml-text-formatting","12.html-css\u002F02.html-text-formatting",{"title":3093,"path":3094,"stem":3095},"Посилання та зображення в HTML","\u002Fhtml-css\u002Fhtml-links-images","12.html-css\u002F03.html-links-images",{"title":3097,"path":3098,"stem":3099},"Списки та таблиці в HTML","\u002Fhtml-css\u002Fhtml-lists-tables","12.html-css\u002F04.html-lists-tables",{"title":3101,"path":3102,"stem":3103},"Форми в HTML","\u002Fhtml-css\u002Fhtml-forms","12.html-css\u002F05.html-forms",{"title":3105,"path":3106,"stem":3107},"Семантичні елементи HTML5","\u002Fhtml-css\u002Fhtml-semantic-elements","12.html-css\u002F06.html-semantic-elements",{"title":3109,"path":3110,"stem":3111},"Мультимедіа та розширені елементи HTML","\u002Fhtml-css\u002Fhtml-multimedia-advanced","12.html-css\u002F07.html-multimedia-advanced",{"title":3113,"path":3114,"stem":3115},"Мікророзмітка та SEO в HTML","\u002Fhtml-css\u002Fhtml-microdata-seo","12.html-css\u002F08.html-microdata-seo",{"title":3117,"path":3118,"stem":3119},"Вступ до CSS. Селектори та специфічність","\u002Fhtml-css\u002Fcss-intro-selectors","12.html-css\u002F09.css-intro-selectors",{"title":3121,"path":3122,"stem":3123},"Блокова модель CSS. Відступи. Box Sizing","\u002Fhtml-css\u002Fcss-box-model","12.html-css\u002F10.css-box-model",{"title":3125,"path":3126,"stem":3127},"Розміри у CSS: повний довідник одиниць і ключових слів","\u002Fhtml-css\u002F10a.css-sizing","12.html-css\u002F10a.css-sizing",{"title":3129,"path":3130,"stem":3131},"Типографіка в CSS. Шрифти та текст","\u002Fhtml-css\u002Fcss-typography","12.html-css\u002F11.css-typography",{"title":3133,"path":3134,"stem":3135},"Кольори та фони в CSS","\u002Fhtml-css\u002Fcss-colors-backgrounds","12.html-css\u002F12.css-colors-backgrounds",{"title":3137,"path":3138,"stem":3139},"Тіні та фільтри в CSS","\u002Fhtml-css\u002F12b.css-shadows-filters","12.html-css\u002F12b.css-shadows-filters",{"title":3141,"path":3142,"stem":3143},"CSS Flexbox: Фундамент гнучких макетів","\u002Fhtml-css\u002Fcss-flexbox-fundamentals","12.html-css\u002F13.css-flexbox-fundamentals",{"title":3145,"path":3146,"stem":3147},"CSS Flexbox: Вирівнювання та Позиціонування","\u002Fhtml-css\u002Fcss-flexbox-alignment-sizing-and-patterns","12.html-css\u002F14.css-flexbox-alignment-sizing-and-patterns",{"title":3149,"path":3150,"stem":3151},"CSS Grid. Двовимірний макет. Частина 1","\u002Fhtml-css\u002Fcss-layout-grid","12.html-css\u002F15.css-layout-grid",{"title":3153,"path":3154,"stem":3155},"CSS Grid. Двовимірний макет. Частина 2","\u002Fhtml-css\u002Fcss-layout-grid-advanced","12.html-css\u002F16.css-layout-grid-advanced",{"title":3157,"path":3158,"stem":3159},"Позиціонування в CSS. Z-index. Stacking Context","\u002Fhtml-css\u002Fcss-positioning","12.html-css\u002F17.css-positioning",{"title":3161,"path":3162,"stem":3163},"CSS Анімації та Переходи","\u002Fhtml-css\u002Fcss-animations-transitions","12.html-css\u002F18.css-animations-transitions",{"title":3165,"path":3166,"stem":3167},"Адаптивний дизайн. Media Queries. Частина 1","\u002Fhtml-css\u002Fcss-responsive-media-queries","12.html-css\u002F19.css-responsive-media-queries",{"title":3169,"path":3170,"stem":3171},"Адаптивний дизайн. Частина 2: clamp(), Container Queries, @layer","\u002Fhtml-css\u002Fcss-responsive-advanced","12.html-css\u002F20.css-responsive-advanced",{"title":3173,"path":3174,"stem":3175},"CSS Custom Properties. Методології. Сучасний CSS","\u002Fhtml-css\u002Fcss-variables-methodologies","12.html-css\u002F21.css-variables-methodologies",{"title":3177,"path":3178,"stem":3179},"Сучасний CSS 2023–2025: Нові можливості","\u002Fhtml-css\u002Fcss-modern-features","12.html-css\u002F22.css-modern-features",{"title":3181,"path":3182,"stem":3183},"CSS Nesting, @layer, @scope та @property: нативний препроцесор","\u002Fhtml-css\u002F22a.css-nesting-modern-syntax","12.html-css\u002F22a.css-nesting-modern-syntax",{"title":3185,"path":3186,"stem":3187},"CSS для форм та інтерактивних станів","\u002Fhtml-css\u002Fcss-forms-interactive-states","12.html-css\u002F23.css-forms-interactive-states",{"title":3189,"path":3190,"stem":3191},"Доступність у CSS (CSS Accessibility)","\u002Fhtml-css\u002Fcss-accessibility","12.html-css\u002F24.css-accessibility",{"title":3193,"path":3194,"stem":3195},"CSS-функції та сучасні sizing primitives","\u002Fhtml-css\u002Fcss-functions-sizing","12.html-css\u002F25.css-functions-sizing",{"title":3197,"path":3198,"stem":3199},"Rendering Pipeline і CSS Performance","\u002Fhtml-css\u002Fcss-rendering-performance","12.html-css\u002F26.css-rendering-performance",{"title":3201,"path":3202,"stem":3203},"CSS Best Practices: типові ситуації та правильні рішення","\u002Fhtml-css\u002Fcss-best-practices","12.html-css\u002F27.css-best-practices",{"title":3205,"path":3206,"stem":3207,"children":3208,"page":59},"AWS","\u002Faws","13.aws",[3209,3213,3217,3221,3225,3229,3233,3237,3241,3245,3249,3253,3257,3261,3265,3269,3273,3277],{"title":3210,"path":3211,"stem":3212},"Реєстрація AWS акаунту та студентські програми","\u002Faws\u002Faccount-registration","13.aws\u002F00.account-registration",{"title":3214,"path":3215,"stem":3216},"Вступ до хмарних обчислень та AWS","\u002Faws\u002Fintroduction-to-cloud","13.aws\u002F01.introduction-to-cloud",{"title":3218,"path":3219,"stem":3220},"AWS IAM — Identity and Access Management","\u002Faws\u002Fiam","13.aws\u002F02.iam",{"title":3222,"path":3223,"stem":3224},"AWS IAM CLI — Довідник команд","\u002Faws\u002F02a.iam-doc","13.aws\u002F02a.iam-doc",{"title":3226,"path":3227,"stem":3228},"Docker та контейнеризація в AWS — ECR, ECS та Fargate","\u002Faws\u002Fdocker-ecs","13.aws\u002F03.docker-ecs",{"title":3230,"path":3231,"stem":3232},"AWS ECR \u002F ECS CLI — Довідник команд","\u002Faws\u002F03a.docker-ecs-doc","13.aws\u002F03a.docker-ecs-doc",{"title":3234,"path":3235,"stem":3236},"Amazon EC2 — Elastic Compute Cloud","\u002Faws\u002Fec2","13.aws\u002F04.ec2",{"title":3238,"path":3239,"stem":3240},"AWS EC2 CLI — Довідник команд","\u002Faws\u002F04a.ec2-doc","13.aws\u002F04a.ec2-doc",{"title":3242,"path":3243,"stem":3244},"Elastic Load Balancing та Auto Scaling","\u002Faws\u002Falb-asg","13.aws\u002F05.alb-asg",{"title":3246,"path":3247,"stem":3248},"Amazon S3 — Simple Storage Service","\u002Faws\u002Fs3","13.aws\u002F06.s3",{"title":3250,"path":3251,"stem":3252},"Amazon CloudFront — Content Delivery Network","\u002Faws\u002Fcloudfront","13.aws\u002F07.cloudfront",{"title":3254,"path":3255,"stem":3256},"Amazon RDS — Relational Database Service","\u002Faws\u002Frds","13.aws\u002F08.rds",{"title":3258,"path":3259,"stem":3260},"Amazon DynamoDB — NoSQL Database","\u002Faws\u002Fdynamodb","13.aws\u002F09.dynamodb",{"title":3262,"path":3263,"stem":3264},"AWS Lambda та Serverless Compute","\u002Faws\u002Flambda","13.aws\u002F10.lambda",{"title":3266,"path":3267,"stem":3268},"Amazon Bedrock - Foundation Models, RAG та Agents","\u002Faws\u002Fbedrock","13.aws\u002F22.bedrock",{"title":3270,"path":3271,"stem":3272},"Amazon Rekognition - Комп'ютерний зір","\u002Faws\u002Frekognition","13.aws\u002F23.rekognition",{"title":3274,"path":3275,"stem":3276},"Amazon Textract - Інтелектуальний аналіз документів","\u002Faws\u002Ftextract","13.aws\u002F24.textract",{"title":3278,"path":3279,"stem":3280},"Amazon Polly, Transcribe, Comprehend та Translate","\u002Faws\u002Faudio-nlp-services","13.aws\u002F25.audio-nlp-services",{"title":3282,"path":3283,"stem":3284,"children":3285,"page":59},"Tailwind","\u002Ftailwind","21.tailwind",[3286,3290,3294,3298,3302,3306,3310,3314,3318,3322,3326,3330],{"title":3287,"path":3288,"stem":3289},"Що таке Tailwind CSS і навіщо він потрібен","\u002Ftailwind\u002Ftailwind-intro-philosophy","21.tailwind\u002F01.tailwind-intro-philosophy",{"title":3291,"path":3292,"stem":3293},"Встановлення та налаштування Tailwind CSS v4","\u002Ftailwind\u002Ftailwind-installation-setup","21.tailwind\u002F02.tailwind-installation-setup",{"title":3295,"path":3296,"stem":3297},"Utility-класи: основи та система Tailwind","\u002Ftailwind\u002Ftailwind-utility-classes-core","21.tailwind\u002F03.tailwind-utility-classes-core",{"title":3299,"path":3300,"stem":3301},"Layout: Flexbox та Grid через Tailwind","\u002Ftailwind\u002Ftailwind-flexbox-grid","21.tailwind\u002F04.tailwind-flexbox-grid",{"title":3303,"path":3304,"stem":3305},"Кастомізація теми через @theme у Tailwind v4","\u002Ftailwind\u002Ftailwind-theme-customization","21.tailwind\u002F05.tailwind-theme-customization",{"title":3307,"path":3308,"stem":3309},"Варіанти: hover, focus, responsive, dark mode та нові v4","\u002Ftailwind\u002Ftailwind-variants-states","21.tailwind\u002F06.tailwind-variants-states",{"title":3311,"path":3312,"stem":3313},"Типографіка та система кольорів у Tailwind v4","\u002Ftailwind\u002Ftailwind-typography-colors","21.tailwind\u002F07.tailwind-typography-colors",{"title":3315,"path":3316,"stem":3317},"Компоненти та повторюваність: @apply, @utility та патерни","\u002Ftailwind\u002Ftailwind-components-patterns","21.tailwind\u002F08.tailwind-components-patterns",{"title":3319,"path":3320,"stem":3321},"Темна тема та система дизайн-токенів у Tailwind v4","\u002Ftailwind\u002Ftailwind-dark-mode-theming","21.tailwind\u002F09.tailwind-dark-mode-theming",{"title":3323,"path":3324,"stem":3325},"Довільні значення та контейнерні запити у Tailwind v4","\u002Ftailwind\u002Ftailwind-arbitrary-container-queries","21.tailwind\u002F10.tailwind-arbitrary-container-queries",{"title":3327,"path":3328,"stem":3329},"Анімації, трансформації та 3D у Tailwind v4","\u002Ftailwind\u002Ftailwind-animations-transforms","21.tailwind\u002F11.tailwind-animations-transforms",{"title":3331,"path":3332,"stem":3333},"Tailwind CLI, PostCSS та інтеграція з фреймворками","\u002Ftailwind\u002Ftailwind-cli-tooling","21.tailwind\u002F12.tailwind-cli-tooling",{"title":3335,"path":3336,"stem":3337},"Тестування компонентів діаграм","\u002Ftest-components","98.test-components",{"id":3339,"title":3246,"body":3340,"description":32270,"extension":32271,"links":32272,"meta":32273,"navigation":3627,"path":3247,"seo":32274,"stem":3248,"__hash__":32275},"docs\u002F13.aws\u002F06.s3.md",{"type":3341,"value":3342,"toc":32173},"minimark",[3343,3347,3352,3360,3363,3368,3396,3404,3407,3411,3416,3422,3461,3485,3489,3495,3550,3554,3560,3570,3581,3586,3592,3712,3714,3718,3724,3728,3750,3753,3758,3784,3789,3792,3824,3828,3839,3842,3847,3873,3878,3891,3981,3985,3990,3995,4025,4030,4033,4093,4097,4102,4105,4131,4134,4139,4159,4164,4167,4227,4231,4237,4247,4257,4262,4323,4328,4348,4353,4367,4407,4411,4575,4578,4761,4793,4797,4800,4906,5021,5109,5192,5196,6060,6075,6077,6081,6095,6109,6114,6139,6148,6155,6162,6167,6193,6371,7020,7156,7167,7169,7173,7178,7193,7198,7224,7388,7393,7586,7591,7686,8082,8207,8209,8213,8220,8227,8535,8539,8552,8555,8576,8865,8936,8943,8945,8949,8959,8972,8977,9079,9084,9196,9201,9333,9338,9461,9466,9576,9933,10037,10039,10043,10046,10214,10308,10318,10328,10334,10340,10345,10355,10358,10387,10390,10395,10478,10514,10780,10907,10912,11425,11462,11895,11984,11986,11990,11996,12001,12008,12013,12018,12021,12026,12032,12037,12040,12227,12411,12536,12949,12954,13057,13059,13063,13068,13073,13092,13097,13113,13135,13138,13159,13168,13250,13257,13270,13360,13367,13441,13455,13457,13461,13467,13472,13475,13534,13539,13559,13675,13786,13791,13804,13914,13960,13965,14019,14021,14025,14034,14039,14042,14047,14050,14062,14145,14150,14212,14223,14341,14344,14420,14422,14426,14429,14433,14453,14467,14473,14477,14483,14490,14668,14672,14675,14681,14687,14729,14742,14746,14753,14764,14769,14773,14776,14782,14788,14963,14967,15433,15437,15440,15509,15511,15515,15519,15522,15550,15585,15591,15643,15663,15722,15726,15736,15762,15775,17197,17202,17212,17849,17851,17855,17858,17860,17864,17935,17981,17984,17990,17999,18300,18315,18481,18492,18769,18777,20030,20037,20344,20351,20715,20717,20721,20746,20795,20798,20828,20836,20838,20842,21034,21036,21040,21167,21169,21173,21290,21292,21296,21420,21422,21426,21433,21436,21475,21526,21639,21670,21676,21678,21682,21687,21693,21696,21742,21765,21768,21812,21846,21848,21852,21855,21978,21981,21983,21987,21990,22130,22132,22136,22147,22153,22160,22162,22166,22173,22296,22302,22413,22422,22424,22428,22523,22526,22532,22534,22538,22545,22760,22767,22899,22901,22905,22913,23449,23451,23455,23463,25509,25511,25515,25521,25623,25632,25661,25666,25671,25677,25682,25688,25698,25701,25954,25959,25970,26085,26094,26101,26200,26203,26210,26242,26244,26248,26265,26270,26335,26368,26391,26420,26496,26523,26585,26602,26625,26660,26692,26694,26698,26774,26776,26780,26783,26803,26874,26876,26880,27049,27052,27054,27058,27210,27220,27222,27226,27403,27406,27906,28199,28201,28203,28207,28214,28229,28247,28250,28270,28272,28276,28282,28289,28416,28434,28441,28850,28855,29214,29296,29298,29302,29309,29868,29875,29877,29881,30233,30316,30318,30322,30325,30670,30672,30676,30679,30684,30722,30727,30850,30853,30855,30859,30933,30935,30939,31030,31032,31036,31040,31046,31055,31059,31074,31092,31096,31102,31104,31108,31113,31125,31127,31131,31134,31139,31153,31191,31196,31202,31279,31284,31290,31297,31302,31305,31391,31396,31403,31409,31412,31496,31501,31560,31571,31576,31604,31606,31610,31613,31619,31685,31692,31694,31698,31703,31709,31756,31763,31765,31769,31774,31780,31783,31843,31846,31848,31852,31857,31863,31902,31908,31915,31917,31921,31926,31932,31989,31991,31995,32159,32169],[3344,3345,3246],"h1",{"id":3346},"amazon-s3-simple-storage-service",[3348,3349,3351],"h2",{"id":3350},"що-таке-s3-і-чому-він-один-із-найважливіших-сервісів-aws","Що таке S3 і чому він один із найважливіших сервісів AWS",[3353,3354,3355,3359],"p",{},[3356,3357,3358],"strong",{},"Amazon S3 (Simple Storage Service)"," — це об'єктне сховище з необмеженою місткістю, доступністю 99.999999999% (дев'ять дев'яток!), і ціною від $0.023 за гігабайт на місяць. За цими сухими цифрами стоїть один із найвпливовіших хмарних сервісів в історії IT.",[3353,3361,3362],{},"S3 запустили у 2006 році — і він докорінно змінив підхід до зберігання даних. До S3 компанії купували NAS (Network Attached Storage) або будували власні файлові сервери: дорого, складно масштабувати, єдина точка відмови. S3 запропонував: завантажуй скільки хочеш файлів — від одного байта до терабайтів — і плати лише за те, що використовуєш.",[3353,3364,3365],{},[3356,3366,3367],{},"Сьогодні S3 використовується для:",[3369,3370,3371,3375,3378,3381,3384,3387,3390,3393],"ul",{},[3372,3373,3374],"li",{},"Зберігання зображень, відео, PDF та будь-яких файлів для веб-застосунків",[3372,3376,3377],{},"Хостингу статичних веб-сайтів та React\u002FVue\u002FAngular SPA",[3372,3379,3380],{},"Сховища резервних копій баз даних та серверів",[3372,3382,3383],{},"Дистрибуції медіаконтенту (відео HLS\u002FDASH потоки)",[3372,3385,3386],{},"Data Lake для аналітики (Athena, EMR, SageMaker читають з S3)",[3372,3388,3389],{},"Логів CloudTrail, ALB, CloudFront",[3372,3391,3392],{},"Артефактів CI\u002FCD (збірки, Docker образи через ECR який теж базується на S3)",[3372,3394,3395],{},"Static website hosting для фронтенд SPA",[3397,3398,3399,3400,3403],"note",{},"S3 — це не файлова система. Тут немає справжніх директорій, немає POSIX операцій, немає блокувань файлів. Це ",[3356,3401,3402],{},"об'єктне сховище",": ви зберігаєте об'єкти (файли + метадані) і отримуєте їх за унікальним ключем. Це принципово важливо для розуміння.",[3405,3406],"hr",{},[3348,3408,3410],{"id":3409},"bucket-object-key-фундаментальні-концепції","Bucket, Object, Key — фундаментальні концепції",[3412,3413,3415],"h3",{"id":3414},"bucket-контейнер-для-обєктів","Bucket — контейнер для об'єктів",[3353,3417,3418,3421],{},[3356,3419,3420],{},"Bucket"," — це верхньорівневий контейнер для зберігання об'єктів в S3. Думайте про bucket як про «диск» або «кореневу директорію». Кожен bucket:",[3369,3423,3424,3436,3443,3454],{},[3372,3425,3426,3427,3430,3431,3435],{},"Має ",[3356,3428,3429],{},"унікальну глобальну назву"," — унікальну серед всіх AWS клієнтів у всьому світі. Якщо назва ",[3432,3433,3434],"code",{},"photos"," вже зайнята кимось — ви не зможете створити bucket з такою ж назвою.",[3372,3437,3438,3439,3442],{},"Прив'язаний до ",[3356,3440,3441],{},"конкретного регіону"," (eu-central-1, us-east-1 тощо). Дані фізично зберігаються у цьому регіоні.",[3372,3444,3445,3446,3449,3450,3453],{},"Може містити ",[3356,3447,3448],{},"необмежену кількість об'єктів",", але один об'єкт не може перевищувати ",[3356,3451,3452],{},"5 TB",".",[3372,3455,3456,3457,3460],{},"Назва bucket може містити лише малі літери, цифри та дефіси (",[3432,3458,3459],{},"my-bucket-2024","), довжина 3–63 символи.",[3353,3462,3463,3466,3467,3470,3471,3470,3474,3477,3478,3480,3481,3484],{},[3356,3464,3465],{},"Правила іменування bucket:"," lowercase, лише ",[3432,3468,3469],{},"a-z",", ",[3432,3472,3473],{},"0-9",[3432,3475,3476],{},"-",". Не може починатись або закінчуватись на ",[3432,3479,3476],{},". Не може містити ",[3432,3482,3483],{},"_"," або великі літери. Не може виглядати як IP-адреса.",[3412,3486,3488],{"id":3487},"object-файл-з-метаданими","Object — файл з метаданими",[3353,3490,3491,3494],{},[3356,3492,3493],{},"Object"," (об'єкт) — це файл плюс метадані про цей файл. Кожен об'єкт складається з:",[3369,3496,3497,3503,3509,3538,3544],{},[3372,3498,3499,3502],{},[3356,3500,3501],{},"Data (Body):"," власне вміст файлу — від 0 байтів до 5 TB",[3372,3504,3505,3508],{},[3356,3506,3507],{},"Key:"," унікальний ідентифікатор об'єкту у bucket (докладніше нижче)",[3372,3510,3511,3514,3515],{},[3356,3512,3513],{},"Metadata:"," набір пар ключ-значення з інформацією про об'єкт\n",[3369,3516,3517,3532],{},[3372,3518,3519,3520,3470,3523,3470,3526,3470,3529],{},"System metadata: ",[3432,3521,3522],{},"Content-Type",[3432,3524,3525],{},"Content-Length",[3432,3527,3528],{},"Last-Modified",[3432,3530,3531],{},"ETag",[3372,3533,3534,3535],{},"User metadata: будь-які дані, наприклад ",[3432,3536,3537],{},"x-amz-meta-author: Ivan",[3372,3539,3540,3543],{},[3356,3541,3542],{},"Version ID:"," якщо включене версіонування — унікальний ID версії",[3372,3545,3546,3549],{},[3356,3547,3548],{},"Storage Class:"," клас зберігання (Standard, IA, Glacier тощо)",[3412,3551,3553],{"id":3552},"key-адреса-обєкту","Key — адреса об'єкту",[3353,3555,3556,3559],{},[3356,3557,3558],{},"Key"," — це рядок, що однозначно ідентифікує об'єкт у bucket. Key може виглядати як шлях з «директоріями»:",[3561,3562,3567],"pre",{"className":3563,"code":3565,"language":3566},[3564],"language-text","photos\u002F2024\u002Fjanuary\u002Fbirthday.jpg\nuploads\u002Fusers\u002Fivan\u002Favatar.png\nreports\u002Fmonthly\u002F2024-01-report.pdf\n","text",[3432,3568,3565],{"__ignoreMap":3569},"",[3353,3571,3572,3573,3576,3577,3580],{},"Насправді ніяких реальних директорій немає — є лише рядок з ",[3432,3574,3575],{},"\u002F"," символами. S3 Console та AWS CLI лише імітують директорії для зручності, фільтруючи об'єкти за префіксом. Але для програми ",[3432,3578,3579],{},"photos\u002F2024\u002Fjanuary\u002Fbirthday.jpg"," — це просто рядок-ключ.",[3353,3582,3583],{},[3356,3584,3585],{},"Повна адреса об'єкту (URL):",[3561,3587,3590],{"className":3588,"code":3589,"language":3566},[3564],"https:\u002F\u002Fmy-bucket.s3.eu-central-1.amazonaws.com\u002Fphotos\u002F2024\u002Fjanuary\u002Fbirthday.jpg\nабо через path-style (застарілий):\nhttps:\u002F\u002Fs3.eu-central-1.amazonaws.com\u002Fmy-bucket\u002Fphotos\u002F2024\u002Fjanuary\u002Fbirthday.jpg\n",[3432,3591,3589],{"__ignoreMap":3569},[3593,3594,3595],"plant-uml",{},[3561,3596,3600],{"className":3597,"code":3598,"language":3599,"meta":3569,"style":3569},"language-plantuml shiki shiki-themes light-plus dark-plus dark-plus","@startuml\nskinparam style plain\nskinparam backgroundColor #ffffff\n\npackage \"AWS S3\" {\n    package \"Bucket: my-app-storage\\n(eu-central-1)\" as B #dbeafe {\n        rectangle \"Object: uploads\u002Favatar.png\\nSize: 234 KB | Class: Standard\\nMetadata: Content-Type: image\u002Fpng\" as O1 #bbf7d0\n        rectangle \"Object: reports\u002Fjan-2024.pdf\\nSize: 1.2 MB | Class: Standard-IA\\nMetadata: Content-Type: application\u002Fpdf\" as O2 #bbf7d0\n        rectangle \"Object: videos\u002Fintro.mp4\\nSize: 450 MB | Class: Intelligent-Tiering\" as O3 #fef3c7\n    }\n}\n\nnote bottom of B\n  Bucket name глобально унікальний\n  Регіон: eu-central-1\n  Об'єктів: необмежено\nend note\n@enduml\n","plantuml",[3432,3601,3602,3610,3616,3622,3629,3635,3641,3647,3653,3659,3665,3671,3676,3682,3688,3694,3700,3706],{"__ignoreMap":3569},[3603,3604,3607],"span",{"class":3605,"line":3606},"line",1,[3603,3608,3609],{},"@startuml\n",[3603,3611,3613],{"class":3605,"line":3612},2,[3603,3614,3615],{},"skinparam style plain\n",[3603,3617,3619],{"class":3605,"line":3618},3,[3603,3620,3621],{},"skinparam backgroundColor #ffffff\n",[3603,3623,3625],{"class":3605,"line":3624},4,[3603,3626,3628],{"emptyLinePlaceholder":3627},true,"\n",[3603,3630,3632],{"class":3605,"line":3631},5,[3603,3633,3634],{},"package \"AWS S3\" {\n",[3603,3636,3638],{"class":3605,"line":3637},6,[3603,3639,3640],{},"    package \"Bucket: my-app-storage\\n(eu-central-1)\" as B #dbeafe {\n",[3603,3642,3644],{"class":3605,"line":3643},7,[3603,3645,3646],{},"        rectangle \"Object: uploads\u002Favatar.png\\nSize: 234 KB | Class: Standard\\nMetadata: Content-Type: image\u002Fpng\" as O1 #bbf7d0\n",[3603,3648,3650],{"class":3605,"line":3649},8,[3603,3651,3652],{},"        rectangle \"Object: reports\u002Fjan-2024.pdf\\nSize: 1.2 MB | Class: Standard-IA\\nMetadata: Content-Type: application\u002Fpdf\" as O2 #bbf7d0\n",[3603,3654,3656],{"class":3605,"line":3655},9,[3603,3657,3658],{},"        rectangle \"Object: videos\u002Fintro.mp4\\nSize: 450 MB | Class: Intelligent-Tiering\" as O3 #fef3c7\n",[3603,3660,3662],{"class":3605,"line":3661},10,[3603,3663,3664],{},"    }\n",[3603,3666,3668],{"class":3605,"line":3667},11,[3603,3669,3670],{},"}\n",[3603,3672,3674],{"class":3605,"line":3673},12,[3603,3675,3628],{"emptyLinePlaceholder":3627},[3603,3677,3679],{"class":3605,"line":3678},13,[3603,3680,3681],{},"note bottom of B\n",[3603,3683,3685],{"class":3605,"line":3684},14,[3603,3686,3687],{},"  Bucket name глобально унікальний\n",[3603,3689,3691],{"class":3605,"line":3690},15,[3603,3692,3693],{},"  Регіон: eu-central-1\n",[3603,3695,3697],{"class":3605,"line":3696},16,[3603,3698,3699],{},"  Об'єктів: необмежено\n",[3603,3701,3703],{"class":3605,"line":3702},17,[3603,3704,3705],{},"end note\n",[3603,3707,3709],{"class":3605,"line":3708},18,[3603,3710,3711],{},"@enduml\n",[3405,3713],{},[3348,3715,3717],{"id":3716},"s3-storage-classes-класи-зберігання","S3 Storage Classes — класи зберігання",[3353,3719,3720,3723],{},[3356,3721,3722],{},"Storage Class"," — це рівень зберігання об'єкту, який визначає баланс між вартістю зберігання, вартістю отримання та часом доступу. AWS пропонує кілька класів, кожен оптимізований для різних патернів використання.",[3412,3725,3727],{"id":3726},"s3-standard-для-активних-даних","S3 Standard — для активних даних",[3353,3729,3730,3733,3734,3737,3738,3741,3742,3745,3746,3749],{},[3356,3731,3732],{},"Вартість:"," ~$0.023\u002FGB\u002Fмісяць (eu-central-1)\n",[3356,3735,3736],{},"Доступність:"," 99.99%, ",[3356,3739,3740],{},"Durability:"," 99.999999999% (11 дев'яток)\n",[3356,3743,3744],{},"Затримка:"," мілісекунди\n",[3356,3747,3748],{},"Мінімум зберігання:"," немає",[3353,3751,3752],{},"Зберігає дані у мінімум трьох Availability Zones. Ідеальний для даних, до яких часто звертаються: аватари, завантажені файли, артефакти CI\u002FCD, логи.",[3353,3754,3755],{},[3356,3756,3757],{},"Типові сценарії використання Standard:",[3369,3759,3760,3766,3772,3778],{},[3372,3761,3762,3765],{},[3356,3763,3764],{},"SaaS-платформа з документами:"," користувач щойно завантажив файл → фронтенд одразу відображає його. Він може відкривати документ кілька разів на день — потрібна найнижча затримка.",[3372,3767,3768,3771],{},[3356,3769,3770],{},"E-commerce продуктові зображення:"," кожна сторінка товару завантажує 5–10 фото. Мільйони запитів на день — будь-яка затримка читання відразу відчутна у conversion rate.",[3372,3773,3774,3777],{},[3356,3775,3776],{},"Артефакти CI\u002FCD:"," Jenkins або GitHub Actions зберігають ZIP-архів після кожного pipeline. Повторний деплой через 2 хвилини потягне його знову — не може бути IA.",[3372,3779,3780,3783],{},[3356,3781,3782],{},"Активні логи застосунку:"," CloudWatch або Datadog парсять логи в реальному часі — дані читаються одразу після запису.",[3353,3785,3786],{},[3356,3787,3788],{},"Коли Standard — неправильний вибір:",[3353,3790,3791],{},"Якщо об'єкт після завантаження лежить невитребуваним більше місяця — ви переплачуєте. Наприклад, щомісячний дамп бази на 50 GB займає Standard і ніколи не читається → $1.15\u002Fмісяць замість $0.63 при Standard-IA.",[3397,3793,3794,3799,3802],{},[3353,3795,3796],{},[3356,3797,3798],{},"Текстова демо: вартість SaaS-платформи",[3353,3800,3801],{},"Уявіть: ваш застосунок зберігає аватари та документи. 1 000 активних користувачів, по 5 MB в середньому:",[3369,3803,3804,3814,3817],{},[3372,3805,3806,3807,3810,3811],{},"Зберігання: 1000 × 5 MB = ",[3356,3808,3809],{},"5 GB"," × $0.023 = ",[3356,3812,3813],{},"$0.115\u002Fмісяць",[3372,3815,3816],{},"50 000 GET запитів \u002F місяць = безкоштовно (Standard не тарифікує GET за вибірку з S3 в рамках Free Tier, а зверху — $0.0004\u002F1000 запитів)",[3372,3818,3819,3820,3823],{},"Загалом: ",[3356,3821,3822],{},"\u003C $1 на місяць"," за файлове сховище",[3412,3825,3827],{"id":3826},"s3-standard-ia-для-рідкісного-доступу","S3 Standard-IA — для рідкісного доступу",[3353,3829,3830,3833,3835,3836,3838],{},[3356,3831,3832],{},"IA = Infrequent Access",[3356,3834,3732],{}," ~$0.0125\u002FGB\u002Fмісяць (зберігання дешевше), але є додаткова плата за кожне читання ($0.01\u002FGB retrieved)\n",[3356,3837,3748],{}," 30 днів (якщо видалити раніше — все одно заплатите за 30 днів)",[3353,3840,3841],{},"Для резервних копій, старих логів, даних що рідко переглядаються, але потрібні при потребі. Вигідний якщо дані зберігаються 30+ днів і читаються не частіше ніж раз на місяць.",[3353,3843,3844],{},[3356,3845,3846],{},"Типові сценарії використання Standard-IA:",[3369,3848,3849,3855,3861,3867],{},[3372,3850,3851,3854],{},[3356,3852,3853],{},"Щотижневі дампи PostgreSQL:"," база на 20 GB дампується щонеділі, зберігається 3 місяці. Читається лише якщо трапляється інцидент — 1–2 рази на квартал. Ідеальний кандидат.",[3372,3856,3857,3860],{},[3356,3858,3859],{},"Сезонні дані:"," рітейл-платформа, дані транзакцій за минулий рік. Потрібні для звіту раз на рік — але мають бути досяжними за мілісекунди, а не годинами.",[3372,3862,3863,3866],{},[3356,3864,3865],{},"Disaster recovery копії:"," вторинна копія об'єктів, що вже є в Standard у іншому регіоні. Читається лише при збої основного сховища.",[3372,3868,3869,3872],{},[3356,3870,3871],{},"Processed ML datasets:"," 500 GB датасет оброблено, модель натренована. Може знадобитись через 2 місяці для re-training.",[3353,3874,3875],{},[3356,3876,3877],{},"Пастка Standard-IA — маленькі файли, що часто читаються:",[3879,3880,3881,3888],"caution",{},[3353,3882,3883,3884,3887],{},"Мінімальна одиниця тарифікації читання — ",[3356,3885,3886],{},"128 KB",". Якщо ваш файл 2 KB і він читається часто — ви платите як за 128 KB кожного разу. Плюс $0.01\u002FGB retrieved. Для файлів \u003C 128 KB і частого доступу Standard вигідніший.",[3353,3889,3890],{},"Приклад: тисяча 4-KB thumbnail-зображень у Standard-IA, до яких звертаються щохвилини — це дорожче, ніж Standard.",[3397,3892,3893,3898,3901,3970],{},[3353,3894,3895],{},[3356,3896,3897],{},"Текстова демо: вибір між Standard і Standard-IA",[3353,3899,3900],{},"Сценарій: 1 TB backup-файлів, читаються 1 раз на місяць (50 GB retrieved):",[3902,3903,3904,3923],"table",{},[3905,3906,3907],"thead",{},[3908,3909,3910,3914,3917,3920],"tr",{},[3911,3912,3913],"th",{},"Клас",[3911,3915,3916],{},"Зберігання",[3911,3918,3919],{},"Читання",[3911,3921,3922],{},"Разом\u002Fмісяць",[3924,3925,3926,3948],"tbody",{},[3908,3927,3928,3932,3938,3944],{},[3929,3930,3931],"td",{},"Standard",[3929,3933,3934,3935],{},"1024 GB × $0.023 = ",[3356,3936,3937],{},"$23.55",[3929,3939,3940,3941],{},"50 GB × $0 = ",[3356,3942,3943],{},"$0",[3929,3945,3946],{},[3356,3947,3937],{},[3908,3949,3950,3953,3959,3965],{},[3929,3951,3952],{},"Standard-IA",[3929,3954,3955,3956],{},"1024 GB × $0.0125 = ",[3356,3957,3958],{},"$12.80",[3929,3960,3961,3962],{},"50 GB × $0.01 = ",[3356,3963,3964],{},"$0.50",[3929,3966,3967],{},[3356,3968,3969],{},"$13.30",[3353,3971,3972,3973,3976,3977,3980],{},"Економія: ",[3356,3974,3975],{},"$10.25\u002Fмісяць"," → ",[3356,3978,3979],{},"$123\u002Fрік"," тільки за рахунок зміни класу.",[3412,3982,3984],{"id":3983},"s3-one-zone-ia-один-az-ще-дешевше","S3 One Zone-IA — один AZ, ще дешевше",[3353,3986,3987,3989],{},[3356,3988,3732],{}," ~$0.01\u002FGB\u002Fмісяць\nЗберігає лише в одному Availability Zone — якщо AZ вийде з ладу, дані недоступні (але не втрачаються). Для відтворюваних даних (ескізи зображень, тимчасові файли).",[3353,3991,3992],{},[3356,3993,3994],{},"Типові сценарії використання One Zone-IA:",[3369,3996,3997,4003,4019],{},[3372,3998,3999,4002],{},[3356,4000,4001],{},"Image thumbnails:"," у вас є 100 000 фото від користувачів у Standard, а thumbnail 200×200 генерується динамічно при першому запиті і кешується. Якщо AZ впаде — thumbnail просто перегенерується, нічого не втрачається.",[3372,4004,4005,4008,4009,3470,4012,3470,4015,4018],{},[3356,4006,4007],{},"Build cache:"," артефакти ",[3432,4010,4011],{},".gradle",[3432,4013,4014],{},".npm",[3432,4016,4017],{},"vendor\u002F"," після CI-пайплайну. Якщо щось зникне — наступний build просто завантажить заново.",[3372,4020,4021,4024],{},[3356,4022,4023],{},"Staging environment assets:"," дані для staging, який і так відтворюється з нуля при потребі.",[3353,4026,4027],{},[3356,4028,4029],{},"Коли One Zone-IA заборонений:",[3353,4031,4032],{},"Якщо даних немає в іншому місці — ніколи не використовуйте One Zone-IA. Збій одного AZ (це рідкість, але трапляється) зробить дані недоступними. Для critical storage завжди вибирайте multi-AZ класи.",[3397,4034,4035,4040,4043,4090],{},[3353,4036,4037],{},[3356,4038,4039],{},"Текстова демо: економія на thumbnails",[3353,4041,4042],{},"500 GB thumbnail зображень, читаються 20 GB\u002Fмісяць:",[3902,4044,4045,4058],{},[3905,4046,4047],{},[3908,4048,4049,4051,4053,4055],{},[3911,4050,3913],{},[3911,4052,3916],{},[3911,4054,3919],{},[3911,4056,4057],{},"Разом",[3924,4059,4060,4075],{},[3908,4061,4062,4064,4067,4070],{},[3929,4063,3952],{},[3929,4065,4066],{},"$6.25",[3929,4068,4069],{},"$0.20",[3929,4071,4072],{},[3356,4073,4074],{},"$6.45",[3908,4076,4077,4080,4083,4085],{},[3929,4078,4079],{},"One Zone-IA",[3929,4081,4082],{},"$5.00",[3929,4084,4069],{},[3929,4086,4087],{},[3356,4088,4089],{},"$5.20",[3353,4091,4092],{},"Економія скромна ($1.25\u002Fмісяць), але при терабайтах thumbnail — вже $30–50 на місяць.",[3412,4094,4096],{"id":4095},"s3-intelligent-tiering-автоматичний-вибір-класу","S3 Intelligent-Tiering — автоматичний вибір класу",[3353,4098,4099,4101],{},[3356,4100,3732],{}," аналогічна Standard\u002FStandard-IA + невелика плата за моніторинг (~$0.0025\u002F1000 об'єктів)",[3353,4103,4104],{},"AWS автоматично переміщує об'єкти між рівнями:",[3369,4106,4107,4113,4119,4125],{},[3372,4108,4109,4112],{},[3356,4110,4111],{},"Frequent Access tier:"," для активно використовуваних об'єктів",[3372,4114,4115,4118],{},[3356,4116,4117],{},"Infrequent Access tier:"," якщо об'єкт не читався 30 днів",[3372,4120,4121,4124],{},[3356,4122,4123],{},"Archive Instant Access:"," якщо не читався 90 днів",[3372,4126,4127,4130],{},[3356,4128,4129],{},"Archive Access:"," якщо не читався 90–180 днів",[3353,4132,4133],{},"Ідеальний якщо патерн доступу непередбачуваний. AWS сам оптимізує витрати.",[3353,4135,4136],{},[3356,4137,4138],{},"Типові сценарії використання Intelligent-Tiering:",[3369,4140,4141,4147,4153],{},[3372,4142,4143,4146],{},[3356,4144,4145],{},"Медіатека:"," відео-контент, де нові відео дивляться мільйони, а старі — рідко. Новий сезон серіалу потрапляє у Frequent Access, серіал 2018 року автоматично мігрує в Infrequent і далі в Archive Instant після 90 днів бездіяльності.",[3372,4148,4149,4152],{},[3356,4150,4151],{},"User-generated content:"," платформа зберігання документів. Більшість документів читаються одразу після завантаження, потім лежать роками. IT-команда не хоче вручну налаштовувати Lifecycle для кожного bucket.",[3372,4154,4155,4158],{},[3356,4156,4157],{},"Наукові датасети:"," великі CSV\u002FParquet файли. Одні активно використовуються для навчання моделей, інші — забуті. Непередбачувано.",[3353,4160,4161],{},[3356,4162,4163],{},"Коли Intelligent-Tiering може бути зайвим:",[3353,4165,4166],{},"Якщо у вас 10 000 маленьких файлів (\u003C 128 KB кожен) — плата за моніторинг ($0.0025\u002F1000 об'єктів) може перевищити економію на зберіганні. Для дрібних файлів з відомим патерном доступу — вручну призначте Standard-IA.",[3397,4168,4169,4174,4177,4224],{},[3353,4170,4171],{},[3356,4172,4173],{},"Текстова демо: Intelligent-Tiering vs ручний вибір",[3353,4175,4176],{},"Медіатека: 10 TB відео. 2 TB — активні (переглядаються), 8 TB — старий контент (майже не читається).",[3902,4178,4179,4189],{},[3905,4180,4181],{},[3908,4182,4183,4186],{},[3911,4184,4185],{},"Підхід",[3911,4187,4188],{},"Вартість",[3924,4190,4191,4202,4213],{},[3908,4192,4193,4196],{},[3929,4194,4195],{},"Все у Standard",[3929,4197,4198,4199],{},"10240 GB × $0.023 = ",[3356,4200,4201],{},"$235.52\u002Fміс",[3908,4203,4204,4207],{},[3929,4205,4206],{},"Вручну: 2 TB Standard + 8 TB Standard-IA",[3929,4208,4209,4210],{},"$47.10 + $102.40 = ",[3356,4211,4212],{},"$149.50\u002Fміс",[3908,4214,4215,4218],{},[3929,4216,4217],{},"Intelligent-Tiering (авто)",[3929,4219,4220,4221],{},"~$47.10 + $81.92 + $0.026 моніторинг ≈ ",[3356,4222,4223],{},"$129.05\u002Fміс",[3353,4225,4226],{},"IT-tiering сам «знайде» ті 8 TB неактивного контенту без жодної конфігурації.",[3412,4228,4230],{"id":4229},"s3-glacier-для-архівів","S3 Glacier — для архівів",[3353,4232,4233,4236],{},[3356,4234,4235],{},"S3 Glacier Instant Retrieval:"," ~$0.004\u002FGB\u002Fмісяць. Миттєвий доступ, але дорожче за читання. Для архівів, до яких звертаються раз на квартал.",[3353,4238,4239,4242,4243,4246],{},[3356,4240,4241],{},"S3 Glacier Flexible Retrieval:"," ~$0.0036\u002FGB\u002Fмісяць. Відновлення займає ",[3356,4244,4245],{},"від 1 хвилини до 12 годин"," (залежно від режиму). Для довгострокових архівів, compliance.",[3353,4248,4249,4252,4253,4256],{},[3356,4250,4251],{},"S3 Glacier Deep Archive:"," ~$0.00099\u002FGB\u002Fмісяць — найдешевший клас. Відновлення займає ",[3356,4254,4255],{},"12–48 годин",". Для даних що зберігаються роками і майже ніколи не читаються.",[3353,4258,4259],{},[3356,4260,4261],{},"Режими відновлення Glacier Flexible (впливають на вартість та час):",[3902,4263,4264,4280],{},[3905,4265,4266],{},[3908,4267,4268,4271,4274,4277],{},[3911,4269,4270],{},"Режим",[3911,4272,4273],{},"Час відновлення",[3911,4275,4276],{},"Вартість відновлення (+ вибірка)",[3911,4278,4279],{},"Використання",[3924,4281,4282,4296,4309],{},[3908,4283,4284,4287,4290,4293],{},[3929,4285,4286],{},"Expedited",[3929,4288,4289],{},"1–5 хвилин",[3929,4291,4292],{},"$0.03\u002FGB + $0.01\u002F1000 req",[3929,4294,4295],{},"Термінові, невеликі об'єкти",[3908,4297,4298,4300,4303,4306],{},[3929,4299,3931],{},[3929,4301,4302],{},"3–5 годин",[3929,4304,4305],{},"$0.01\u002FGB + $0.05\u002F1000 req",[3929,4307,4308],{},"Звичайне відновлення",[3908,4310,4311,4314,4317,4320],{},[3929,4312,4313],{},"Bulk",[3929,4315,4316],{},"5–12 годин",[3929,4318,4319],{},"$0.0025\u002FGB + $0.025\u002F1000 req",[3929,4321,4322],{},"Масове відновлення, найдешевше",[3353,4324,4325],{},[3356,4326,4327],{},"Типові сценарії використання Glacier:",[3369,4329,4330,4336,4342],{},[3372,4331,4332,4335],{},[3356,4333,4334],{},"Glacier Instant Retrieval:"," медичні знімки (DICOM), до яких можуть звернутись будь-якої миті, але в середньому — раз на рік. Лікар вводить номер пацієнта — знімок завантажується за мілісекунди.",[3372,4337,4338,4341],{},[3356,4339,4340],{},"Glacier Flexible:"," фінансова звітність за 7 років (вимога законодавства). Відновлення потрібне лише при аудиті — раз на 2–3 роки, можна почекати 3–5 годин.",[3372,4343,4344,4347],{},[3356,4345,4346],{},"Glacier Deep Archive:"," медичні карти, юридичні договори, відеозаписи з камер відеоспостереження за попередні роки. GDPR і HIPAA вимагають зберігати дані — витяги з них майже ніколи не потрібні.",[3353,4349,4350],{},[3356,4351,4352],{},"Підводні камені Glacier:",[3879,4354,4355,4361],{},[3353,4356,4357,4360],{},[3356,4358,4359],{},"Мінімальний термін зберігання:"," Glacier Flexible — 90 днів, Deep Archive — 180 днів. Якщо видалите раніше — AWS нараховує плату ніби дані пролежали весь мінімальний термін. Не кладіть в Glacier те, що може знадобитись видалити через місяць.",[3353,4362,4363,4366],{},[3356,4364,4365],{},"Вартість читання може перевищити зберігання:"," 1 TB у Deep Archive = $1.01\u002Fмісяць зберігання. Але якщо хтось помилково вирішить прочитати весь 1 TB через Expedited режим Flexible — виставка рахунку на $30+ за одноразове відновлення.",[3397,4368,4369,4374,4377,4384],{},[3353,4370,4371],{},[3356,4372,4373],{},"Текстова демо: compliance архів на 10 років",[3353,4375,4376],{},"Нотаріальна контора зобов'язана зберігати скановані документи 10 років. Накопичується 5 GB на місяць.",[3353,4378,4379,4380,4383],{},"Після 10 років: ",[3356,4381,4382],{},"600 GB"," у Glacier Deep Archive:",[3369,4385,4386,4392,4398],{},[3372,4387,4388,4389],{},"Зберігання: 600 GB × $0.00099 = ",[3356,4390,4391],{},"$0.59\u002Fмісяць",[3372,4393,4394,4395],{},"Vs Standard: 600 GB × $0.023 = ",[3356,4396,4397],{},"$13.80\u002Fмісяць",[3372,4399,4400,3976,4403,4406],{},[3356,4401,4402],{},"Економія: $13.21\u002Fмісяць",[3356,4404,4405],{},"$1585 за 10 років"," тільки за переведення в архів",[3412,4408,4410],{"id":4409},"порівняльна-таблиця-storage-classes","Порівняльна таблиця Storage Classes",[3902,4412,4413,4434],{},[3905,4414,4415],{},[3908,4416,4417,4419,4422,4425,4428,4431],{},[3911,4418,3913],{},[3911,4420,4421],{},"Вартість збер.",[3911,4423,4424],{},"Затримка",[3911,4426,4427],{},"Мінімум збер.",[3911,4429,4430],{},"Плата за читання",[3911,4432,4433],{},"Durability",[3924,4435,4436,4457,4477,4496,4515,4535,4554],{},[3908,4437,4438,4443,4446,4449,4452,4454],{},[3929,4439,4440],{},[3356,4441,4442],{},"S3 Standard",[3929,4444,4445],{},"$0.023\u002FGB",[3929,4447,4448],{},"мс",[3929,4450,4451],{},"немає",[3929,4453,4451],{},[3929,4455,4456],{},"11×9",[3908,4458,4459,4464,4467,4469,4472,4475],{},[3929,4460,4461],{},[3356,4462,4463],{},"S3 Standard-IA",[3929,4465,4466],{},"$0.0125\u002FGB",[3929,4468,4448],{},[3929,4470,4471],{},"30 днів",[3929,4473,4474],{},"$0.01\u002FGB",[3929,4476,4456],{},[3908,4478,4479,4484,4487,4489,4491,4493],{},[3929,4480,4481],{},[3356,4482,4483],{},"S3 One Zone-IA",[3929,4485,4486],{},"$0.010\u002FGB",[3929,4488,4448],{},[3929,4490,4471],{},[3929,4492,4474],{},[3929,4494,4495],{},"11×9*",[3908,4497,4498,4503,4506,4509,4511,4513],{},[3929,4499,4500],{},[3356,4501,4502],{},"Intelligent-Tiering",[3929,4504,4505],{},"$0.023→$0.004\u002FGB",[3929,4507,4508],{},"мс–хвилини",[3929,4510,4451],{},[3929,4512,4451],{},[3929,4514,4456],{},[3908,4516,4517,4522,4525,4527,4530,4533],{},[3929,4518,4519],{},[3356,4520,4521],{},"Glacier Instant",[3929,4523,4524],{},"$0.004\u002FGB",[3929,4526,4448],{},[3929,4528,4529],{},"90 днів",[3929,4531,4532],{},"$0.03\u002FGB",[3929,4534,4456],{},[3908,4536,4537,4542,4545,4548,4550,4552],{},[3929,4538,4539],{},[3356,4540,4541],{},"Glacier Flexible",[3929,4543,4544],{},"$0.0036\u002FGB",[3929,4546,4547],{},"1хв–12год",[3929,4549,4529],{},[3929,4551,4474],{},[3929,4553,4456],{},[3908,4555,4556,4561,4564,4567,4570,4573],{},[3929,4557,4558],{},[3356,4559,4560],{},"Glacier Deep Archive",[3929,4562,4563],{},"$0.00099\u002FGB",[3929,4565,4566],{},"12–48 год",[3929,4568,4569],{},"180 днів",[3929,4571,4572],{},"$0.02\u002FGB",[3929,4574,4456],{},[3353,4576,4577],{},"* One Zone-IA зберігає дані лише в одному AZ — при збої AZ дані стають тимчасово недоступними.",[3593,4579,4580],{},[3561,4581,4583],{"className":3597,"code":4582,"language":3599,"meta":3569,"style":3569},"@startuml\nskinparam style plain\nskinparam backgroundColor #ffffff\ntitle \"S3 Storage Classes: ієрархія вартості та затримки доступу\"\n\nrectangle \"АКТИВНІ ДАНІ\\n(часто читаються)\" as ACTIVE #d1fae5 {\n    rectangle \"S3 Standard\\n$0.023\u002FGB\\nLatency: мілісекунди\\nMin storage: немає\" as STD #bbf7d0\n}\n\nrectangle \"НЕЧАСТІ ДАНІ\\n(рідко читаються)\" as INFREQ #fef3c7 {\n    rectangle \"Standard-IA\\n$0.0125\u002FGB\\n+$0.01\u002FGB retrieved\\nMin: 30 днів\" as SIA #fef3c7\n    rectangle \"One Zone-IA\\n$0.010\u002FGB\\n+$0.01\u002FGB retrieved\\nОдин AZ\" as OZIA #fef3c7\n}\n\nrectangle \"АВТОМАТИЧНА ОПТИМІЗАЦІЯ\" as AUTO #dbeafe {\n    rectangle \"Intelligent-Tiering\\n$0.023→$0.004\u002FGB\\nАвто між рівнями\\n+$0.0025\u002F1000 obj\" as IT #dbeafe\n}\n\nrectangle \"АРХІВ\\n(майже не читаються)\" as ARCH #fce7f3 {\n    rectangle \"Glacier Instant\\n$0.004\u002FGB\\n+$0.03\u002FGB\\nMin: 90 днів\" as GI #fce7f3\n    rectangle \"Glacier Flexible\\n$0.0036\u002FGB\\n1хв–12год відновл.\\nMin: 90 днів\" as GF #fce7f3\n    rectangle \"Glacier Deep Archive\\n$0.00099\u002FGB\\n12–48год відновл.\\nMin: 180 днів\" as GDA #fca5a5\n}\n\nSTD -down-> SIA : Lifecycle:\\n30+ днів без доступу\nSIA -down-> GF : Lifecycle:\\n90+ днів\nGF -down-> GDA : Lifecycle:\\n365+ днів\n\nnote bottom of GDA\n  Найдешевший клас.\n  Для compliance, юридичних\n  архівів, медичних записів.\nend note\n\n@enduml\n",[3432,4584,4585,4589,4593,4597,4602,4606,4611,4616,4620,4624,4629,4634,4639,4643,4647,4652,4657,4661,4665,4671,4677,4683,4689,4694,4699,4705,4711,4717,4722,4728,4734,4740,4746,4751,4756],{"__ignoreMap":3569},[3603,4586,4587],{"class":3605,"line":3606},[3603,4588,3609],{},[3603,4590,4591],{"class":3605,"line":3612},[3603,4592,3615],{},[3603,4594,4595],{"class":3605,"line":3618},[3603,4596,3621],{},[3603,4598,4599],{"class":3605,"line":3624},[3603,4600,4601],{},"title \"S3 Storage Classes: ієрархія вартості та затримки доступу\"\n",[3603,4603,4604],{"class":3605,"line":3631},[3603,4605,3628],{"emptyLinePlaceholder":3627},[3603,4607,4608],{"class":3605,"line":3637},[3603,4609,4610],{},"rectangle \"АКТИВНІ ДАНІ\\n(часто читаються)\" as ACTIVE #d1fae5 {\n",[3603,4612,4613],{"class":3605,"line":3643},[3603,4614,4615],{},"    rectangle \"S3 Standard\\n$0.023\u002FGB\\nLatency: мілісекунди\\nMin storage: немає\" as STD #bbf7d0\n",[3603,4617,4618],{"class":3605,"line":3649},[3603,4619,3670],{},[3603,4621,4622],{"class":3605,"line":3655},[3603,4623,3628],{"emptyLinePlaceholder":3627},[3603,4625,4626],{"class":3605,"line":3661},[3603,4627,4628],{},"rectangle \"НЕЧАСТІ ДАНІ\\n(рідко читаються)\" as INFREQ #fef3c7 {\n",[3603,4630,4631],{"class":3605,"line":3667},[3603,4632,4633],{},"    rectangle \"Standard-IA\\n$0.0125\u002FGB\\n+$0.01\u002FGB retrieved\\nMin: 30 днів\" as SIA #fef3c7\n",[3603,4635,4636],{"class":3605,"line":3673},[3603,4637,4638],{},"    rectangle \"One Zone-IA\\n$0.010\u002FGB\\n+$0.01\u002FGB retrieved\\nОдин AZ\" as OZIA #fef3c7\n",[3603,4640,4641],{"class":3605,"line":3678},[3603,4642,3670],{},[3603,4644,4645],{"class":3605,"line":3684},[3603,4646,3628],{"emptyLinePlaceholder":3627},[3603,4648,4649],{"class":3605,"line":3690},[3603,4650,4651],{},"rectangle \"АВТОМАТИЧНА ОПТИМІЗАЦІЯ\" as AUTO #dbeafe {\n",[3603,4653,4654],{"class":3605,"line":3696},[3603,4655,4656],{},"    rectangle \"Intelligent-Tiering\\n$0.023→$0.004\u002FGB\\nАвто між рівнями\\n+$0.0025\u002F1000 obj\" as IT #dbeafe\n",[3603,4658,4659],{"class":3605,"line":3702},[3603,4660,3670],{},[3603,4662,4663],{"class":3605,"line":3708},[3603,4664,3628],{"emptyLinePlaceholder":3627},[3603,4666,4668],{"class":3605,"line":4667},19,[3603,4669,4670],{},"rectangle \"АРХІВ\\n(майже не читаються)\" as ARCH #fce7f3 {\n",[3603,4672,4674],{"class":3605,"line":4673},20,[3603,4675,4676],{},"    rectangle \"Glacier Instant\\n$0.004\u002FGB\\n+$0.03\u002FGB\\nMin: 90 днів\" as GI #fce7f3\n",[3603,4678,4680],{"class":3605,"line":4679},21,[3603,4681,4682],{},"    rectangle \"Glacier Flexible\\n$0.0036\u002FGB\\n1хв–12год відновл.\\nMin: 90 днів\" as GF #fce7f3\n",[3603,4684,4686],{"class":3605,"line":4685},22,[3603,4687,4688],{},"    rectangle \"Glacier Deep Archive\\n$0.00099\u002FGB\\n12–48год відновл.\\nMin: 180 днів\" as GDA #fca5a5\n",[3603,4690,4692],{"class":3605,"line":4691},23,[3603,4693,3670],{},[3603,4695,4697],{"class":3605,"line":4696},24,[3603,4698,3628],{"emptyLinePlaceholder":3627},[3603,4700,4702],{"class":3605,"line":4701},25,[3603,4703,4704],{},"STD -down-> SIA : Lifecycle:\\n30+ днів без доступу\n",[3603,4706,4708],{"class":3605,"line":4707},26,[3603,4709,4710],{},"SIA -down-> GF : Lifecycle:\\n90+ днів\n",[3603,4712,4714],{"class":3605,"line":4713},27,[3603,4715,4716],{},"GF -down-> GDA : Lifecycle:\\n365+ днів\n",[3603,4718,4720],{"class":3605,"line":4719},28,[3603,4721,3628],{"emptyLinePlaceholder":3627},[3603,4723,4725],{"class":3605,"line":4724},29,[3603,4726,4727],{},"note bottom of GDA\n",[3603,4729,4731],{"class":3605,"line":4730},30,[3603,4732,4733],{},"  Найдешевший клас.\n",[3603,4735,4737],{"class":3605,"line":4736},31,[3603,4738,4739],{},"  Для compliance, юридичних\n",[3603,4741,4743],{"class":3605,"line":4742},32,[3603,4744,4745],{},"  архівів, медичних записів.\n",[3603,4747,4749],{"class":3605,"line":4748},33,[3603,4750,3705],{},[3603,4752,4754],{"class":3605,"line":4753},34,[3603,4755,3628],{"emptyLinePlaceholder":3627},[3603,4757,4759],{"class":3605,"line":4758},35,[3603,4760,3711],{},[4762,4763,4764,4772,4779,4786],"card-group",{},[4765,4766,4769,4771],"card",{"icon":4767,"title":4768},"i-heroicons-bolt","Частий доступ",[3356,4770,3931],{}," — $0.023\u002FGB. Зображення, відео, активні файли, веб-ресурси.",[4765,4773,4776,4778],{"icon":4774,"title":4775},"i-heroicons-archive-box","Рідкий доступ",[3356,4777,3952],{}," — $0.0125\u002FGB. Резервні копії, старі логи, рідко використовувані файли.",[4765,4780,4783,4785],{"icon":4781,"title":4782},"i-heroicons-clock","Архів",[3356,4784,4560],{}," — $0.001\u002FGB. Відповідність регуляторним вимогам, юридичні архіви.",[4765,4787,4790,4792],{"icon":4788,"title":4789},"i-heroicons-sparkles","Невідомий патерн",[3356,4791,4502],{}," — автоматична оптимізація. Якщо не знаєте частоту доступу.",[3412,4794,4796],{"id":4795},"як-обрати-storage-class-практичні-сценарії","Як обрати Storage Class: практичні сценарії",[3353,4798,4799],{},"Нижче — 4 реальних юзкейси з підрахунком вартості та відповіддю «що вибрати і чому».",[4801,4802,4804,4813,4822,4830,4838,4842,4849,4853,4857,4866,4869,4876,4880,4884,4891,4894,4902],"terminal-preview",{"title":4803},"Сценарій 1: 500 GB старих логів, читаємо 1 раз на рік",[4805,4806,4808],"div",{"className":4807},[3605],[3603,4809,4812],{"className":4810},[4811],"text-green-400","# Маємо",[4805,4814,4816,4821],{"className":4815},[3605],[3603,4817,4820],{"className":4818},[4819],"text-yellow-300","Розмір:","    500 GB",[4805,4823,4825,4829],{"className":4824},[3605],[3603,4826,4828],{"className":4827},[4819],"Читання:","   ~50 GB\u002Fрік (аудит раз на рік)",[4805,4831,4833,4837],{"className":4832},[3605],[3603,4834,4836],{"className":4835},[4819],"Зберігаємо:"," 3 роки",[4805,4839,4841],{"className":4840},[3605]," ",[4805,4843,4845],{"className":4844},[3605],[3603,4846,4848],{"className":4847},[4811],"# Розрахунок Standard (не оптимально)",[4805,4850,4852],{"className":4851},[3605],"Зберігання: 500 GB × $0.023 = $11.50\u002Fміс",[4805,4854,4856],{"className":4855},[3605],"Читання:    $0 (включено)",[4805,4858,4860,4861],{"className":4859},[3605],"За 3 роки: ",[3603,4862,4865],{"className":4863},[4864],"text-red-400","$414.00",[4805,4867,4841],{"className":4868},[3605],[4805,4870,4872],{"className":4871},[3605],[3603,4873,4875],{"className":4874},[4811],"# Розрахунок Glacier Flexible (оптимально)",[4805,4877,4879],{"className":4878},[3605],"Зберігання: 500 GB × $0.0036 = $1.80\u002Fміс",[4805,4881,4883],{"className":4882},[3605],"Читання:    50 GB × $0.01 (Standard retrieval) = $0.50\u002Fрік",[4805,4885,4860,4887],{"className":4886},[3605],[3603,4888,4890],{"className":4889},[4811],"$65.30",[4805,4892,4841],{"className":4893},[3605],[4805,4895,4897],{"className":4896},[3605],[3603,4898,4901],{"className":4899},[4900],"text-cyan-400","Рішення: Glacier Flexible. Економія: $348.70 за 3 роки",[4805,4903,4905],{"className":4904},[3605],"Але: читання займе 3–5 годин — прийнятно для аудиту",[4801,4907,4909,4915,4923,4931,4939,4942,4949,4955,4958,4965,4969,4973,4977,4985,4988,4995,4999,5007,5010,5017],{"title":4908},"Сценарій 2: Фотосток — 50% нових, 50% старих фото",[4805,4910,4912],{"className":4911},[3605],[3603,4913,4812],{"className":4914},[4811],[4805,4916,4918,4922],{"className":4917},[3605],[3603,4919,4921],{"className":4920},[4819],"Всього:","     10 TB фото (зростає на 100 GB\u002Fміс)",[4805,4924,4926,4930],{"className":4925},[3605],[3603,4927,4929],{"className":4928},[4819],"Нові фото:","  5 TB — читаються щодня (Standard)",[4805,4932,4934,4938],{"className":4933},[3605],[3603,4935,4937],{"className":4936},[4819],"Старі фото:"," 5 TB — рідко (раз на кілька місяців)",[4805,4940,4841],{"className":4941},[3605],[4805,4943,4945],{"className":4944},[3605],[3603,4946,4948],{"className":4947},[4811],"# Варіант A: все у Standard",[4805,4950,4198,4952],{"className":4951},[3605],[3603,4953,4201],{"className":4954},[4864],[4805,4956,4841],{"className":4957},[3605],[4805,4959,4961],{"className":4960},[3605],[3603,4962,4964],{"className":4963},[4811],"# Варіант B: Lifecycle — Standard → Standard-IA (після 30 днів)",[4805,4966,4968],{"className":4967},[3605],"5120 GB Standard:    5120 × $0.023 = $117.76",[4805,4970,4972],{"className":4971},[3605],"5120 GB Standard-IA: 5120 × $0.0125 = $64.00",[4805,4974,4976],{"className":4975},[3605],"Читання зі Standard-IA: ~100 GB × $0.01 = $1.00",[4805,4978,4980,4981],{"className":4979},[3605],"Разом: ",[3603,4982,4984],{"className":4983},[4811],"$182.76\u002Fміс",[4805,4986,4841],{"className":4987},[3605],[4805,4989,4991],{"className":4990},[3605],[3603,4992,4994],{"className":4993},[4811],"# Варіант C: Intelligent-Tiering (без налаштувань)",[4805,4996,4998],{"className":4997},[3605],"~5120 Frequent + ~5120 Infrequent + моніторинг",[4805,5000,5002,5003],{"className":5001},[3605],"≈ $117.76 + $51.20 + $0.256 = ",[3603,5004,5006],{"className":5005},[4811],"$169.22\u002Fміс",[4805,5008,4841],{"className":5009},[3605],[4805,5011,5013],{"className":5012},[3605],[3603,5014,5016],{"className":5015},[4900],"Рішення: Intelligent-Tiering. $66\u002Fміс → $792\u002Fрік економії",[4805,5018,5020],{"className":5019},[3605],"Плюс: не треба вручну налаштовувати Lifecycle для кожного bucket",[4801,5022,5024,5030,5038,5046,5053,5061,5064,5071,5079,5083,5087,5095,5098,5105],{"title":5023},"Сценарій 3: PostgreSQL бекапи — щоденні, зберігаємо 3 місяці",[4805,5025,5027],{"className":5026},[3605],[3603,5028,4812],{"className":5029},[4811],[4805,5031,5033,5037],{"className":5032},[3605],[3603,5034,5036],{"className":5035},[4819],"Розмір бекапу:"," 15 GB (gzip dump)",[4805,5039,5041,5045],{"className":5040},[3605],[3603,5042,5044],{"className":5043},[4819],"Частота:","       щодня (90 бекапів за 3 місяці)",[4805,5047,5049,5052],{"className":5048},[3605],[3603,5050,4828],{"className":5051},[4819],"       лише при інцидент (1–2 рази на квартал)",[4805,5054,5056,5060],{"className":5055},[3605],[3603,5057,5059],{"className":5058},[4819],"Загальний розмір:"," ~1350 GB",[4805,5062,4841],{"className":5063},[3605],[4805,5065,5067],{"className":5066},[3605],[3603,5068,5070],{"className":5069},[4811],"# Standard-IA vs Standard",[4805,5072,5074,5075],{"className":5073},[3605],"Standard:    1350 × $0.023 = ",[3603,5076,5078],{"className":5077},[4864],"$31.05\u002Fміс",[4805,5080,5082],{"className":5081},[3605],"Standard-IA: 1350 × $0.0125 + 15 GB × $0.01 (retrieval) = $17.03",[4805,5084,5086],{"className":5085},[3605],"Але! 128 KB мінімум × 1350 файлів = тарифікується коректно",[4805,5088,5090,5091],{"className":5089},[3605],"Standard-IA: ",[3603,5092,5094],{"className":5093},[4811],"$17.18\u002Fміс",[4805,5096,4841],{"className":5097},[3605],[4805,5099,5101],{"className":5100},[3605],[3603,5102,5104],{"className":5103},[4900],"Рішення: Standard-IA з Lifecycle для видалення після 90 днів",[4805,5106,5108],{"className":5107},[3605],"Економія: $13.87\u002Fміс → $166\u002Fрік",[4801,5110,5112,5118,5125,5133,5140,5143,5150,5158,5162,5170,5173,5180,5184,5188],{"title":5111},"Сценарій 4: GDPR compliance — документи 7 років, майже ніколи не читаються",[4805,5113,5115],{"className":5114},[3605],[3603,5116,4812],{"className":5117},[4811],[4805,5119,5121,5124],{"className":5120},[3605],[3603,5122,4820],{"className":5123},[4819],"   200 GB\u002Fрік (сканування договорів)",[4805,5126,5128,5132],{"className":5127},[3605],[3603,5129,5131],{"className":5130},[4819],"Термін:","   7 років (регуляторна вимога)",[4805,5134,5136,5139],{"className":5135},[3605],[3603,5137,4828],{"className":5138},[4819],"  при аудиті — 1–2 рази за весь термін",[4805,5141,4841],{"className":5142},[3605],[4805,5144,5146],{"className":5145},[3605],[3603,5147,5149],{"className":5148},[4811],"# Порівняння після 7 років (накопичено 1400 GB)",[4805,5151,5153,5154],{"className":5152},[3605],"Standard:           1400 × $0.023 = ",[3603,5155,5157],{"className":5156},[4864],"$32.20\u002Fміс",[4805,5159,5161],{"className":5160},[3605],"Glacier Flexible:   1400 × $0.0036 = $5.04\u002Fміс",[4805,5163,5165,5166],{"className":5164},[3605],"Deep Archive:       1400 × $0.00099 = ",[3603,5167,5169],{"className":5168},[4811],"$1.39\u002Fміс",[4805,5171,4841],{"className":5172},[3605],[4805,5174,5176],{"className":5175},[3605],[3603,5177,5179],{"className":5178},[4900],"Рішення: Deep Archive з S3 Object Lock (WORM)",[4805,5181,5183],{"className":5182},[3605],"При аудиті: Bulk retrieval (5–12 год) — $0.0025\u002FGB retrieval",[4805,5185,5187],{"className":5186},[3605],"200 GB для перевірки: $0.50 одноразово",[4805,5189,5191],{"className":5190},[3605],"Загальна економія vs Standard за 7 років: ~$2500",[3412,5193,5195],{"id":5194},"як-встановити-storage-class","Як встановити Storage Class",[5197,5198,5199,5300,5514,5846],"tabs",{},[5200,5201,5203],"tabs-item",{"label":5202},"AWS CLI — upload",[3561,5204,5208],{"className":5205,"code":5206,"language":5207,"meta":3569,"style":3569},"language-bash shiki shiki-themes light-plus dark-plus dark-plus","# Завантажити з конкретним класом\naws s3 cp backup.sql.gz s3:\u002F\u002Fmy-bucket\u002Fbackups\u002F \\\n  --storage-class STANDARD_IA\n\n# Доступні значення:\n# STANDARD | REDUCED_REDUNDANCY | STANDARD_IA | ONEZONE_IA\n# INTELLIGENT_TIERING | GLACIER | DEEP_ARCHIVE | GLACIER_IR\n\n# Синхронізувати папку з класом\naws s3 sync .\u002Flogs\u002F s3:\u002F\u002Fmy-bucket\u002Flogs\u002F \\\n  --storage-class INTELLIGENT_TIERING\n","bash",[3432,5209,5210,5216,5239,5248,5252,5257,5262,5267,5271,5276,5293],{"__ignoreMap":3569},[3603,5211,5212],{"class":3605,"line":3606},[3603,5213,5215],{"class":5214},"spJ8K","# Завантажити з конкретним класом\n",[3603,5217,5218,5222,5226,5229,5232,5235],{"class":3605,"line":3612},[3603,5219,5221],{"class":5220},"s8Opu","aws",[3603,5223,5225],{"class":5224},"sbdoH"," s3",[3603,5227,5228],{"class":5224}," cp",[3603,5230,5231],{"class":5224}," backup.sql.gz",[3603,5233,5234],{"class":5224}," s3:\u002F\u002Fmy-bucket\u002Fbackups\u002F",[3603,5236,5238],{"class":5237},"sjcCO"," \\\n",[3603,5240,5241,5245],{"class":3605,"line":3618},[3603,5242,5244],{"class":5243},"su1O8","  --storage-class",[3603,5246,5247],{"class":5224}," STANDARD_IA\n",[3603,5249,5250],{"class":3605,"line":3624},[3603,5251,3628],{"emptyLinePlaceholder":3627},[3603,5253,5254],{"class":3605,"line":3631},[3603,5255,5256],{"class":5214},"# Доступні значення:\n",[3603,5258,5259],{"class":3605,"line":3637},[3603,5260,5261],{"class":5214},"# STANDARD | REDUCED_REDUNDANCY | STANDARD_IA | ONEZONE_IA\n",[3603,5263,5264],{"class":3605,"line":3643},[3603,5265,5266],{"class":5214},"# INTELLIGENT_TIERING | GLACIER | DEEP_ARCHIVE | GLACIER_IR\n",[3603,5268,5269],{"class":3605,"line":3649},[3603,5270,3628],{"emptyLinePlaceholder":3627},[3603,5272,5273],{"class":3605,"line":3655},[3603,5274,5275],{"class":5214},"# Синхронізувати папку з класом\n",[3603,5277,5278,5280,5282,5285,5288,5291],{"class":3605,"line":3661},[3603,5279,5221],{"class":5220},[3603,5281,5225],{"class":5224},[3603,5283,5284],{"class":5224}," sync",[3603,5286,5287],{"class":5224}," .\u002Flogs\u002F",[3603,5289,5290],{"class":5224}," s3:\u002F\u002Fmy-bucket\u002Flogs\u002F",[3603,5292,5238],{"class":5237},[3603,5294,5295,5297],{"class":3605,"line":3667},[3603,5296,5244],{"class":5243},[3603,5298,5299],{"class":5224}," INTELLIGENT_TIERING\n",[5200,5301,5303],{"label":5302},"AWS CLI — зміна класу існуючого об'єкта",[3561,5304,5306],{"className":5205,"code":5305,"language":5207,"meta":3569,"style":3569},"# Змінити клас уже збереженого об'єкта (copy-in-place)\naws s3 cp s3:\u002F\u002Fmy-bucket\u002Fold-backup.sql.gz \\\n          s3:\u002F\u002Fmy-bucket\u002Fold-backup.sql.gz \\\n  --storage-class GLACIER \\\n  --metadata-directive COPY\n\n# Перевірити поточний клас\naws s3api head-object \\\n  --bucket my-bucket \\\n  --key old-backup.sql.gz \\\n  --query \"StorageClass\"\n\n# Відновлення з Glacier Flexible (перед завантаженням обов'язково!)\naws s3api restore-object \\\n  --bucket my-bucket \\\n  --key old-backup.sql.gz \\\n  --restore-request '{\n    \"Days\": 3,\n    \"GlacierJobParameters\": {\n      \"Tier\": \"Standard\"\n    }\n  }'\n\n# Перевірити статус відновлення\naws s3api head-object \\\n  --bucket my-bucket \\\n  --key old-backup.sql.gz \\\n  --query \"Restore\"\n# Виведе: \"ongoing-request=\\\"false\\\", expiry-date=\\\"...\\\"\"\n",[3432,5307,5308,5313,5326,5333,5342,5350,5354,5359,5371,5381,5391,5399,5403,5408,5419,5427,5435,5443,5448,5453,5458,5462,5467,5471,5476,5486,5494,5502,5509],{"__ignoreMap":3569},[3603,5309,5310],{"class":3605,"line":3606},[3603,5311,5312],{"class":5214},"# Змінити клас уже збереженого об'єкта (copy-in-place)\n",[3603,5314,5315,5317,5319,5321,5324],{"class":3605,"line":3612},[3603,5316,5221],{"class":5220},[3603,5318,5225],{"class":5224},[3603,5320,5228],{"class":5224},[3603,5322,5323],{"class":5224}," s3:\u002F\u002Fmy-bucket\u002Fold-backup.sql.gz",[3603,5325,5238],{"class":5237},[3603,5327,5328,5331],{"class":3605,"line":3618},[3603,5329,5330],{"class":5224},"          s3:\u002F\u002Fmy-bucket\u002Fold-backup.sql.gz",[3603,5332,5238],{"class":5237},[3603,5334,5335,5337,5340],{"class":3605,"line":3624},[3603,5336,5244],{"class":5243},[3603,5338,5339],{"class":5224}," GLACIER",[3603,5341,5238],{"class":5237},[3603,5343,5344,5347],{"class":3605,"line":3631},[3603,5345,5346],{"class":5243},"  --metadata-directive",[3603,5348,5349],{"class":5224}," COPY\n",[3603,5351,5352],{"class":3605,"line":3637},[3603,5353,3628],{"emptyLinePlaceholder":3627},[3603,5355,5356],{"class":3605,"line":3643},[3603,5357,5358],{"class":5214},"# Перевірити поточний клас\n",[3603,5360,5361,5363,5366,5369],{"class":3605,"line":3649},[3603,5362,5221],{"class":5220},[3603,5364,5365],{"class":5224}," s3api",[3603,5367,5368],{"class":5224}," head-object",[3603,5370,5238],{"class":5237},[3603,5372,5373,5376,5379],{"class":3605,"line":3655},[3603,5374,5375],{"class":5243},"  --bucket",[3603,5377,5378],{"class":5224}," my-bucket",[3603,5380,5238],{"class":5237},[3603,5382,5383,5386,5389],{"class":3605,"line":3661},[3603,5384,5385],{"class":5243},"  --key",[3603,5387,5388],{"class":5224}," old-backup.sql.gz",[3603,5390,5238],{"class":5237},[3603,5392,5393,5396],{"class":3605,"line":3667},[3603,5394,5395],{"class":5243},"  --query",[3603,5397,5398],{"class":5224}," \"StorageClass\"\n",[3603,5400,5401],{"class":3605,"line":3673},[3603,5402,3628],{"emptyLinePlaceholder":3627},[3603,5404,5405],{"class":3605,"line":3678},[3603,5406,5407],{"class":5214},"# Відновлення з Glacier Flexible (перед завантаженням обов'язково!)\n",[3603,5409,5410,5412,5414,5417],{"class":3605,"line":3684},[3603,5411,5221],{"class":5220},[3603,5413,5365],{"class":5224},[3603,5415,5416],{"class":5224}," restore-object",[3603,5418,5238],{"class":5237},[3603,5420,5421,5423,5425],{"class":3605,"line":3690},[3603,5422,5375],{"class":5243},[3603,5424,5378],{"class":5224},[3603,5426,5238],{"class":5237},[3603,5428,5429,5431,5433],{"class":3605,"line":3696},[3603,5430,5385],{"class":5243},[3603,5432,5388],{"class":5224},[3603,5434,5238],{"class":5237},[3603,5436,5437,5440],{"class":3605,"line":3702},[3603,5438,5439],{"class":5243},"  --restore-request",[3603,5441,5442],{"class":5224}," '{\n",[3603,5444,5445],{"class":3605,"line":3708},[3603,5446,5447],{"class":5224},"    \"Days\": 3,\n",[3603,5449,5450],{"class":3605,"line":4667},[3603,5451,5452],{"class":5224},"    \"GlacierJobParameters\": {\n",[3603,5454,5455],{"class":3605,"line":4673},[3603,5456,5457],{"class":5224},"      \"Tier\": \"Standard\"\n",[3603,5459,5460],{"class":3605,"line":4679},[3603,5461,3664],{"class":5224},[3603,5463,5464],{"class":3605,"line":4685},[3603,5465,5466],{"class":5224},"  }'\n",[3603,5468,5469],{"class":3605,"line":4691},[3603,5470,3628],{"emptyLinePlaceholder":3627},[3603,5472,5473],{"class":3605,"line":4696},[3603,5474,5475],{"class":5214},"# Перевірити статус відновлення\n",[3603,5477,5478,5480,5482,5484],{"class":3605,"line":4701},[3603,5479,5221],{"class":5220},[3603,5481,5365],{"class":5224},[3603,5483,5368],{"class":5224},[3603,5485,5238],{"class":5237},[3603,5487,5488,5490,5492],{"class":3605,"line":4707},[3603,5489,5375],{"class":5243},[3603,5491,5378],{"class":5224},[3603,5493,5238],{"class":5237},[3603,5495,5496,5498,5500],{"class":3605,"line":4713},[3603,5497,5385],{"class":5243},[3603,5499,5388],{"class":5224},[3603,5501,5238],{"class":5237},[3603,5503,5504,5506],{"class":3605,"line":4719},[3603,5505,5395],{"class":5243},[3603,5507,5508],{"class":5224}," \"Restore\"\n",[3603,5510,5511],{"class":3605,"line":4724},[3603,5512,5513],{"class":5214},"# Виведе: \"ongoing-request=\\\"false\\\", expiry-date=\\\"...\\\"\"\n",[5200,5515,5517],{"label":5516},"ASP.NET — upload з класом",[3561,5518,5522],{"className":5519,"code":5520,"language":5521,"meta":3569,"style":3569},"language-csharp shiki shiki-themes light-plus dark-plus dark-plus","\u002F\u002F Встановлення Storage Class при завантаженні через ASP.NET \u002F AWSSDK\nusing Amazon.S3;\nusing Amazon.S3.Model;\n\nvar putRequest = new PutObjectRequest\n{\n    BucketName = \"my-bucket\",\n    Key = $\"backups\u002F{DateTime.UtcNow:yyyy-MM-dd}\u002Fdb.sql.gz\",\n    FilePath = localFilePath,\n    \u002F\u002F Обираємо клас залежно від типу об'єкта\n    StorageClass = S3StorageClass.StandardInfrequentAccess\n};\n\nawait s3Client.PutObjectAsync(putRequest);\n\n\u002F\u002F Зміна класу вже збереженого об'єкта\nvar copyRequest = new CopyObjectRequest\n{\n    SourceBucket = \"my-bucket\",\n    SourceKey = \"backups\u002Fold\u002Fdb.sql.gz\",\n    DestinationBucket = \"my-bucket\",\n    DestinationKey = \"backups\u002Fold\u002Fdb.sql.gz\",   \u002F\u002F той самий ключ\n    StorageClass = S3StorageClass.Glacier,\n    MetadataDirective = S3MetadataDirective.COPY\n};\n\nawait s3Client.CopyObjectAsync(copyRequest);\n","csharp",[3432,5523,5524,5529,5548,5565,5569,5587,5592,5605,5651,5663,5668,5683,5688,5692,5714,5718,5723,5737,5741,5752,5764,5775,5790,5805,5820,5824,5828],{"__ignoreMap":3569},[3603,5525,5526],{"class":3605,"line":3606},[3603,5527,5528],{"class":5214},"\u002F\u002F Встановлення Storage Class при завантаженні через ASP.NET \u002F AWSSDK\n",[3603,5530,5531,5535,5539,5542,5545],{"class":3605,"line":3612},[3603,5532,5534],{"class":5533},"s8xlr","using",[3603,5536,5538],{"class":5537},"sN1BT"," Amazon",[3603,5540,3453],{"class":5541},"sHH4Y",[3603,5543,5544],{"class":5537},"S3",[3603,5546,5547],{"class":5541},";\n",[3603,5549,5550,5552,5554,5556,5558,5560,5563],{"class":3605,"line":3618},[3603,5551,5534],{"class":5533},[3603,5553,5538],{"class":5537},[3603,5555,3453],{"class":5541},[3603,5557,5544],{"class":5537},[3603,5559,3453],{"class":5541},[3603,5561,5562],{"class":5537},"Model",[3603,5564,5547],{"class":5541},[3603,5566,5567],{"class":3605,"line":3624},[3603,5568,3628],{"emptyLinePlaceholder":3627},[3603,5570,5571,5574,5578,5581,5584],{"class":3605,"line":3631},[3603,5572,5573],{"class":5243},"var",[3603,5575,5577],{"class":5576},"siwwj"," putRequest",[3603,5579,5580],{"class":5541}," = ",[3603,5582,5583],{"class":5243},"new",[3603,5585,5586],{"class":5537}," PutObjectRequest\n",[3603,5588,5589],{"class":3605,"line":3637},[3603,5590,5591],{"class":5541},"{\n",[3603,5593,5594,5597,5599,5602],{"class":3605,"line":3643},[3603,5595,5596],{"class":5576},"    BucketName",[3603,5598,5580],{"class":5541},[3603,5600,5601],{"class":5224},"\"my-bucket\"",[3603,5603,5604],{"class":5541},",\n",[3603,5606,5607,5610,5612,5615,5619,5622,5624,5627,5630,5633,5635,5638,5640,5643,5646,5649],{"class":3605,"line":3649},[3603,5608,5609],{"class":5576},"    Key",[3603,5611,5580],{"class":5541},[3603,5613,5614],{"class":5224},"$\"backups\u002F",[3603,5616,5618],{"class":5617},"sD7JJ","{",[3603,5620,5621],{"class":5576},"DateTime",[3603,5623,3453],{"class":5617},[3603,5625,5626],{"class":5576},"UtcNow",[3603,5628,5629],{"class":5541},":",[3603,5631,5632],{"class":5576},"yyyy",[3603,5634,3476],{"class":5541},[3603,5636,5637],{"class":5576},"MM",[3603,5639,3476],{"class":5541},[3603,5641,5642],{"class":5576},"dd",[3603,5644,5645],{"class":5617},"}",[3603,5647,5648],{"class":5224},"\u002Fdb.sql.gz\"",[3603,5650,5604],{"class":5541},[3603,5652,5653,5656,5658,5661],{"class":3605,"line":3655},[3603,5654,5655],{"class":5576},"    FilePath",[3603,5657,5580],{"class":5541},[3603,5659,5660],{"class":5576},"localFilePath",[3603,5662,5604],{"class":5541},[3603,5664,5665],{"class":3605,"line":3661},[3603,5666,5667],{"class":5214},"    \u002F\u002F Обираємо клас залежно від типу об'єкта\n",[3603,5669,5670,5673,5675,5678,5680],{"class":3605,"line":3667},[3603,5671,5672],{"class":5576},"    StorageClass",[3603,5674,5580],{"class":5541},[3603,5676,5677],{"class":5576},"S3StorageClass",[3603,5679,3453],{"class":5541},[3603,5681,5682],{"class":5576},"StandardInfrequentAccess\n",[3603,5684,5685],{"class":3605,"line":3673},[3603,5686,5687],{"class":5541},"};\n",[3603,5689,5690],{"class":3605,"line":3678},[3603,5691,3628],{"emptyLinePlaceholder":3627},[3603,5693,5694,5697,5700,5702,5705,5708,5711],{"class":3605,"line":3684},[3603,5695,5696],{"class":5243},"await",[3603,5698,5699],{"class":5576}," s3Client",[3603,5701,3453],{"class":5541},[3603,5703,5704],{"class":5220},"PutObjectAsync",[3603,5706,5707],{"class":5541},"(",[3603,5709,5710],{"class":5576},"putRequest",[3603,5712,5713],{"class":5541},");\n",[3603,5715,5716],{"class":3605,"line":3690},[3603,5717,3628],{"emptyLinePlaceholder":3627},[3603,5719,5720],{"class":3605,"line":3696},[3603,5721,5722],{"class":5214},"\u002F\u002F Зміна класу вже збереженого об'єкта\n",[3603,5724,5725,5727,5730,5732,5734],{"class":3605,"line":3702},[3603,5726,5573],{"class":5243},[3603,5728,5729],{"class":5576}," copyRequest",[3603,5731,5580],{"class":5541},[3603,5733,5583],{"class":5243},[3603,5735,5736],{"class":5537}," CopyObjectRequest\n",[3603,5738,5739],{"class":3605,"line":3708},[3603,5740,5591],{"class":5541},[3603,5742,5743,5746,5748,5750],{"class":3605,"line":4667},[3603,5744,5745],{"class":5576},"    SourceBucket",[3603,5747,5580],{"class":5541},[3603,5749,5601],{"class":5224},[3603,5751,5604],{"class":5541},[3603,5753,5754,5757,5759,5762],{"class":3605,"line":4673},[3603,5755,5756],{"class":5576},"    SourceKey",[3603,5758,5580],{"class":5541},[3603,5760,5761],{"class":5224},"\"backups\u002Fold\u002Fdb.sql.gz\"",[3603,5763,5604],{"class":5541},[3603,5765,5766,5769,5771,5773],{"class":3605,"line":4679},[3603,5767,5768],{"class":5576},"    DestinationBucket",[3603,5770,5580],{"class":5541},[3603,5772,5601],{"class":5224},[3603,5774,5604],{"class":5541},[3603,5776,5777,5780,5782,5784,5787],{"class":3605,"line":4685},[3603,5778,5779],{"class":5576},"    DestinationKey",[3603,5781,5580],{"class":5541},[3603,5783,5761],{"class":5224},[3603,5785,5786],{"class":5541},",   ",[3603,5788,5789],{"class":5214},"\u002F\u002F той самий ключ\n",[3603,5791,5792,5794,5796,5798,5800,5803],{"class":3605,"line":4691},[3603,5793,5672],{"class":5576},[3603,5795,5580],{"class":5541},[3603,5797,5677],{"class":5576},[3603,5799,3453],{"class":5541},[3603,5801,5802],{"class":5576},"Glacier",[3603,5804,5604],{"class":5541},[3603,5806,5807,5810,5812,5815,5817],{"class":3605,"line":4696},[3603,5808,5809],{"class":5576},"    MetadataDirective",[3603,5811,5580],{"class":5541},[3603,5813,5814],{"class":5576},"S3MetadataDirective",[3603,5816,3453],{"class":5541},[3603,5818,5819],{"class":5576},"COPY\n",[3603,5821,5822],{"class":3605,"line":4701},[3603,5823,5687],{"class":5541},[3603,5825,5826],{"class":3605,"line":4707},[3603,5827,3628],{"emptyLinePlaceholder":3627},[3603,5829,5830,5832,5834,5836,5839,5841,5844],{"class":3605,"line":4713},[3603,5831,5696],{"class":5243},[3603,5833,5699],{"class":5576},[3603,5835,3453],{"class":5541},[3603,5837,5838],{"class":5220},"CopyObjectAsync",[3603,5840,5707],{"class":5541},[3603,5842,5843],{"class":5576},"copyRequest",[3603,5845,5713],{"class":5541},[5200,5847,5849],{"label":5848},"Lifecycle Policy (JSON)",[3561,5850,5854],{"className":5851,"code":5852,"language":5853,"meta":3569,"style":3569},"language-json shiki shiki-themes light-plus dark-plus dark-plus","{\n    \"Rules\": [\n        {\n            \"ID\": \"move-backups-to-ia-then-glacier\",\n            \"Status\": \"Enabled\",\n            \"Filter\": {\n                \"Prefix\": \"backups\u002F\"\n            },\n            \"Transitions\": [\n                {\n                    \"Days\": 30,\n                    \"StorageClass\": \"STANDARD_IA\"\n                },\n                {\n                    \"Days\": 90,\n                    \"StorageClass\": \"GLACIER\"\n                },\n                {\n                    \"Days\": 365,\n                    \"StorageClass\": \"DEEP_ARCHIVE\"\n                }\n            ],\n            \"Expiration\": {\n                \"Days\": 2555\n            }\n        }\n    ]\n}\n","json",[3432,5855,5856,5860,5869,5874,5887,5899,5907,5917,5922,5929,5934,5947,5957,5962,5966,5977,5986,5990,5994,6005,6014,6019,6024,6031,6041,6046,6051,6056],{"__ignoreMap":3569},[3603,5857,5858],{"class":3605,"line":3606},[3603,5859,5591],{"class":5541},[3603,5861,5862,5866],{"class":3605,"line":3612},[3603,5863,5865],{"class":5864},"sLwNe","    \"Rules\"",[3603,5867,5868],{"class":5541},": [\n",[3603,5870,5871],{"class":3605,"line":3618},[3603,5872,5873],{"class":5541},"        {\n",[3603,5875,5876,5879,5882,5885],{"class":3605,"line":3624},[3603,5877,5878],{"class":5864},"            \"ID\"",[3603,5880,5881],{"class":5541},": ",[3603,5883,5884],{"class":5224},"\"move-backups-to-ia-then-glacier\"",[3603,5886,5604],{"class":5541},[3603,5888,5889,5892,5894,5897],{"class":3605,"line":3631},[3603,5890,5891],{"class":5864},"            \"Status\"",[3603,5893,5881],{"class":5541},[3603,5895,5896],{"class":5224},"\"Enabled\"",[3603,5898,5604],{"class":5541},[3603,5900,5901,5904],{"class":3605,"line":3637},[3603,5902,5903],{"class":5864},"            \"Filter\"",[3603,5905,5906],{"class":5541},": {\n",[3603,5908,5909,5912,5914],{"class":3605,"line":3643},[3603,5910,5911],{"class":5864},"                \"Prefix\"",[3603,5913,5881],{"class":5541},[3603,5915,5916],{"class":5224},"\"backups\u002F\"\n",[3603,5918,5919],{"class":3605,"line":3649},[3603,5920,5921],{"class":5541},"            },\n",[3603,5923,5924,5927],{"class":3605,"line":3655},[3603,5925,5926],{"class":5864},"            \"Transitions\"",[3603,5928,5868],{"class":5541},[3603,5930,5931],{"class":3605,"line":3661},[3603,5932,5933],{"class":5541},"                {\n",[3603,5935,5936,5939,5941,5945],{"class":3605,"line":3667},[3603,5937,5938],{"class":5864},"                    \"Days\"",[3603,5940,5881],{"class":5541},[3603,5942,5944],{"class":5943},"sJj4R","30",[3603,5946,5604],{"class":5541},[3603,5948,5949,5952,5954],{"class":3605,"line":3673},[3603,5950,5951],{"class":5864},"                    \"StorageClass\"",[3603,5953,5881],{"class":5541},[3603,5955,5956],{"class":5224},"\"STANDARD_IA\"\n",[3603,5958,5959],{"class":3605,"line":3678},[3603,5960,5961],{"class":5541},"                },\n",[3603,5963,5964],{"class":3605,"line":3684},[3603,5965,5933],{"class":5541},[3603,5967,5968,5970,5972,5975],{"class":3605,"line":3690},[3603,5969,5938],{"class":5864},[3603,5971,5881],{"class":5541},[3603,5973,5974],{"class":5943},"90",[3603,5976,5604],{"class":5541},[3603,5978,5979,5981,5983],{"class":3605,"line":3696},[3603,5980,5951],{"class":5864},[3603,5982,5881],{"class":5541},[3603,5984,5985],{"class":5224},"\"GLACIER\"\n",[3603,5987,5988],{"class":3605,"line":3702},[3603,5989,5961],{"class":5541},[3603,5991,5992],{"class":3605,"line":3708},[3603,5993,5933],{"class":5541},[3603,5995,5996,5998,6000,6003],{"class":3605,"line":4667},[3603,5997,5938],{"class":5864},[3603,5999,5881],{"class":5541},[3603,6001,6002],{"class":5943},"365",[3603,6004,5604],{"class":5541},[3603,6006,6007,6009,6011],{"class":3605,"line":4673},[3603,6008,5951],{"class":5864},[3603,6010,5881],{"class":5541},[3603,6012,6013],{"class":5224},"\"DEEP_ARCHIVE\"\n",[3603,6015,6016],{"class":3605,"line":4679},[3603,6017,6018],{"class":5541},"                }\n",[3603,6020,6021],{"class":3605,"line":4685},[3603,6022,6023],{"class":5541},"            ],\n",[3603,6025,6026,6029],{"class":3605,"line":4691},[3603,6027,6028],{"class":5864},"            \"Expiration\"",[3603,6030,5906],{"class":5541},[3603,6032,6033,6036,6038],{"class":3605,"line":4696},[3603,6034,6035],{"class":5864},"                \"Days\"",[3603,6037,5881],{"class":5541},[3603,6039,6040],{"class":5943},"2555\n",[3603,6042,6043],{"class":3605,"line":4701},[3603,6044,6045],{"class":5541},"            }\n",[3603,6047,6048],{"class":3605,"line":4707},[3603,6049,6050],{"class":5541},"        }\n",[3603,6052,6053],{"class":3605,"line":4713},[3603,6054,6055],{"class":5541},"    ]\n",[3603,6057,6058],{"class":3605,"line":4719},[3603,6059,3670],{"class":5541},[3879,6061,6062,6063,6066,6067,6070,6071,6074],{},"При міграції існуючих об'єктів між класами через CLI або SDK фактично виконується ",[3356,6064,6065],{},"copy-in-place"," — об'єкт копіюється сам у себе з новим storage class. Це ",[3356,6068,6069],{},"генерує PUT-запит"," (оплачується). Для масової міграції тисяч об'єктів використовуйте ",[3356,6072,6073],{},"S3 Batch Operations"," — дешевше та ефективніше.",[3405,6076],{},[3348,6078,6080],{"id":6079},"s3-versioning-збереження-всіх-версій-обєктів","S3 Versioning — збереження всіх версій об'єктів",[3353,6082,6083,6086,6087,6090,6091,6094],{},[3356,6084,6085],{},"S3 Versioning"," — це механізм захисту даних від випадкового видалення та перезапису. Коли версіонування увімкнено, Amazon S3 зберігає ",[3356,6088,6089],{},"кожну версію"," кожного об'єкту у bucket, призначаючи кожній унікальний ідентифікатор — ",[3356,6092,6093],{},"Version ID",". При повторному завантаженні об'єкту з тим самим ключем попередня версія не перезаписується, а стає «некурентною» (noncurrent), і нова версія набуває статусу поточної.",[3353,6096,6097,6100,6101,6104,6105,6108],{},[3356,6098,6099],{},"Концепція Delete Marker."," Видалення об'єкту при увімкненому версіонуванні не знищує дані фізично. Натомість S3 додає спеціальний ",[3356,6102,6103],{},"Delete Marker"," — порожній маркерний об'єкт без тіла, що стає поточною «версією» з цим ключем. Будь-який ",[3432,6106,6107],{},"GET","-запит на цей ключ повертатиме HTTP 404, однак усі попередні версії залишаються фізично збереженими у bucket. Для відновлення достатньо видалити Delete Marker.",[3353,6110,6111],{},[3356,6112,6113],{},"Стани Versioning:",[3369,6115,6116,6126],{},[3372,6117,6118,6121,6122,6125],{},[3432,6119,6120],{},"Suspended"," (за замовчуванням): версіонування вимкнене. Нові об'єкти зберігаються без Version ID (значення ",[3432,6123,6124],{},"null","). Якщо bucket раніше був версіонованим — старі версії залишаються.",[3372,6127,6128,6131,6132,6135,6136,6138],{},[3432,6129,6130],{},"Enabled",": кожен ",[3432,6133,6134],{},"PUT","-запит створює нову версію з унікальним Version ID. Після увімкнення — не можна повністю вимкнути. Можна лише перевести у ",[3432,6137,6120],{}," (нові версії не створюються, старі зберігаються).",[3353,6140,6141],{},[6142,6143],"img",{"alt":6144,"className":6145,"src":6147},"Amazon S3 versioning enabled overview screenshot",[6146],"diagram-img","\u002Fimages\u002Faws\u002Fs3\u002F01.png",[3353,6149,6150],{},[6142,6151],{"alt":6152,"className":6153,"src":6154},"Amazon S3 versioning put operation screenshot",[6146],"\u002Fimages\u002Faws\u002Fs3\u002F02.png",[3353,6156,6157],{},[6142,6158],{"alt":6159,"className":6160,"src":6161},"Amazon S3 versioning delete marker screenshot",[6146],"\u002Fimages\u002Faws\u002Fs3\u002F03.png",[3353,6163,6164],{},[3356,6165,6166],{},"Навіщо потрібне версіонування:",[3369,6168,6169,6175,6181,6187],{},[3372,6170,6171,6174],{},[3356,6172,6173],{},"Захист від випадкового видалення:"," видалений об'єкт отримує Delete Marker — відновлення тривіальне",[3372,6176,6177,6180],{},[3356,6178,6179],{},"Захист від перезапису:"," можна повернутись до будь-якої попередньої версії файлу",[3372,6182,6183,6186],{},[3356,6184,6185],{},"Регуляторна відповідність"," (GDPR, SOC 2, ISO 27001, HIPAA): зберігати незмінювані версії документів",[3372,6188,6189,6192],{},[3356,6190,6191],{},"Відкат деплою:"," якщо новий build фронтенду зламав сайт — повернути попередню версію за секунди",[3593,6194,6195],{},[3561,6196,6198],{"className":3597,"code":6197,"language":3599,"meta":3569,"style":3569},"@startuml\nskinparam style plain\nskinparam backgroundColor #ffffff\ntitle \"S3 Versioning: структура версій при PUT та DELETE операціях\"\n\npackage \"Bucket: my-app-bucket (Versioning: Enabled)\" as BKT {\n\n    package \"Key: uploads\u002Favatar.png\" as K1 {\n        rectangle \"VersionId: Abc1xZy3\\n◀ CURRENT (IsLatest: true)\\nSize: 89 KB | Uploaded: May 20\" as V1 #bbf7d0\n        rectangle \"VersionId: mK8pQrT2\\n(noncurrent)\\nSize: 76 KB | Uploaded: May 15\" as V2 #fef3c7\n        rectangle \"VersionId: nJ5wLvR9\\n(noncurrent)\\nSize: 81 KB | Uploaded: May 5\" as V3 #e5e7eb\n    }\n\n    package \"Key: config\u002Fapp.json\" as K2 {\n        rectangle \"DELETE MARKER\\n◀ CURRENT (IsLatest: true)\\n(нема тіла — GET → 404)\" as DM #fca5a5\n        rectangle \"VersionId: zT6hMwK1\\n(noncurrent — дані збережені!)\\nSize: 12 KB\" as V4 #fef3c7\n    }\n}\n\nV1 -[hidden]down- V2\nV2 -[hidden]down- V3\nDM -[hidden]down- V4\n\nnote bottom of K2\n  Відновлення config\u002Fapp.json:\n  1. aws s3api delete-object --version-id \u003CDM_ID>\n  2. Об'єкт знову доступний — дані не втрачались!\nend note\n\nnote right of V3\n  Lifecycle Policy:\n  NoncurrentVersionExpiration.NoncurrentDays: 30\n  → автоматично видалить noncurrent\n  версії старші 30 днів\nend note\n\n@enduml\n",[3432,6199,6200,6204,6208,6212,6217,6221,6226,6230,6235,6240,6245,6250,6254,6258,6263,6268,6273,6277,6281,6285,6290,6295,6300,6304,6309,6314,6319,6324,6328,6332,6337,6342,6347,6352,6357,6361,6366],{"__ignoreMap":3569},[3603,6201,6202],{"class":3605,"line":3606},[3603,6203,3609],{},[3603,6205,6206],{"class":3605,"line":3612},[3603,6207,3615],{},[3603,6209,6210],{"class":3605,"line":3618},[3603,6211,3621],{},[3603,6213,6214],{"class":3605,"line":3624},[3603,6215,6216],{},"title \"S3 Versioning: структура версій при PUT та DELETE операціях\"\n",[3603,6218,6219],{"class":3605,"line":3631},[3603,6220,3628],{"emptyLinePlaceholder":3627},[3603,6222,6223],{"class":3605,"line":3637},[3603,6224,6225],{},"package \"Bucket: my-app-bucket (Versioning: Enabled)\" as BKT {\n",[3603,6227,6228],{"class":3605,"line":3643},[3603,6229,3628],{"emptyLinePlaceholder":3627},[3603,6231,6232],{"class":3605,"line":3649},[3603,6233,6234],{},"    package \"Key: uploads\u002Favatar.png\" as K1 {\n",[3603,6236,6237],{"class":3605,"line":3655},[3603,6238,6239],{},"        rectangle \"VersionId: Abc1xZy3\\n◀ CURRENT (IsLatest: true)\\nSize: 89 KB | Uploaded: May 20\" as V1 #bbf7d0\n",[3603,6241,6242],{"class":3605,"line":3661},[3603,6243,6244],{},"        rectangle \"VersionId: mK8pQrT2\\n(noncurrent)\\nSize: 76 KB | Uploaded: May 15\" as V2 #fef3c7\n",[3603,6246,6247],{"class":3605,"line":3667},[3603,6248,6249],{},"        rectangle \"VersionId: nJ5wLvR9\\n(noncurrent)\\nSize: 81 KB | Uploaded: May 5\" as V3 #e5e7eb\n",[3603,6251,6252],{"class":3605,"line":3673},[3603,6253,3664],{},[3603,6255,6256],{"class":3605,"line":3678},[3603,6257,3628],{"emptyLinePlaceholder":3627},[3603,6259,6260],{"class":3605,"line":3684},[3603,6261,6262],{},"    package \"Key: config\u002Fapp.json\" as K2 {\n",[3603,6264,6265],{"class":3605,"line":3690},[3603,6266,6267],{},"        rectangle \"DELETE MARKER\\n◀ CURRENT (IsLatest: true)\\n(нема тіла — GET → 404)\" as DM #fca5a5\n",[3603,6269,6270],{"class":3605,"line":3696},[3603,6271,6272],{},"        rectangle \"VersionId: zT6hMwK1\\n(noncurrent — дані збережені!)\\nSize: 12 KB\" as V4 #fef3c7\n",[3603,6274,6275],{"class":3605,"line":3702},[3603,6276,3664],{},[3603,6278,6279],{"class":3605,"line":3708},[3603,6280,3670],{},[3603,6282,6283],{"class":3605,"line":4667},[3603,6284,3628],{"emptyLinePlaceholder":3627},[3603,6286,6287],{"class":3605,"line":4673},[3603,6288,6289],{},"V1 -[hidden]down- V2\n",[3603,6291,6292],{"class":3605,"line":4679},[3603,6293,6294],{},"V2 -[hidden]down- V3\n",[3603,6296,6297],{"class":3605,"line":4685},[3603,6298,6299],{},"DM -[hidden]down- V4\n",[3603,6301,6302],{"class":3605,"line":4691},[3603,6303,3628],{"emptyLinePlaceholder":3627},[3603,6305,6306],{"class":3605,"line":4696},[3603,6307,6308],{},"note bottom of K2\n",[3603,6310,6311],{"class":3605,"line":4701},[3603,6312,6313],{},"  Відновлення config\u002Fapp.json:\n",[3603,6315,6316],{"class":3605,"line":4707},[3603,6317,6318],{},"  1. aws s3api delete-object --version-id \u003CDM_ID>\n",[3603,6320,6321],{"class":3605,"line":4713},[3603,6322,6323],{},"  2. Об'єкт знову доступний — дані не втрачались!\n",[3603,6325,6326],{"class":3605,"line":4719},[3603,6327,3705],{},[3603,6329,6330],{"class":3605,"line":4724},[3603,6331,3628],{"emptyLinePlaceholder":3627},[3603,6333,6334],{"class":3605,"line":4730},[3603,6335,6336],{},"note right of V3\n",[3603,6338,6339],{"class":3605,"line":4736},[3603,6340,6341],{},"  Lifecycle Policy:\n",[3603,6343,6344],{"class":3605,"line":4742},[3603,6345,6346],{},"  NoncurrentVersionExpiration.NoncurrentDays: 30\n",[3603,6348,6349],{"class":3605,"line":4748},[3603,6350,6351],{},"  → автоматично видалить noncurrent\n",[3603,6353,6354],{"class":3605,"line":4753},[3603,6355,6356],{},"  версії старші 30 днів\n",[3603,6358,6359],{"class":3605,"line":4758},[3603,6360,3705],{},[3603,6362,6364],{"class":3605,"line":6363},36,[3603,6365,3628],{"emptyLinePlaceholder":3627},[3603,6367,6369],{"class":3605,"line":6368},37,[3603,6370,3711],{},[5197,6372,6373,6492],{},[5200,6374,6376,6381,6411,6416,6437,6442,6466,6471],{"label":6375},"AWS Console",[3353,6377,6378],{},[3356,6379,6380],{},"Увімкнення версіонування:",[6382,6383,6384,6393,6402],"ol",{},[3372,6385,6386,6387,6389,6390],{},"Відкрийте ",[3356,6388,5544],{}," → оберіть bucket → вкладка ",[3356,6391,6392],{},"Properties",[3372,6394,6395,6396,3976,6399],{},"Прокрутіть до секції ",[3356,6397,6398],{},"Bucket Versioning",[3356,6400,6401],{},"Edit",[3372,6403,6404,6405,3976,6408],{},"Оберіть ",[3356,6406,6407],{},"Enable",[3356,6409,6410],{},"Save changes",[3353,6412,6413],{},[3356,6414,6415],{},"Перегляд версій об'єктів:",[6382,6417,6418,6424,6431],{},[3372,6419,6420,6421],{},"S3 → bucket → у верхньому правому куті ввімкніть перемикач ",[3356,6422,6423],{},"Show versions",[3372,6425,6426,6427,6430],{},"У списку з'являться всі версії кожного файлу та Delete Markers (з міткою ",[3432,6428,6429],{},"Delete marker",")",[3372,6432,6433,6434,6436],{},"Стовпець ",[3356,6435,6093],{}," — унікальний ідентифікатор версії",[3353,6438,6439],{},[3356,6440,6441],{},"Відновлення «видаленого» об'єкту (видалення Delete Marker):",[6382,6443,6444,6450,6456,6463],{},[3372,6445,6446,6447],{},"Bucket → ",[3356,6448,6449],{},"Show versions: ON",[3372,6451,6452,6453,6455],{},"Знайдіть рядок з міткою ",[3432,6454,6429],{}," для потрібного ключа",[3372,6457,6458,6459,6462],{},"Оберіть його прапорцем → ",[3356,6460,6461],{},"Delete"," → підтвердіть",[3372,6464,6465],{},"Об'єкт знову доступний — попередня версія автоматично стала поточною",[3353,6467,6468],{},[3356,6469,6470],{},"Відновлення конкретної версії:",[6382,6472,6473,6476,6486],{},[3372,6474,6475],{},"Show versions: ON → знайдіть потрібну версію",[3372,6477,6478,6479,6482,6483],{},"Клікніть на Version ID → ",[3356,6480,6481],{},"Download"," або ",[3356,6484,6485],{},"Copy S3 URI",[3372,6487,6488,6489,6430],{},"Щоб зробити її поточною: завантажте та повторно завантажте (або скопіюйте через CLI ",[3432,6490,6491],{},"copy-object",[5200,6493,6495],{"label":6494},"AWS CLI",[3561,6496,6498],{"className":5205,"code":6497,"language":5207,"meta":3569,"style":3569},"BUCKET=\"my-app-bucket\"\nREGION=\"eu-central-1\"\n\n# Увімкнути версіонування\naws s3api put-bucket-versioning \\\n    --bucket \"$BUCKET\" \\\n    --versioning-configuration Status=Enabled \\\n    --region \"$REGION\"\n\n# Перевірити стан версіонування\naws s3api get-bucket-versioning \\\n    --bucket \"$BUCKET\" --region \"$REGION\"\n\n# Переглянути всі версії об'єкту з відфільтрованим виводом\naws s3api list-object-versions \\\n    --bucket \"$BUCKET\" \\\n    --prefix \"uploads\u002Favatar.png\" \\\n    --region \"$REGION\" \\\n    --query \"{\n        Versions: Versions[*].{Key:Key, VersionId:VersionId, IsLatest:IsLatest, Size:Size},\n        DeleteMarkers: DeleteMarkers[*].{Key:Key, VersionId:VersionId, IsLatest:IsLatest}\n    }\"\n\n# Завантажити конкретну версію\naws s3api get-object \\\n    --bucket \"$BUCKET\" \\\n    --key \"uploads\u002Favatar.png\" \\\n    --version-id \"mK8pQrT2abc123xyz\" \\\n    downloaded-old-version.png \\\n    --region \"$REGION\"\n\n# Відновити попередню версію (зробити її поточною через copy-object)\naws s3api copy-object \\\n    --bucket \"$BUCKET\" \\\n    --copy-source \"$BUCKET\u002Fuploads\u002Favatar.png?versionId=mK8pQrT2abc123xyz\" \\\n    --key \"uploads\u002Favatar.png\" \\\n    --region \"$REGION\"\n\n# Знайти та видалити Delete Marker (відновити 'видалений' об'єкт)\nDM_VERSION=$(aws s3api list-object-versions \\\n    --bucket \"$BUCKET\" \\\n    --prefix \"config\u002Fapp.json\" \\\n    --region \"$REGION\" \\\n    --query \"DeleteMarkers[?IsLatest==\\`true\\`].VersionId\" \\\n    --output text)\n\necho \"Delete Marker Version ID: $DM_VERSION\"\n\naws s3api delete-object \\\n    --bucket \"$BUCKET\" \\\n    --key \"config\u002Fapp.json\" \\\n    --version-id \"$DM_VERSION\" \\\n    --region \"$REGION\"\n\necho \"Object restored successfully!\"\n",[3432,6499,6500,6511,6521,6525,6530,6541,6557,6567,6580,6584,6589,6600,6619,6623,6628,6639,6651,6661,6673,6681,6686,6691,6696,6700,6705,6716,6728,6737,6747,6754,6764,6768,6773,6784,6796,6810,6818,6828,6833,6839,6856,6869,6879,6892,6913,6925,6930,6944,6949,6961,6974,6983,6996,7007,7012],{"__ignoreMap":3569},[3603,6501,6502,6505,6508],{"class":3605,"line":3606},[3603,6503,6504],{"class":5576},"BUCKET",[3603,6506,6507],{"class":5541},"=",[3603,6509,6510],{"class":5224},"\"my-app-bucket\"\n",[3603,6512,6513,6516,6518],{"class":3605,"line":3612},[3603,6514,6515],{"class":5576},"REGION",[3603,6517,6507],{"class":5541},[3603,6519,6520],{"class":5224},"\"eu-central-1\"\n",[3603,6522,6523],{"class":3605,"line":3618},[3603,6524,3628],{"emptyLinePlaceholder":3627},[3603,6526,6527],{"class":3605,"line":3624},[3603,6528,6529],{"class":5214},"# Увімкнути версіонування\n",[3603,6531,6532,6534,6536,6539],{"class":3605,"line":3631},[3603,6533,5221],{"class":5220},[3603,6535,5365],{"class":5224},[3603,6537,6538],{"class":5224}," put-bucket-versioning",[3603,6540,5238],{"class":5237},[3603,6542,6543,6546,6549,6552,6555],{"class":3605,"line":3637},[3603,6544,6545],{"class":5243},"    --bucket",[3603,6547,6548],{"class":5224}," \"",[3603,6550,6551],{"class":5576},"$BUCKET",[3603,6553,6554],{"class":5224},"\"",[3603,6556,5238],{"class":5237},[3603,6558,6559,6562,6565],{"class":3605,"line":3643},[3603,6560,6561],{"class":5243},"    --versioning-configuration",[3603,6563,6564],{"class":5224}," Status=Enabled",[3603,6566,5238],{"class":5237},[3603,6568,6569,6572,6574,6577],{"class":3605,"line":3649},[3603,6570,6571],{"class":5243},"    --region",[3603,6573,6548],{"class":5224},[3603,6575,6576],{"class":5576},"$REGION",[3603,6578,6579],{"class":5224},"\"\n",[3603,6581,6582],{"class":3605,"line":3655},[3603,6583,3628],{"emptyLinePlaceholder":3627},[3603,6585,6586],{"class":3605,"line":3661},[3603,6587,6588],{"class":5214},"# Перевірити стан версіонування\n",[3603,6590,6591,6593,6595,6598],{"class":3605,"line":3667},[3603,6592,5221],{"class":5220},[3603,6594,5365],{"class":5224},[3603,6596,6597],{"class":5224}," get-bucket-versioning",[3603,6599,5238],{"class":5237},[3603,6601,6602,6604,6606,6608,6610,6613,6615,6617],{"class":3605,"line":3673},[3603,6603,6545],{"class":5243},[3603,6605,6548],{"class":5224},[3603,6607,6551],{"class":5576},[3603,6609,6554],{"class":5224},[3603,6611,6612],{"class":5243}," --region",[3603,6614,6548],{"class":5224},[3603,6616,6576],{"class":5576},[3603,6618,6579],{"class":5224},[3603,6620,6621],{"class":3605,"line":3678},[3603,6622,3628],{"emptyLinePlaceholder":3627},[3603,6624,6625],{"class":3605,"line":3684},[3603,6626,6627],{"class":5214},"# Переглянути всі версії об'єкту з відфільтрованим виводом\n",[3603,6629,6630,6632,6634,6637],{"class":3605,"line":3690},[3603,6631,5221],{"class":5220},[3603,6633,5365],{"class":5224},[3603,6635,6636],{"class":5224}," list-object-versions",[3603,6638,5238],{"class":5237},[3603,6640,6641,6643,6645,6647,6649],{"class":3605,"line":3696},[3603,6642,6545],{"class":5243},[3603,6644,6548],{"class":5224},[3603,6646,6551],{"class":5576},[3603,6648,6554],{"class":5224},[3603,6650,5238],{"class":5237},[3603,6652,6653,6656,6659],{"class":3605,"line":3702},[3603,6654,6655],{"class":5243},"    --prefix",[3603,6657,6658],{"class":5224}," \"uploads\u002Favatar.png\"",[3603,6660,5238],{"class":5237},[3603,6662,6663,6665,6667,6669,6671],{"class":3605,"line":3708},[3603,6664,6571],{"class":5243},[3603,6666,6548],{"class":5224},[3603,6668,6576],{"class":5576},[3603,6670,6554],{"class":5224},[3603,6672,5238],{"class":5237},[3603,6674,6675,6678],{"class":3605,"line":4667},[3603,6676,6677],{"class":5243},"    --query",[3603,6679,6680],{"class":5224}," \"{\n",[3603,6682,6683],{"class":3605,"line":4673},[3603,6684,6685],{"class":5224},"        Versions: Versions[*].{Key:Key, VersionId:VersionId, IsLatest:IsLatest, Size:Size},\n",[3603,6687,6688],{"class":3605,"line":4679},[3603,6689,6690],{"class":5224},"        DeleteMarkers: DeleteMarkers[*].{Key:Key, VersionId:VersionId, IsLatest:IsLatest}\n",[3603,6692,6693],{"class":3605,"line":4685},[3603,6694,6695],{"class":5224},"    }\"\n",[3603,6697,6698],{"class":3605,"line":4691},[3603,6699,3628],{"emptyLinePlaceholder":3627},[3603,6701,6702],{"class":3605,"line":4696},[3603,6703,6704],{"class":5214},"# Завантажити конкретну версію\n",[3603,6706,6707,6709,6711,6714],{"class":3605,"line":4701},[3603,6708,5221],{"class":5220},[3603,6710,5365],{"class":5224},[3603,6712,6713],{"class":5224}," get-object",[3603,6715,5238],{"class":5237},[3603,6717,6718,6720,6722,6724,6726],{"class":3605,"line":4707},[3603,6719,6545],{"class":5243},[3603,6721,6548],{"class":5224},[3603,6723,6551],{"class":5576},[3603,6725,6554],{"class":5224},[3603,6727,5238],{"class":5237},[3603,6729,6730,6733,6735],{"class":3605,"line":4713},[3603,6731,6732],{"class":5243},"    --key",[3603,6734,6658],{"class":5224},[3603,6736,5238],{"class":5237},[3603,6738,6739,6742,6745],{"class":3605,"line":4719},[3603,6740,6741],{"class":5243},"    --version-id",[3603,6743,6744],{"class":5224}," \"mK8pQrT2abc123xyz\"",[3603,6746,5238],{"class":5237},[3603,6748,6749,6752],{"class":3605,"line":4724},[3603,6750,6751],{"class":5224},"    downloaded-old-version.png",[3603,6753,5238],{"class":5237},[3603,6755,6756,6758,6760,6762],{"class":3605,"line":4730},[3603,6757,6571],{"class":5243},[3603,6759,6548],{"class":5224},[3603,6761,6576],{"class":5576},[3603,6763,6579],{"class":5224},[3603,6765,6766],{"class":3605,"line":4736},[3603,6767,3628],{"emptyLinePlaceholder":3627},[3603,6769,6770],{"class":3605,"line":4742},[3603,6771,6772],{"class":5214},"# Відновити попередню версію (зробити її поточною через copy-object)\n",[3603,6774,6775,6777,6779,6782],{"class":3605,"line":4748},[3603,6776,5221],{"class":5220},[3603,6778,5365],{"class":5224},[3603,6780,6781],{"class":5224}," copy-object",[3603,6783,5238],{"class":5237},[3603,6785,6786,6788,6790,6792,6794],{"class":3605,"line":4753},[3603,6787,6545],{"class":5243},[3603,6789,6548],{"class":5224},[3603,6791,6551],{"class":5576},[3603,6793,6554],{"class":5224},[3603,6795,5238],{"class":5237},[3603,6797,6798,6801,6803,6805,6808],{"class":3605,"line":4758},[3603,6799,6800],{"class":5243},"    --copy-source",[3603,6802,6548],{"class":5224},[3603,6804,6551],{"class":5576},[3603,6806,6807],{"class":5224},"\u002Fuploads\u002Favatar.png?versionId=mK8pQrT2abc123xyz\"",[3603,6809,5238],{"class":5237},[3603,6811,6812,6814,6816],{"class":3605,"line":6363},[3603,6813,6732],{"class":5243},[3603,6815,6658],{"class":5224},[3603,6817,5238],{"class":5237},[3603,6819,6820,6822,6824,6826],{"class":3605,"line":6368},[3603,6821,6571],{"class":5243},[3603,6823,6548],{"class":5224},[3603,6825,6576],{"class":5576},[3603,6827,6579],{"class":5224},[3603,6829,6831],{"class":3605,"line":6830},38,[3603,6832,3628],{"emptyLinePlaceholder":3627},[3603,6834,6836],{"class":3605,"line":6835},39,[3603,6837,6838],{"class":5214},"# Знайти та видалити Delete Marker (відновити 'видалений' об'єкт)\n",[3603,6840,6842,6845,6848,6850,6852,6854],{"class":3605,"line":6841},40,[3603,6843,6844],{"class":5576},"DM_VERSION",[3603,6846,6847],{"class":5541},"=$(",[3603,6849,5221],{"class":5220},[3603,6851,5365],{"class":5224},[3603,6853,6636],{"class":5224},[3603,6855,5238],{"class":5237},[3603,6857,6859,6861,6863,6865,6867],{"class":3605,"line":6858},41,[3603,6860,6545],{"class":5243},[3603,6862,6548],{"class":5224},[3603,6864,6551],{"class":5576},[3603,6866,6554],{"class":5224},[3603,6868,5238],{"class":5237},[3603,6870,6872,6874,6877],{"class":3605,"line":6871},42,[3603,6873,6655],{"class":5243},[3603,6875,6876],{"class":5224}," \"config\u002Fapp.json\"",[3603,6878,5238],{"class":5237},[3603,6880,6882,6884,6886,6888,6890],{"class":3605,"line":6881},43,[3603,6883,6571],{"class":5243},[3603,6885,6548],{"class":5224},[3603,6887,6576],{"class":5576},[3603,6889,6554],{"class":5224},[3603,6891,5238],{"class":5237},[3603,6893,6895,6897,6900,6903,6906,6908,6911],{"class":3605,"line":6894},44,[3603,6896,6677],{"class":5243},[3603,6898,6899],{"class":5224}," \"DeleteMarkers[?IsLatest==",[3603,6901,6902],{"class":5237},"\\`",[3603,6904,6905],{"class":5224},"true",[3603,6907,6902],{"class":5237},[3603,6909,6910],{"class":5224},"].VersionId\"",[3603,6912,5238],{"class":5237},[3603,6914,6916,6919,6922],{"class":3605,"line":6915},45,[3603,6917,6918],{"class":5243},"    --output",[3603,6920,6921],{"class":5224}," text",[3603,6923,6924],{"class":5541},")\n",[3603,6926,6928],{"class":3605,"line":6927},46,[3603,6929,3628],{"emptyLinePlaceholder":3627},[3603,6931,6933,6936,6939,6942],{"class":3605,"line":6932},47,[3603,6934,6935],{"class":5220},"echo",[3603,6937,6938],{"class":5224}," \"Delete Marker Version ID: ",[3603,6940,6941],{"class":5576},"$DM_VERSION",[3603,6943,6579],{"class":5224},[3603,6945,6947],{"class":3605,"line":6946},48,[3603,6948,3628],{"emptyLinePlaceholder":3627},[3603,6950,6952,6954,6956,6959],{"class":3605,"line":6951},49,[3603,6953,5221],{"class":5220},[3603,6955,5365],{"class":5224},[3603,6957,6958],{"class":5224}," delete-object",[3603,6960,5238],{"class":5237},[3603,6962,6964,6966,6968,6970,6972],{"class":3605,"line":6963},50,[3603,6965,6545],{"class":5243},[3603,6967,6548],{"class":5224},[3603,6969,6551],{"class":5576},[3603,6971,6554],{"class":5224},[3603,6973,5238],{"class":5237},[3603,6975,6977,6979,6981],{"class":3605,"line":6976},51,[3603,6978,6732],{"class":5243},[3603,6980,6876],{"class":5224},[3603,6982,5238],{"class":5237},[3603,6984,6986,6988,6990,6992,6994],{"class":3605,"line":6985},52,[3603,6987,6741],{"class":5243},[3603,6989,6548],{"class":5224},[3603,6991,6941],{"class":5576},[3603,6993,6554],{"class":5224},[3603,6995,5238],{"class":5237},[3603,6997,6999,7001,7003,7005],{"class":3605,"line":6998},53,[3603,7000,6571],{"class":5243},[3603,7002,6548],{"class":5224},[3603,7004,6576],{"class":5576},[3603,7006,6579],{"class":5224},[3603,7008,7010],{"class":3605,"line":7009},54,[3603,7011,3628],{"emptyLinePlaceholder":3627},[3603,7013,7015,7017],{"class":3605,"line":7014},55,[3603,7016,6935],{"class":5220},[3603,7018,7019],{"class":5224}," \"Object restored successfully!\"\n",[4801,7021,7023,7034,7037,7047,7051,7064,7075,7086,7094,7098,7101,7110,7120,7130,7137,7141,7145,7153],{"title":7022},"aws s3api list-object-versions",[4805,7024,7026,4841,7031],{"className":7025},[3605],[3603,7027,7030],{"className":7028},[7029],"opacity-40","$",[3356,7032,7033],{},"aws s3api list-object-versions --bucket my-app-bucket --prefix \"uploads\u002Favatar.png\" ...",[4805,7035,5618],{"className":7036},[3605],[4805,7038,7040,7041,7046],{"className":7039},[3605],"  ",[3603,7042,7045],{"className":7043},[7044],"text-blue-400","\"Versions\"",": [",[4805,7048,7050],{"className":7049},[3605],"    {",[4805,7052,7054,7055,5881,7059,7063],{"className":7053},[3605],"      ",[3603,7056,7058],{"className":7057},[7044],"\"Key\"",[3603,7060,7062],{"className":7061},[4811],"\"uploads\u002Favatar.png\"",",",[4805,7065,7054,7067,5881,7071,7063],{"className":7066},[3605],[3603,7068,7070],{"className":7069},[7044],"\"VersionId\"",[3603,7072,7074],{"className":7073},[4811],"\"Abc1xZy3mno456pqr\"",[4805,7076,7054,7078,5881,7082,7063],{"className":7077},[3605],[3603,7079,7081],{"className":7080},[7044],"\"IsLatest\"",[3603,7083,6905],{"className":7084},[7085],"text-yellow-400",[4805,7087,7054,7089,7093],{"className":7088},[3605],[3603,7090,7092],{"className":7091},[7044],"\"Size\"",": 91136",[4805,7095,7097],{"className":7096},[3605],"    },",[4805,7099,7050],{"className":7100},[3605],[4805,7102,7054,7104,5881,7107,7063],{"className":7103},[3605],[3603,7105,7058],{"className":7106},[7044],[3603,7108,7062],{"className":7109},[4811],[4805,7111,7054,7113,5881,7116,7063],{"className":7112},[3605],[3603,7114,7070],{"className":7115},[7044],[3603,7117,7119],{"className":7118},[4811],"\"mK8pQrT2xyz789abc\"",[4805,7121,7054,7123,5881,7126,7063],{"className":7122},[3605],[3603,7124,7081],{"className":7125},[7044],[3603,7127,7129],{"className":7128},[4864],"false",[4805,7131,7054,7133,7136],{"className":7132},[3605],[3603,7134,7092],{"className":7135},[7044],": 77824",[4805,7138,7140],{"className":7139},[3605],"    }",[4805,7142,7144],{"className":7143},[3605],"  ],",[4805,7146,7040,7148,7152],{"className":7147},[3605],[3603,7149,7151],{"className":7150},[7044],"\"DeleteMarkers\"",": []",[4805,7154,5645],{"className":7155},[3605],[3879,7157,7158,7159,7162,7163,7166],{},"Версіонування суттєво збільшує витрати на зберігання: кожна версія кожного файлу займає місце і тарифікується окремо. Обов'язково налаштуйте ",[3356,7160,7161],{},"Lifecycle Policy"," з ",[3432,7164,7165],{},"NoncurrentVersionExpiration"," для автоматичного видалення старих версій — наприклад, зберігати не більше 5 попередніх версій і видаляти noncurrent старші 30 днів.",[3405,7168],{},[3348,7170,7172],{"id":7171},"s3-lifecycle-policies-автоматичне-управління-обєктами","S3 Lifecycle Policies — автоматичне управління об'єктами",[3353,7174,7175,7177],{},[3356,7176,7161],{}," — це набір правил, що автоматично переміщують або видаляють об'єкти через певний час. Механізм є ключовим інструментом оптимізації витрат: AWS оцінює правила щоночі і виконує переходи для об'єктів, що відповідають умовам.",[3353,7179,7180,7181,7184,7185,7188,7189,7192],{},"Кожне правило складається з трьох компонентів: ",[3356,7182,7183],{},"фільтру"," (до яких об'єктів застосовується), ",[3356,7186,7187],{},"переходів"," (коли і в який клас перемістити) та ",[3356,7190,7191],{},"закінчення терміну"," (коли видалити).",[3353,7194,7195],{},[3356,7196,7197],{},"Типові сценарії:",[3369,7199,7200,7206,7212,7218],{},[3372,7201,7202,7205],{},[3356,7203,7204],{},"Логи:"," Standard 30 днів → Standard-IA 90 днів → Glacier → видалення через 365 днів",[3372,7207,7208,7211],{},[3356,7209,7210],{},"Резервні копії БД:"," Standard 7 днів → Glacier Flexible 30 днів → Deep Archive 365 днів → видалення через 7 років",[3372,7213,7214,7217],{},[3356,7215,7216],{},"Старі версії при Versioning:"," зберігати поточну + 5 попередніх → видаляти noncurrent старші 30 днів",[3372,7219,7220,7223],{},[3356,7221,7222],{},"Незавершені Multipart Uploads:"," AbortIncompleteMultipartUpload після 7 днів — прибирає «сміття»",[3593,7225,7226],{},[3561,7227,7229],{"className":3597,"code":7228,"language":3599,"meta":3569,"style":3569},"@startuml\nskinparam style plain\nskinparam backgroundColor #ffffff\ntitle \"S3 Lifecycle: автоматична зміна Storage Class (правило для logs\u002F)\"\n\n[*] --> Standard : PUT object\nnote on link\n  День 0: файл завантажено\n  до s3:\u002F\u002Fbucket\u002Flogs\u002F\nend note\n\nStandard --> StandardIA : Day 30\nnote on link\n  Transition: STANDARD_IA\n  Вартість знижується:\n  $0.023 → $0.0125\u002FGB\nend note\n\nStandardIA --> Glacier : Day 90\nnote on link\n  Transition: GLACIER\n  Вартість: $0.0036\u002FGB\nend note\n\nGlacier --> Deleted : Day 365\nnote on link\n  Expiration: Days 365\n  Об'єкт видалено\nend note\n\nDeleted --> [*]\n\nnote \"Noncurrent версії (при Versioning enabled):\\nNoncurrentVersionExpiration:\\n  NoncurrentDays: 30\\n  NewerNoncurrentVersions: 5\\n→ зберігати 5 версій, решту видалити\" as N1\n\n@enduml\n",[3432,7230,7231,7235,7239,7243,7248,7252,7257,7262,7267,7272,7276,7280,7285,7289,7294,7299,7304,7308,7312,7317,7321,7326,7331,7335,7339,7344,7348,7353,7358,7362,7366,7371,7375,7380,7384],{"__ignoreMap":3569},[3603,7232,7233],{"class":3605,"line":3606},[3603,7234,3609],{},[3603,7236,7237],{"class":3605,"line":3612},[3603,7238,3615],{},[3603,7240,7241],{"class":3605,"line":3618},[3603,7242,3621],{},[3603,7244,7245],{"class":3605,"line":3624},[3603,7246,7247],{},"title \"S3 Lifecycle: автоматична зміна Storage Class (правило для logs\u002F)\"\n",[3603,7249,7250],{"class":3605,"line":3631},[3603,7251,3628],{"emptyLinePlaceholder":3627},[3603,7253,7254],{"class":3605,"line":3637},[3603,7255,7256],{},"[*] --> Standard : PUT object\n",[3603,7258,7259],{"class":3605,"line":3643},[3603,7260,7261],{},"note on link\n",[3603,7263,7264],{"class":3605,"line":3649},[3603,7265,7266],{},"  День 0: файл завантажено\n",[3603,7268,7269],{"class":3605,"line":3655},[3603,7270,7271],{},"  до s3:\u002F\u002Fbucket\u002Flogs\u002F\n",[3603,7273,7274],{"class":3605,"line":3661},[3603,7275,3705],{},[3603,7277,7278],{"class":3605,"line":3667},[3603,7279,3628],{"emptyLinePlaceholder":3627},[3603,7281,7282],{"class":3605,"line":3673},[3603,7283,7284],{},"Standard --> StandardIA : Day 30\n",[3603,7286,7287],{"class":3605,"line":3678},[3603,7288,7261],{},[3603,7290,7291],{"class":3605,"line":3684},[3603,7292,7293],{},"  Transition: STANDARD_IA\n",[3603,7295,7296],{"class":3605,"line":3690},[3603,7297,7298],{},"  Вартість знижується:\n",[3603,7300,7301],{"class":3605,"line":3696},[3603,7302,7303],{},"  $0.023 → $0.0125\u002FGB\n",[3603,7305,7306],{"class":3605,"line":3702},[3603,7307,3705],{},[3603,7309,7310],{"class":3605,"line":3708},[3603,7311,3628],{"emptyLinePlaceholder":3627},[3603,7313,7314],{"class":3605,"line":4667},[3603,7315,7316],{},"StandardIA --> Glacier : Day 90\n",[3603,7318,7319],{"class":3605,"line":4673},[3603,7320,7261],{},[3603,7322,7323],{"class":3605,"line":4679},[3603,7324,7325],{},"  Transition: GLACIER\n",[3603,7327,7328],{"class":3605,"line":4685},[3603,7329,7330],{},"  Вартість: $0.0036\u002FGB\n",[3603,7332,7333],{"class":3605,"line":4691},[3603,7334,3705],{},[3603,7336,7337],{"class":3605,"line":4696},[3603,7338,3628],{"emptyLinePlaceholder":3627},[3603,7340,7341],{"class":3605,"line":4701},[3603,7342,7343],{},"Glacier --> Deleted : Day 365\n",[3603,7345,7346],{"class":3605,"line":4707},[3603,7347,7261],{},[3603,7349,7350],{"class":3605,"line":4713},[3603,7351,7352],{},"  Expiration: Days 365\n",[3603,7354,7355],{"class":3605,"line":4719},[3603,7356,7357],{},"  Об'єкт видалено\n",[3603,7359,7360],{"class":3605,"line":4724},[3603,7361,3705],{},[3603,7363,7364],{"class":3605,"line":4730},[3603,7365,3628],{"emptyLinePlaceholder":3627},[3603,7367,7368],{"class":3605,"line":4736},[3603,7369,7370],{},"Deleted --> [*]\n",[3603,7372,7373],{"class":3605,"line":4742},[3603,7374,3628],{"emptyLinePlaceholder":3627},[3603,7376,7377],{"class":3605,"line":4748},[3603,7378,7379],{},"note \"Noncurrent версії (при Versioning enabled):\\nNoncurrentVersionExpiration:\\n  NoncurrentDays: 30\\n  NewerNoncurrentVersions: 5\\n→ зберігати 5 версій, решту видалити\" as N1\n",[3603,7381,7382],{"class":3605,"line":4753},[3603,7383,3628],{"emptyLinePlaceholder":3627},[3603,7385,7386],{"class":3605,"line":4758},[3603,7387,3711],{},[3353,7389,7390],{},[3356,7391,7392],{},"Структура конфігурації Lifecycle Policy:",[3561,7394,7396],{"className":5851,"code":7395,"language":5853,"meta":3569,"style":3569},"{\n    \"Rules\": [\n        {\n            \"ID\": \"LogsLifecycle\",\n            \"Status\": \"Enabled\",\n            \"Filter\": { \"Prefix\": \"logs\u002F\" },\n            \"Transitions\": [\n                { \"Days\": 30, \"StorageClass\": \"STANDARD_IA\" },\n                { \"Days\": 90, \"StorageClass\": \"GLACIER\" }\n            ],\n            \"Expiration\": { \"Days\": 365 },\n            \"NoncurrentVersionExpiration\": {\n                \"NoncurrentDays\": 30,\n                \"NewerNoncurrentVersions\": 5\n            },\n            \"AbortIncompleteMultipartUpload\": {\n                \"DaysAfterInitiation\": 7\n            }\n        }\n    ]\n}\n",[3432,7397,7398,7402,7408,7412,7423,7433,7451,7457,7481,7503,7507,7521,7528,7539,7549,7553,7560,7570,7574,7578,7582],{"__ignoreMap":3569},[3603,7399,7400],{"class":3605,"line":3606},[3603,7401,5591],{"class":5541},[3603,7403,7404,7406],{"class":3605,"line":3612},[3603,7405,5865],{"class":5864},[3603,7407,5868],{"class":5541},[3603,7409,7410],{"class":3605,"line":3618},[3603,7411,5873],{"class":5541},[3603,7413,7414,7416,7418,7421],{"class":3605,"line":3624},[3603,7415,5878],{"class":5864},[3603,7417,5881],{"class":5541},[3603,7419,7420],{"class":5224},"\"LogsLifecycle\"",[3603,7422,5604],{"class":5541},[3603,7424,7425,7427,7429,7431],{"class":3605,"line":3631},[3603,7426,5891],{"class":5864},[3603,7428,5881],{"class":5541},[3603,7430,5896],{"class":5224},[3603,7432,5604],{"class":5541},[3603,7434,7435,7437,7440,7443,7445,7448],{"class":3605,"line":3637},[3603,7436,5903],{"class":5864},[3603,7438,7439],{"class":5541},": { ",[3603,7441,7442],{"class":5864},"\"Prefix\"",[3603,7444,5881],{"class":5541},[3603,7446,7447],{"class":5224},"\"logs\u002F\"",[3603,7449,7450],{"class":5541}," },\n",[3603,7452,7453,7455],{"class":3605,"line":3643},[3603,7454,5926],{"class":5864},[3603,7456,5868],{"class":5541},[3603,7458,7459,7462,7465,7467,7469,7471,7474,7476,7479],{"class":3605,"line":3649},[3603,7460,7461],{"class":5541},"                { ",[3603,7463,7464],{"class":5864},"\"Days\"",[3603,7466,5881],{"class":5541},[3603,7468,5944],{"class":5943},[3603,7470,3470],{"class":5541},[3603,7472,7473],{"class":5864},"\"StorageClass\"",[3603,7475,5881],{"class":5541},[3603,7477,7478],{"class":5224},"\"STANDARD_IA\"",[3603,7480,7450],{"class":5541},[3603,7482,7483,7485,7487,7489,7491,7493,7495,7497,7500],{"class":3605,"line":3655},[3603,7484,7461],{"class":5541},[3603,7486,7464],{"class":5864},[3603,7488,5881],{"class":5541},[3603,7490,5974],{"class":5943},[3603,7492,3470],{"class":5541},[3603,7494,7473],{"class":5864},[3603,7496,5881],{"class":5541},[3603,7498,7499],{"class":5224},"\"GLACIER\"",[3603,7501,7502],{"class":5541}," }\n",[3603,7504,7505],{"class":3605,"line":3661},[3603,7506,6023],{"class":5541},[3603,7508,7509,7511,7513,7515,7517,7519],{"class":3605,"line":3667},[3603,7510,6028],{"class":5864},[3603,7512,7439],{"class":5541},[3603,7514,7464],{"class":5864},[3603,7516,5881],{"class":5541},[3603,7518,6002],{"class":5943},[3603,7520,7450],{"class":5541},[3603,7522,7523,7526],{"class":3605,"line":3673},[3603,7524,7525],{"class":5864},"            \"NoncurrentVersionExpiration\"",[3603,7527,5906],{"class":5541},[3603,7529,7530,7533,7535,7537],{"class":3605,"line":3678},[3603,7531,7532],{"class":5864},"                \"NoncurrentDays\"",[3603,7534,5881],{"class":5541},[3603,7536,5944],{"class":5943},[3603,7538,5604],{"class":5541},[3603,7540,7541,7544,7546],{"class":3605,"line":3684},[3603,7542,7543],{"class":5864},"                \"NewerNoncurrentVersions\"",[3603,7545,5881],{"class":5541},[3603,7547,7548],{"class":5943},"5\n",[3603,7550,7551],{"class":3605,"line":3690},[3603,7552,5921],{"class":5541},[3603,7554,7555,7558],{"class":3605,"line":3696},[3603,7556,7557],{"class":5864},"            \"AbortIncompleteMultipartUpload\"",[3603,7559,5906],{"class":5541},[3603,7561,7562,7565,7567],{"class":3605,"line":3702},[3603,7563,7564],{"class":5864},"                \"DaysAfterInitiation\"",[3603,7566,5881],{"class":5541},[3603,7568,7569],{"class":5943},"7\n",[3603,7571,7572],{"class":3605,"line":3708},[3603,7573,6045],{"class":5541},[3603,7575,7576],{"class":3605,"line":4667},[3603,7577,6050],{"class":5541},[3603,7579,7580],{"class":3605,"line":4673},[3603,7581,6055],{"class":5541},[3603,7583,7584],{"class":3605,"line":4679},[3603,7585,3670],{"class":5541},[3353,7587,7588],{},[3356,7589,7590],{},"Документація полів правила:",[7592,7593,7594,7600,7611,7628,7656,7668,7679],"field-group",{},[7595,7596,7599],"field",{"name":7597,"type":7598,"required":6905},"ID","string","Унікальний ідентифікатор правила в межах bucket. Максимум 255 символів. Використовується для ідентифікації у звітах та AWS CLI.",[7595,7601,7603,7604,7606,7607,7610],{"name":7602,"type":7598,"required":6905,"default":6130},"Status","Стан правила: ",[3432,7605,6130],{}," — активне, ",[3432,7608,7609],{},"Disabled"," — призупинене. Дозволяє тимчасово вимкнути правило без видалення.",[7595,7612,7615,7616,7619,7620,7623,7624,7627],{"name":7613,"type":7614},"Filter","object","Фільтр об'єктів: ",[3432,7617,7618],{},"{\"Prefix\": \"logs\u002F\"}"," — за префіксом ключа; ",[3432,7621,7622],{},"{\"Tag\": {\"Key\": \"env\", \"Value\": \"prod\"}}"," — за тегом; ",[3432,7625,7626],{},"{}"," — всі об'єкти в bucket.",[7595,7629,7632,7633,7636,7637,7640,7641,3470,7644,3470,7647,3470,7650,3470,7653,3453],{"name":7630,"type":7631},"Transitions","array","Масив переходів між Storage Class. Поле ",[3432,7634,7635],{},"Days"," — кількість днів від дати створення об'єкту (не від останнього доступу). ",[3432,7638,7639],{},"StorageClass"," — цільовий клас: ",[3432,7642,7643],{},"STANDARD_IA",[3432,7645,7646],{},"ONEZONE_IA",[3432,7648,7649],{},"INTELLIGENT_TIERING",[3432,7651,7652],{},"GLACIER",[3432,7654,7655],{},"DEEP_ARCHIVE",[7595,7657,7659,7660,7663,7664,7667],{"name":7658,"type":7614},"Expiration","Автоматичне видалення поточних версій. ",[3432,7661,7662],{},"{\"Days\": 365}"," — через 365 днів. ",[3432,7665,7666],{},"{\"ExpiredObjectDeleteMarker\": true}"," — видаляти Delete Markers без noncurrent версій.",[7595,7669,7670,7671,7674,7675,7678],{"name":7165,"type":7614},"Управління старими версіями при увімкненому Versioning. ",[3432,7672,7673],{},"NoncurrentDays"," — зберігати не більше N днів. ",[3432,7676,7677],{},"NewerNoncurrentVersions"," — зберігати не більше N попередніх версій.",[7595,7680,7682,7685],{"name":7681,"type":7614},"AbortIncompleteMultipartUpload",[3432,7683,7684],{},"{\"DaysAfterInitiation\": 7}"," — автоматично скасовувати та видаляти незавершені multipart uploads старші 7 днів. Критично важливо для уникнення непомітних витрат на неповні завантаження.",[5197,7687,7688,7795],{},[5200,7689,7690],{"label":6375},[6382,7691,7692,7698,7707,7715,7727,7751,7771,7782,7790],{},[3372,7693,7694,7695],{},"S3 → оберіть bucket → вкладка ",[3356,7696,7697],{},"Management",[3372,7699,7700,7701,3976,7704],{},"У секції ",[3356,7702,7703],{},"Lifecycle rules",[3356,7705,7706],{},"Create lifecycle rule",[3372,7708,7709,4841,7712],{},[3356,7710,7711],{},"Rule name:",[3432,7713,7714],{},"LogsLifecycle",[3372,7716,7717,4841,7720,7723,7724],{},[3356,7718,7719],{},"Choose a rule scope:",[3432,7721,7722],{},"Limit the scope to specific prefixes or tags"," → вкажіть prefix ",[3432,7725,7726],{},"logs\u002F",[3372,7728,7729,7732,7733],{},[3356,7730,7731],{},"Lifecycle rule actions:"," оберіть:\n",[3369,7734,7735,7741,7746],{},[3372,7736,7737,7738],{},"✅ ",[3356,7739,7740],{},"Transition current versions of objects between storage classes",[3372,7742,7737,7743],{},[3356,7744,7745],{},"Expire current versions of objects",[3372,7747,7737,7748],{},[3356,7749,7750],{},"Delete expired object delete markers or incomplete multipart uploads",[3372,7752,7753,7754,7757,7758],{},"У ",[3356,7755,7756],{},"Transition current versions:"," додайте два переходи:\n",[3369,7759,7760,7766],{},[3372,7761,7762,7763,7765],{},"Standard-IA after ",[3356,7764,5944],{}," days",[3372,7767,7768,7769,7765],{},"Glacier Flexible Retrieval after ",[3356,7770,5974],{},[3372,7772,7773,7776,7777,7779,7780],{},[3356,7774,7775],{},"Expiration:"," ✅ → ",[3356,7778,5944],{}," days after object creation → enter ",[3432,7781,6002],{},[3372,7783,7784,3976,7787,7765],{},[3356,7785,7786],{},"Delete incomplete multipart uploads",[3356,7788,7789],{},"7",[3372,7791,7792],{},[3356,7793,7794],{},"Create rule",[5200,7796,7797],{"label":6494},[3561,7798,7800],{"className":5205,"code":7799,"language":5207,"meta":3569,"style":3569},"BUCKET=\"my-app-bucket\"\nREGION=\"eu-central-1\"\n\n# Зберегти конфігурацію у файл\ncat > \u002Ftmp\u002Flifecycle.json \u003C\u003C 'EOF'\n{\n    \"Rules\": [\n        {\n            \"ID\": \"LogsLifecycle\",\n            \"Status\": \"Enabled\",\n            \"Filter\": {\"Prefix\": \"logs\u002F\"},\n            \"Transitions\": [\n                {\"Days\": 30, \"StorageClass\": \"STANDARD_IA\"},\n                {\"Days\": 90, \"StorageClass\": \"GLACIER\"}\n            ],\n            \"Expiration\": {\"Days\": 365},\n            \"NoncurrentVersionExpiration\": {\n                \"NoncurrentDays\": 30,\n                \"NewerNoncurrentVersions\": 5\n            },\n            \"AbortIncompleteMultipartUpload\": {\n                \"DaysAfterInitiation\": 7\n            }\n        }\n    ]\n}\nEOF\n\n# Застосувати Lifecycle Policy\naws s3api put-bucket-lifecycle-configuration \\\n    --bucket \"$BUCKET\" \\\n    --lifecycle-configuration file:\u002F\u002F\u002Ftmp\u002Flifecycle.json \\\n    --region \"$REGION\"\n\n# Переглянути поточні правила\naws s3api get-bucket-lifecycle-configuration \\\n    --bucket \"$BUCKET\" \\\n    --region \"$REGION\"\n\n# Видалити всі lifecycle правила\naws s3api delete-bucket-lifecycle \\\n    --bucket \"$BUCKET\" \\\n    --region \"$REGION\"\n",[3432,7801,7802,7810,7818,7822,7827,7844,7848,7853,7857,7862,7867,7872,7877,7882,7887,7891,7896,7901,7906,7911,7915,7920,7925,7929,7933,7937,7941,7946,7950,7955,7966,7978,7988,7998,8002,8007,8018,8030,8040,8044,8049,8060,8072],{"__ignoreMap":3569},[3603,7803,7804,7806,7808],{"class":3605,"line":3606},[3603,7805,6504],{"class":5576},[3603,7807,6507],{"class":5541},[3603,7809,6510],{"class":5224},[3603,7811,7812,7814,7816],{"class":3605,"line":3612},[3603,7813,6515],{"class":5576},[3603,7815,6507],{"class":5541},[3603,7817,6520],{"class":5224},[3603,7819,7820],{"class":3605,"line":3618},[3603,7821,3628],{"emptyLinePlaceholder":3627},[3603,7823,7824],{"class":3605,"line":3624},[3603,7825,7826],{"class":5214},"# Зберегти конфігурацію у файл\n",[3603,7828,7829,7832,7835,7838,7841],{"class":3605,"line":3631},[3603,7830,7831],{"class":5220},"cat",[3603,7833,7834],{"class":5541}," > ",[3603,7836,7837],{"class":5224},"\u002Ftmp\u002Flifecycle.json",[3603,7839,7840],{"class":5541}," \u003C\u003C ",[3603,7842,7843],{"class":5541},"'EOF'\n",[3603,7845,7846],{"class":3605,"line":3637},[3603,7847,5591],{"class":5224},[3603,7849,7850],{"class":3605,"line":3643},[3603,7851,7852],{"class":5224},"    \"Rules\": [\n",[3603,7854,7855],{"class":3605,"line":3649},[3603,7856,5873],{"class":5224},[3603,7858,7859],{"class":3605,"line":3655},[3603,7860,7861],{"class":5224},"            \"ID\": \"LogsLifecycle\",\n",[3603,7863,7864],{"class":3605,"line":3661},[3603,7865,7866],{"class":5224},"            \"Status\": \"Enabled\",\n",[3603,7868,7869],{"class":3605,"line":3667},[3603,7870,7871],{"class":5224},"            \"Filter\": {\"Prefix\": \"logs\u002F\"},\n",[3603,7873,7874],{"class":3605,"line":3673},[3603,7875,7876],{"class":5224},"            \"Transitions\": [\n",[3603,7878,7879],{"class":3605,"line":3678},[3603,7880,7881],{"class":5224},"                {\"Days\": 30, \"StorageClass\": \"STANDARD_IA\"},\n",[3603,7883,7884],{"class":3605,"line":3684},[3603,7885,7886],{"class":5224},"                {\"Days\": 90, \"StorageClass\": \"GLACIER\"}\n",[3603,7888,7889],{"class":3605,"line":3690},[3603,7890,6023],{"class":5224},[3603,7892,7893],{"class":3605,"line":3696},[3603,7894,7895],{"class":5224},"            \"Expiration\": {\"Days\": 365},\n",[3603,7897,7898],{"class":3605,"line":3702},[3603,7899,7900],{"class":5224},"            \"NoncurrentVersionExpiration\": {\n",[3603,7902,7903],{"class":3605,"line":3708},[3603,7904,7905],{"class":5224},"                \"NoncurrentDays\": 30,\n",[3603,7907,7908],{"class":3605,"line":4667},[3603,7909,7910],{"class":5224},"                \"NewerNoncurrentVersions\": 5\n",[3603,7912,7913],{"class":3605,"line":4673},[3603,7914,5921],{"class":5224},[3603,7916,7917],{"class":3605,"line":4679},[3603,7918,7919],{"class":5224},"            \"AbortIncompleteMultipartUpload\": {\n",[3603,7921,7922],{"class":3605,"line":4685},[3603,7923,7924],{"class":5224},"                \"DaysAfterInitiation\": 7\n",[3603,7926,7927],{"class":3605,"line":4691},[3603,7928,6045],{"class":5224},[3603,7930,7931],{"class":3605,"line":4696},[3603,7932,6050],{"class":5224},[3603,7934,7935],{"class":3605,"line":4701},[3603,7936,6055],{"class":5224},[3603,7938,7939],{"class":3605,"line":4707},[3603,7940,3670],{"class":5224},[3603,7942,7943],{"class":3605,"line":4713},[3603,7944,7945],{"class":5541},"EOF\n",[3603,7947,7948],{"class":3605,"line":4719},[3603,7949,3628],{"emptyLinePlaceholder":3627},[3603,7951,7952],{"class":3605,"line":4724},[3603,7953,7954],{"class":5214},"# Застосувати Lifecycle Policy\n",[3603,7956,7957,7959,7961,7964],{"class":3605,"line":4730},[3603,7958,5221],{"class":5220},[3603,7960,5365],{"class":5224},[3603,7962,7963],{"class":5224}," put-bucket-lifecycle-configuration",[3603,7965,5238],{"class":5237},[3603,7967,7968,7970,7972,7974,7976],{"class":3605,"line":4736},[3603,7969,6545],{"class":5243},[3603,7971,6548],{"class":5224},[3603,7973,6551],{"class":5576},[3603,7975,6554],{"class":5224},[3603,7977,5238],{"class":5237},[3603,7979,7980,7983,7986],{"class":3605,"line":4742},[3603,7981,7982],{"class":5243},"    --lifecycle-configuration",[3603,7984,7985],{"class":5224}," file:\u002F\u002F\u002Ftmp\u002Flifecycle.json",[3603,7987,5238],{"class":5237},[3603,7989,7990,7992,7994,7996],{"class":3605,"line":4748},[3603,7991,6571],{"class":5243},[3603,7993,6548],{"class":5224},[3603,7995,6576],{"class":5576},[3603,7997,6579],{"class":5224},[3603,7999,8000],{"class":3605,"line":4753},[3603,8001,3628],{"emptyLinePlaceholder":3627},[3603,8003,8004],{"class":3605,"line":4758},[3603,8005,8006],{"class":5214},"# Переглянути поточні правила\n",[3603,8008,8009,8011,8013,8016],{"class":3605,"line":6363},[3603,8010,5221],{"class":5220},[3603,8012,5365],{"class":5224},[3603,8014,8015],{"class":5224}," get-bucket-lifecycle-configuration",[3603,8017,5238],{"class":5237},[3603,8019,8020,8022,8024,8026,8028],{"class":3605,"line":6368},[3603,8021,6545],{"class":5243},[3603,8023,6548],{"class":5224},[3603,8025,6551],{"class":5576},[3603,8027,6554],{"class":5224},[3603,8029,5238],{"class":5237},[3603,8031,8032,8034,8036,8038],{"class":3605,"line":6830},[3603,8033,6571],{"class":5243},[3603,8035,6548],{"class":5224},[3603,8037,6576],{"class":5576},[3603,8039,6579],{"class":5224},[3603,8041,8042],{"class":3605,"line":6835},[3603,8043,3628],{"emptyLinePlaceholder":3627},[3603,8045,8046],{"class":3605,"line":6841},[3603,8047,8048],{"class":5214},"# Видалити всі lifecycle правила\n",[3603,8050,8051,8053,8055,8058],{"class":3605,"line":6858},[3603,8052,5221],{"class":5220},[3603,8054,5365],{"class":5224},[3603,8056,8057],{"class":5224}," delete-bucket-lifecycle",[3603,8059,5238],{"class":5237},[3603,8061,8062,8064,8066,8068,8070],{"class":3605,"line":6871},[3603,8063,6545],{"class":5243},[3603,8065,6548],{"class":5224},[3603,8067,6551],{"class":5576},[3603,8069,6554],{"class":5224},[3603,8071,5238],{"class":5237},[3603,8073,8074,8076,8078,8080],{"class":3605,"line":6881},[3603,8075,6571],{"class":5243},[3603,8077,6548],{"class":5224},[3603,8079,6576],{"class":5576},[3603,8081,6579],{"class":5224},[4801,8083,8085,8094,8097,8104,8107,8117,8127,8141,8148,8164,8180,8184,8197,8200,8204],{"title":8084},"aws s3api get-bucket-lifecycle-configuration",[4805,8086,8088,4841,8091],{"className":8087},[3605],[3603,8089,7030],{"className":8090},[7029],[3356,8092,8093],{},"aws s3api get-bucket-lifecycle-configuration --bucket my-app-bucket --region eu-central-1",[4805,8095,5618],{"className":8096},[3605],[4805,8098,7040,8100,7046],{"className":8099},[3605],[3603,8101,8103],{"className":8102},[7044],"\"Rules\"",[4805,8105,7050],{"className":8106},[3605],[4805,8108,7054,8110,5881,8114,7063],{"className":8109},[3605],[3603,8111,8113],{"className":8112},[7044],"\"ID\"",[3603,8115,7420],{"className":8116},[4811],[4805,8118,7054,8120,5881,8124,7063],{"className":8119},[3605],[3603,8121,8123],{"className":8122},[7044],"\"Status\"",[3603,8125,5896],{"className":8126},[4811],[4805,8128,7054,8130,7439,8134,5881,8137,8140],{"className":8129},[3605],[3603,8131,8133],{"className":8132},[7044],"\"Filter\"",[3603,8135,7442],{"className":8136},[7044],[3603,8138,7447],{"className":8139},[4811]," },",[4805,8142,7054,8144,7046],{"className":8143},[3605],[3603,8145,8147],{"className":8146},[7044],"\"Transitions\"",[4805,8149,8151,8152,5881,8155,3470,8158,5881,8161,8140],{"className":8150},[3605],"        { ",[3603,8153,7464],{"className":8154},[7044],[3603,8156,5944],{"className":8157},[7085],[3603,8159,7473],{"className":8160},[7044],[3603,8162,7478],{"className":8163},[4811],[4805,8165,8151,8167,5881,8170,3470,8173,5881,8176,8179],{"className":8166},[3605],[3603,8168,7464],{"className":8169},[7044],[3603,8171,5974],{"className":8172},[7085],[3603,8174,7473],{"className":8175},[7044],[3603,8177,7499],{"className":8178},[4811]," }",[4805,8181,8183],{"className":8182},[3605],"      ],",[4805,8185,7054,8187,7439,8191,5881,8194,8179],{"className":8186},[3605],[3603,8188,8190],{"className":8189},[7044],"\"Expiration\"",[3603,8192,7464],{"className":8193},[7044],[3603,8195,6002],{"className":8196},[7085],[4805,8198,7140],{"className":8199},[3605],[4805,8201,8203],{"className":8202},[3605],"  ]",[4805,8205,5645],{"className":8206},[3605],[3405,8208],{},[3348,8210,8212],{"id":8211},"s3-security-безпека-та-контроль-доступу","S3 Security — безпека та контроль доступу",[3353,8214,8215,8216,8219],{},"Безпека Amazon S3 реалізована у вигляді ",[3356,8217,8218],{},"багаторівневої моделі захисту",". Кожен рівень є незалежним і може застосовуватись окремо або у комбінації з іншими. Помилки в налаштуванні S3 bucket стали причиною гучних витоків даних (Capital One, GoDaddy, NASA та інших організацій), тому розуміння всіх рівнів є критично необхідним.",[3353,8221,8222],{},[6142,8223],{"alt":8224,"className":8225,"src":8226},"Amazon S3 Block Public Access settings screenshot",[6146],"\u002Fimages\u002Faws\u002Fs3\u002F04.png",[3593,8228,8229],{},[3561,8230,8232],{"className":3597,"code":8231,"language":3599,"meta":3569,"style":3569},"@startuml\nskinparam style plain\nskinparam backgroundColor #ffffff\ntitle \"S3: багаторівнева модель захисту доступу\"\n\nactor \"Зовнішній\\nкористувач\" as EXT #fca5a5\nactor \"IAM User\u002FRole\\n(внутрішній)\" as IAM #bbf7d0\n\nrectangle \"AWS Account\" as ACC {\n    rectangle \"Рівень 1: Block Public Access\" as L1 #fce7f3 {\n        note as N1\n          BlockPublicPolicy=true\n          RestrictPublicBuckets=true\n          IgnorePublicAcls=true\n          BlockPublicAcls=true\n          ───────────────────────────\n          Перевизначає ВСІ інші\n          дозволи на публічний доступ\n        end note\n    }\n\n    rectangle \"Рівень 2: Bucket Policy (Resource-based)\" as L2 #dbeafe {\n        note as N2\n          Principal: *, Role ARN, Account\n          Action: s3:GetObject, s3:PutObject...\n          Condition: IP, VPC, MFA, Tags\n          ───────────────────────────\n          Прикріплена до bucket.\n          Може дозволяти крос-акаунтний доступ\n        end note\n    }\n\n    rectangle \"Рівень 3: IAM Policy (Identity-based)\" as L3 #d1fae5 {\n        note as N3\n          Прикріплена до User\u002FRole\u002FGroup.\n          Оцінюється одночасно з Bucket Policy.\n          Діє принцип LEAST PRIVILEGE:\n          Allow потрібен в ОБОХ або в одному\n          (якщо Principal — той самий акаунт)\n        end note\n    }\n\n    rectangle \"Рівень 4: Encryption at Rest\" as L4 #fef3c7 {\n        note as N4\n          SSE-S3 (AES-256, ключ AWS)\n          SSE-KMS (ваш ключ в KMS)\n          SSE-C (ваш ключ per-request)\n          ───────────────────────────\n          Шифрує дані фізично на диску\n        end note\n    }\n\n    rectangle \"S3 Bucket\" as BKT #e5e7eb\n}\n\nEXT -down-> L1 : HTTP\u002FHTTPS запит\nL1 -down-> L2 : (якщо не заблоковано)\nL2 -down-> L3 : (якщо Policy дозволяє)\nL3 -down-> L4 : (якщо IAM дозволяє)\nL4 -down-> BKT : Розшифровані дані\nIAM -down-> L2\n\n@enduml\n",[3432,8233,8234,8238,8242,8246,8251,8255,8260,8265,8269,8274,8279,8284,8289,8294,8299,8304,8309,8314,8319,8324,8328,8332,8337,8342,8347,8352,8357,8361,8366,8371,8375,8379,8383,8388,8393,8398,8403,8408,8413,8418,8422,8426,8430,8435,8440,8445,8450,8455,8459,8464,8468,8472,8476,8481,8485,8489,8495,8501,8507,8513,8519,8525,8530],{"__ignoreMap":3569},[3603,8235,8236],{"class":3605,"line":3606},[3603,8237,3609],{},[3603,8239,8240],{"class":3605,"line":3612},[3603,8241,3615],{},[3603,8243,8244],{"class":3605,"line":3618},[3603,8245,3621],{},[3603,8247,8248],{"class":3605,"line":3624},[3603,8249,8250],{},"title \"S3: багаторівнева модель захисту доступу\"\n",[3603,8252,8253],{"class":3605,"line":3631},[3603,8254,3628],{"emptyLinePlaceholder":3627},[3603,8256,8257],{"class":3605,"line":3637},[3603,8258,8259],{},"actor \"Зовнішній\\nкористувач\" as EXT #fca5a5\n",[3603,8261,8262],{"class":3605,"line":3643},[3603,8263,8264],{},"actor \"IAM User\u002FRole\\n(внутрішній)\" as IAM #bbf7d0\n",[3603,8266,8267],{"class":3605,"line":3649},[3603,8268,3628],{"emptyLinePlaceholder":3627},[3603,8270,8271],{"class":3605,"line":3655},[3603,8272,8273],{},"rectangle \"AWS Account\" as ACC {\n",[3603,8275,8276],{"class":3605,"line":3661},[3603,8277,8278],{},"    rectangle \"Рівень 1: Block Public Access\" as L1 #fce7f3 {\n",[3603,8280,8281],{"class":3605,"line":3667},[3603,8282,8283],{},"        note as N1\n",[3603,8285,8286],{"class":3605,"line":3673},[3603,8287,8288],{},"          BlockPublicPolicy=true\n",[3603,8290,8291],{"class":3605,"line":3678},[3603,8292,8293],{},"          RestrictPublicBuckets=true\n",[3603,8295,8296],{"class":3605,"line":3684},[3603,8297,8298],{},"          IgnorePublicAcls=true\n",[3603,8300,8301],{"class":3605,"line":3690},[3603,8302,8303],{},"          BlockPublicAcls=true\n",[3603,8305,8306],{"class":3605,"line":3696},[3603,8307,8308],{},"          ───────────────────────────\n",[3603,8310,8311],{"class":3605,"line":3702},[3603,8312,8313],{},"          Перевизначає ВСІ інші\n",[3603,8315,8316],{"class":3605,"line":3708},[3603,8317,8318],{},"          дозволи на публічний доступ\n",[3603,8320,8321],{"class":3605,"line":4667},[3603,8322,8323],{},"        end note\n",[3603,8325,8326],{"class":3605,"line":4673},[3603,8327,3664],{},[3603,8329,8330],{"class":3605,"line":4679},[3603,8331,3628],{"emptyLinePlaceholder":3627},[3603,8333,8334],{"class":3605,"line":4685},[3603,8335,8336],{},"    rectangle \"Рівень 2: Bucket Policy (Resource-based)\" as L2 #dbeafe {\n",[3603,8338,8339],{"class":3605,"line":4691},[3603,8340,8341],{},"        note as N2\n",[3603,8343,8344],{"class":3605,"line":4696},[3603,8345,8346],{},"          Principal: *, Role ARN, Account\n",[3603,8348,8349],{"class":3605,"line":4701},[3603,8350,8351],{},"          Action: s3:GetObject, s3:PutObject...\n",[3603,8353,8354],{"class":3605,"line":4707},[3603,8355,8356],{},"          Condition: IP, VPC, MFA, Tags\n",[3603,8358,8359],{"class":3605,"line":4713},[3603,8360,8308],{},[3603,8362,8363],{"class":3605,"line":4719},[3603,8364,8365],{},"          Прикріплена до bucket.\n",[3603,8367,8368],{"class":3605,"line":4724},[3603,8369,8370],{},"          Може дозволяти крос-акаунтний доступ\n",[3603,8372,8373],{"class":3605,"line":4730},[3603,8374,8323],{},[3603,8376,8377],{"class":3605,"line":4736},[3603,8378,3664],{},[3603,8380,8381],{"class":3605,"line":4742},[3603,8382,3628],{"emptyLinePlaceholder":3627},[3603,8384,8385],{"class":3605,"line":4748},[3603,8386,8387],{},"    rectangle \"Рівень 3: IAM Policy (Identity-based)\" as L3 #d1fae5 {\n",[3603,8389,8390],{"class":3605,"line":4753},[3603,8391,8392],{},"        note as N3\n",[3603,8394,8395],{"class":3605,"line":4758},[3603,8396,8397],{},"          Прикріплена до User\u002FRole\u002FGroup.\n",[3603,8399,8400],{"class":3605,"line":6363},[3603,8401,8402],{},"          Оцінюється одночасно з Bucket Policy.\n",[3603,8404,8405],{"class":3605,"line":6368},[3603,8406,8407],{},"          Діє принцип LEAST PRIVILEGE:\n",[3603,8409,8410],{"class":3605,"line":6830},[3603,8411,8412],{},"          Allow потрібен в ОБОХ або в одному\n",[3603,8414,8415],{"class":3605,"line":6835},[3603,8416,8417],{},"          (якщо Principal — той самий акаунт)\n",[3603,8419,8420],{"class":3605,"line":6841},[3603,8421,8323],{},[3603,8423,8424],{"class":3605,"line":6858},[3603,8425,3664],{},[3603,8427,8428],{"class":3605,"line":6871},[3603,8429,3628],{"emptyLinePlaceholder":3627},[3603,8431,8432],{"class":3605,"line":6881},[3603,8433,8434],{},"    rectangle \"Рівень 4: Encryption at Rest\" as L4 #fef3c7 {\n",[3603,8436,8437],{"class":3605,"line":6894},[3603,8438,8439],{},"        note as N4\n",[3603,8441,8442],{"class":3605,"line":6915},[3603,8443,8444],{},"          SSE-S3 (AES-256, ключ AWS)\n",[3603,8446,8447],{"class":3605,"line":6927},[3603,8448,8449],{},"          SSE-KMS (ваш ключ в KMS)\n",[3603,8451,8452],{"class":3605,"line":6932},[3603,8453,8454],{},"          SSE-C (ваш ключ per-request)\n",[3603,8456,8457],{"class":3605,"line":6946},[3603,8458,8308],{},[3603,8460,8461],{"class":3605,"line":6951},[3603,8462,8463],{},"          Шифрує дані фізично на диску\n",[3603,8465,8466],{"class":3605,"line":6963},[3603,8467,8323],{},[3603,8469,8470],{"class":3605,"line":6976},[3603,8471,3664],{},[3603,8473,8474],{"class":3605,"line":6985},[3603,8475,3628],{"emptyLinePlaceholder":3627},[3603,8477,8478],{"class":3605,"line":6998},[3603,8479,8480],{},"    rectangle \"S3 Bucket\" as BKT #e5e7eb\n",[3603,8482,8483],{"class":3605,"line":7009},[3603,8484,3670],{},[3603,8486,8487],{"class":3605,"line":7014},[3603,8488,3628],{"emptyLinePlaceholder":3627},[3603,8490,8492],{"class":3605,"line":8491},56,[3603,8493,8494],{},"EXT -down-> L1 : HTTP\u002FHTTPS запит\n",[3603,8496,8498],{"class":3605,"line":8497},57,[3603,8499,8500],{},"L1 -down-> L2 : (якщо не заблоковано)\n",[3603,8502,8504],{"class":3605,"line":8503},58,[3603,8505,8506],{},"L2 -down-> L3 : (якщо Policy дозволяє)\n",[3603,8508,8510],{"class":3605,"line":8509},59,[3603,8511,8512],{},"L3 -down-> L4 : (якщо IAM дозволяє)\n",[3603,8514,8516],{"class":3605,"line":8515},60,[3603,8517,8518],{},"L4 -down-> BKT : Розшифровані дані\n",[3603,8520,8522],{"class":3605,"line":8521},61,[3603,8523,8524],{},"IAM -down-> L2\n",[3603,8526,8528],{"class":3605,"line":8527},62,[3603,8529,3628],{"emptyLinePlaceholder":3627},[3603,8531,8533],{"class":3605,"line":8532},63,[3603,8534,3711],{},[3412,8536,8538],{"id":8537},"block-public-access-перший-захист","Block Public Access — перший захист",[3353,8540,8541,8544,8545,8548,8549,3453],{},[3356,8542,8543],{},"Block Public Access"," — це налаштування на рівні bucket та на рівні AWS-акаунту, яке ",[3356,8546,8547],{},"перевизначає"," всі інші дозволи та гарантує, що bucket залишається приватним, навіть якщо існує Bucket Policy, що явно дозволяє публічний доступ. Цей механізм є ",[3356,8550,8551],{},"першою і найважливішою лінією захисту",[3353,8553,8554],{},"Налаштування складається з чотирьох незалежних прапорців, кожен з яких контролює окремий аспект публічного доступу:",[7592,8556,8557,8562,8568,8572],{},[7595,8558,8561],{"name":8559,"type":8560,"default":6905},"BlockPublicAcls","boolean","Забороняє додавання нових ACL (Access Control Lists), що надають публічний доступ. PUT-запити з публічними ACL відхиляються з помилкою. Не впливає на вже існуючі ACL.",[7595,8563,8565,8566,3453],{"name":8564,"type":8560,"default":6905},"IgnorePublicAcls","Ігнорує всі існуючі публічні ACL на bucket та об'єктах. Навіть якщо ACL вже є і надають публічний доступ — вони ігноруються. Діє разом з ",[3432,8567,8559],{},[7595,8569,8571],{"name":8570,"type":8560,"default":6905},"BlockPublicPolicy","Забороняє додавання нових Bucket Policy, що надають публічний доступ. PUT-запити на Bucket Policy, що містять публічні Principal, відхиляються.",[7595,8573,8575],{"name":8574,"type":8560,"default":6905},"RestrictPublicBuckets","Найбільш агресивний прапорець: обмежує доступ до bucket лише для AWS-сервісів та авторизованих Principal в тому ж акаунті, навіть якщо Bucket Policy вже існує і надає публічний доступ.",[5197,8577,8578,8642],{},[5200,8579,8580,8585,8613,8618,8639],{"label":6375},[3353,8581,8582],{},[3356,8583,8584],{},"Налаштування Block Public Access для bucket:",[6382,8586,8587,8594,8601,8604],{},[3372,8588,6386,8589,6389,8591],{},[3356,8590,5544],{},[3356,8592,8593],{},"Permissions",[3372,8595,7700,8596,3976,8599],{},[3356,8597,8598],{},"Block public access (bucket settings)",[3356,8600,6401],{},[3372,8602,8603],{},"Встановіть усі 4 прапорці ✅ (рекомендовано для всіх bucket з приватними даними)",[3372,8605,8606,8608,8609,8612],{},[3356,8607,6410],{}," → введіть ",[3432,8610,8611],{},"confirm"," у діалозі підтвердження",[3353,8614,8615],{},[3356,8616,8617],{},"Налаштування Block Public Access на рівні акаунту (рекомендовано):",[6382,8619,8620,8628,8633],{},[3372,8621,6386,8622,8624,8625],{},[3356,8623,5544],{}," → у лівому меню ",[3356,8626,8627],{},"Block Public Access settings for this account",[3372,8629,8630,8632],{},[3356,8631,6401],{}," → встановіть усі 4 прапорці",[3372,8634,8635,3976,8637],{},[3356,8636,6410],{},[3432,8638,8611],{},[3353,8640,8641],{},"Налаштування на рівні акаунту застосовується до ВСІХ поточних та майбутніх bucket в акаунті. Це найбезпечніший підхід: навіть якщо хтось помилково створить bucket без захисту — акаунтовий Block Public Access захистить його.",[5200,8643,8644],{"label":6494},[3561,8645,8647],{"className":5205,"code":8646,"language":5207,"meta":3569,"style":3569},"BUCKET=\"my-app-bucket\"\nREGION=\"eu-central-1\"\nACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)\n\n# Заблокувати ВЕСЬ публічний доступ до конкретного bucket\naws s3api put-public-access-block \\\n    --bucket \"$BUCKET\" \\\n    --public-access-block-configuration \\\n        'BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true' \\\n    --region \"$REGION\"\n\n# Перевірити поточні налаштування\naws s3api get-public-access-block \\\n    --bucket \"$BUCKET\" \\\n    --region \"$REGION\"\n\n# Налаштувати Block Public Access на рівні всього акаунту\naws s3control put-public-access-block \\\n    --account-id \"$ACCOUNT_ID\" \\\n    --public-access-block-configuration \\\n        'BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true'\n\n# Перевірити налаштування на рівні акаунту\naws s3control get-public-access-block \\\n    --account-id \"$ACCOUNT_ID\"\n",[3432,8648,8649,8657,8665,8693,8697,8702,8713,8725,8732,8739,8749,8753,8758,8769,8781,8791,8795,8800,8811,8825,8831,8836,8840,8845,8855],{"__ignoreMap":3569},[3603,8650,8651,8653,8655],{"class":3605,"line":3606},[3603,8652,6504],{"class":5576},[3603,8654,6507],{"class":5541},[3603,8656,6510],{"class":5224},[3603,8658,8659,8661,8663],{"class":3605,"line":3612},[3603,8660,6515],{"class":5576},[3603,8662,6507],{"class":5541},[3603,8664,6520],{"class":5224},[3603,8666,8667,8670,8672,8674,8677,8680,8683,8686,8689,8691],{"class":3605,"line":3618},[3603,8668,8669],{"class":5576},"ACCOUNT_ID",[3603,8671,6847],{"class":5541},[3603,8673,5221],{"class":5220},[3603,8675,8676],{"class":5224}," sts",[3603,8678,8679],{"class":5224}," get-caller-identity",[3603,8681,8682],{"class":5243}," --query",[3603,8684,8685],{"class":5224}," Account",[3603,8687,8688],{"class":5243}," --output",[3603,8690,6921],{"class":5224},[3603,8692,6924],{"class":5541},[3603,8694,8695],{"class":3605,"line":3624},[3603,8696,3628],{"emptyLinePlaceholder":3627},[3603,8698,8699],{"class":3605,"line":3631},[3603,8700,8701],{"class":5214},"# Заблокувати ВЕСЬ публічний доступ до конкретного bucket\n",[3603,8703,8704,8706,8708,8711],{"class":3605,"line":3637},[3603,8705,5221],{"class":5220},[3603,8707,5365],{"class":5224},[3603,8709,8710],{"class":5224}," put-public-access-block",[3603,8712,5238],{"class":5237},[3603,8714,8715,8717,8719,8721,8723],{"class":3605,"line":3643},[3603,8716,6545],{"class":5243},[3603,8718,6548],{"class":5224},[3603,8720,6551],{"class":5576},[3603,8722,6554],{"class":5224},[3603,8724,5238],{"class":5237},[3603,8726,8727,8730],{"class":3605,"line":3649},[3603,8728,8729],{"class":5243},"    --public-access-block-configuration",[3603,8731,5238],{"class":5237},[3603,8733,8734,8737],{"class":3605,"line":3655},[3603,8735,8736],{"class":5224},"        'BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true'",[3603,8738,5238],{"class":5237},[3603,8740,8741,8743,8745,8747],{"class":3605,"line":3661},[3603,8742,6571],{"class":5243},[3603,8744,6548],{"class":5224},[3603,8746,6576],{"class":5576},[3603,8748,6579],{"class":5224},[3603,8750,8751],{"class":3605,"line":3667},[3603,8752,3628],{"emptyLinePlaceholder":3627},[3603,8754,8755],{"class":3605,"line":3673},[3603,8756,8757],{"class":5214},"# Перевірити поточні налаштування\n",[3603,8759,8760,8762,8764,8767],{"class":3605,"line":3678},[3603,8761,5221],{"class":5220},[3603,8763,5365],{"class":5224},[3603,8765,8766],{"class":5224}," get-public-access-block",[3603,8768,5238],{"class":5237},[3603,8770,8771,8773,8775,8777,8779],{"class":3605,"line":3684},[3603,8772,6545],{"class":5243},[3603,8774,6548],{"class":5224},[3603,8776,6551],{"class":5576},[3603,8778,6554],{"class":5224},[3603,8780,5238],{"class":5237},[3603,8782,8783,8785,8787,8789],{"class":3605,"line":3690},[3603,8784,6571],{"class":5243},[3603,8786,6548],{"class":5224},[3603,8788,6576],{"class":5576},[3603,8790,6579],{"class":5224},[3603,8792,8793],{"class":3605,"line":3696},[3603,8794,3628],{"emptyLinePlaceholder":3627},[3603,8796,8797],{"class":3605,"line":3702},[3603,8798,8799],{"class":5214},"# Налаштувати Block Public Access на рівні всього акаунту\n",[3603,8801,8802,8804,8807,8809],{"class":3605,"line":3708},[3603,8803,5221],{"class":5220},[3603,8805,8806],{"class":5224}," s3control",[3603,8808,8710],{"class":5224},[3603,8810,5238],{"class":5237},[3603,8812,8813,8816,8818,8821,8823],{"class":3605,"line":4667},[3603,8814,8815],{"class":5243},"    --account-id",[3603,8817,6548],{"class":5224},[3603,8819,8820],{"class":5576},"$ACCOUNT_ID",[3603,8822,6554],{"class":5224},[3603,8824,5238],{"class":5237},[3603,8826,8827,8829],{"class":3605,"line":4673},[3603,8828,8729],{"class":5243},[3603,8830,5238],{"class":5237},[3603,8832,8833],{"class":3605,"line":4679},[3603,8834,8835],{"class":5224},"        'BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true'\n",[3603,8837,8838],{"class":3605,"line":4685},[3603,8839,3628],{"emptyLinePlaceholder":3627},[3603,8841,8842],{"class":3605,"line":4691},[3603,8843,8844],{"class":5214},"# Перевірити налаштування на рівні акаунту\n",[3603,8846,8847,8849,8851,8853],{"class":3605,"line":4696},[3603,8848,5221],{"class":5220},[3603,8850,8806],{"class":5224},[3603,8852,8766],{"class":5224},[3603,8854,5238],{"class":5237},[3603,8856,8857,8859,8861,8863],{"class":3605,"line":4701},[3603,8858,8815],{"class":5243},[3603,8860,6548],{"class":5224},[3603,8862,8820],{"class":5576},[3603,8864,6579],{"class":5224},[4801,8866,8868,8877,8880,8888,8899,8909,8919,8929,8933],{"title":8867},"aws s3api get-public-access-block",[4805,8869,8871,4841,8874],{"className":8870},[3605],[3603,8872,7030],{"className":8873},[7029],[3356,8875,8876],{},"aws s3api get-public-access-block --bucket my-app-bucket --region eu-central-1",[4805,8878,5618],{"className":8879},[3605],[4805,8881,7040,8883,8887],{"className":8882},[3605],[3603,8884,8886],{"className":8885},[7044],"\"PublicAccessBlockConfiguration\"",": {",[4805,8889,8891,8892,5881,8896,7063],{"className":8890},[3605],"    ",[3603,8893,8895],{"className":8894},[7044],"\"BlockPublicAcls\"",[3603,8897,6905],{"className":8898},[7085],[4805,8900,8891,8902,5881,8906,7063],{"className":8901},[3605],[3603,8903,8905],{"className":8904},[7044],"\"IgnorePublicAcls\"",[3603,8907,6905],{"className":8908},[7085],[4805,8910,8891,8912,5881,8916,7063],{"className":8911},[3605],[3603,8913,8915],{"className":8914},[7044],"\"BlockPublicPolicy\"",[3603,8917,6905],{"className":8918},[7085],[4805,8920,8891,8922,5881,8926],{"className":8921},[3605],[3603,8923,8925],{"className":8924},[7044],"\"RestrictPublicBuckets\"",[3603,8927,6905],{"className":8928},[7085],[4805,8930,8932],{"className":8931},[3605],"  }",[4805,8934,5645],{"className":8935},[3605],[3879,8937,8938,8939,8942],{},"AWS рекомендує: ",[3356,8940,8941],{},"усі bucket за замовчуванням приватні",". Увімкнення публічного доступу — навмисна дія з потенційними наслідками для безпеки. Єдиний правомірний сценарій вимкнення Block Public Access — статичний вебсайт, де весь контент свідомо публічний. Навіть тоді краще використовувати CloudFront OAC замість прямого публічного доступу до S3.",[3405,8944],{},[3412,8946,8948],{"id":8947},"bucket-policy-json-правила-доступу","Bucket Policy — JSON-правила доступу",[3353,8950,8951,8954,8955,8958],{},[3356,8952,8953],{},"Bucket Policy"," — це ресурсно-орієнтована IAM-політика у форматі JSON, що прикріплюється безпосередньо до bucket і визначає, які Principal (користувачі, ролі, сервіси, акаунти) можуть виконувати які дії з bucket та його об'єктами. На відміну від IAM Policy (прикріпленої до ідентифікатора), Bucket Policy є атрибутом самого ресурсу і дозволяє надавати ",[3356,8956,8957],{},"крос-акаунтний доступ"," без зміни IAM в інших акаунтах.",[3353,8960,8961,8964,8965,8968,8969,3453],{},[3356,8962,8963],{},"Оцінка доступу:"," S3 оцінює одночасно Bucket Policy і IAM Policy. Якщо хоча б одна з них містить явну заборону (",[3432,8966,8967],{},"Deny",") — доступ заблокований. Для надання доступу principal всередині того самого акаунту достатньо дозволу в одній з двох (IAM або Bucket Policy). Для крос-акаунтного доступу потрібен Allow в ",[3356,8970,8971],{},"обох",[3353,8973,8974],{},[3356,8975,8976],{},"Приклад 1: публічний доступ на читання для статичного вебсайту:",[3561,8978,8980],{"className":5851,"code":8979,"language":5853,"meta":3569,"style":3569},"{\n    \"Version\": \"2012-10-17\",\n    \"Statement\": [\n        {\n            \"Sid\": \"PublicReadGetObject\",\n            \"Effect\": \"Allow\",\n            \"Principal\": \"*\",\n            \"Action\": \"s3:GetObject\",\n            \"Resource\": \"arn:aws:s3:::my-website-bucket\u002F*\"\n        }\n    ]\n}\n",[3432,8981,8982,8986,8998,9005,9009,9021,9033,9045,9057,9067,9071,9075],{"__ignoreMap":3569},[3603,8983,8984],{"class":3605,"line":3606},[3603,8985,5591],{"class":5541},[3603,8987,8988,8991,8993,8996],{"class":3605,"line":3612},[3603,8989,8990],{"class":5864},"    \"Version\"",[3603,8992,5881],{"class":5541},[3603,8994,8995],{"class":5224},"\"2012-10-17\"",[3603,8997,5604],{"class":5541},[3603,8999,9000,9003],{"class":3605,"line":3618},[3603,9001,9002],{"class":5864},"    \"Statement\"",[3603,9004,5868],{"class":5541},[3603,9006,9007],{"class":3605,"line":3624},[3603,9008,5873],{"class":5541},[3603,9010,9011,9014,9016,9019],{"class":3605,"line":3631},[3603,9012,9013],{"class":5864},"            \"Sid\"",[3603,9015,5881],{"class":5541},[3603,9017,9018],{"class":5224},"\"PublicReadGetObject\"",[3603,9020,5604],{"class":5541},[3603,9022,9023,9026,9028,9031],{"class":3605,"line":3637},[3603,9024,9025],{"class":5864},"            \"Effect\"",[3603,9027,5881],{"class":5541},[3603,9029,9030],{"class":5224},"\"Allow\"",[3603,9032,5604],{"class":5541},[3603,9034,9035,9038,9040,9043],{"class":3605,"line":3643},[3603,9036,9037],{"class":5864},"            \"Principal\"",[3603,9039,5881],{"class":5541},[3603,9041,9042],{"class":5224},"\"*\"",[3603,9044,5604],{"class":5541},[3603,9046,9047,9050,9052,9055],{"class":3605,"line":3649},[3603,9048,9049],{"class":5864},"            \"Action\"",[3603,9051,5881],{"class":5541},[3603,9053,9054],{"class":5224},"\"s3:GetObject\"",[3603,9056,5604],{"class":5541},[3603,9058,9059,9062,9064],{"class":3605,"line":3655},[3603,9060,9061],{"class":5864},"            \"Resource\"",[3603,9063,5881],{"class":5541},[3603,9065,9066],{"class":5224},"\"arn:aws:s3:::my-website-bucket\u002F*\"\n",[3603,9068,9069],{"class":3605,"line":3661},[3603,9070,6050],{"class":5541},[3603,9072,9073],{"class":3605,"line":3667},[3603,9074,6055],{"class":5541},[3603,9076,9077],{"class":3605,"line":3673},[3603,9078,3670],{"class":5541},[3353,9080,9081],{},[3356,9082,9083],{},"Приклад 2: доступ лише для конкретної Lambda функції (IAM Role):",[3561,9085,9087],{"className":5851,"code":9086,"language":5853,"meta":3569,"style":3569},"{\n    \"Version\": \"2012-10-17\",\n    \"Statement\": [\n        {\n            \"Sid\": \"LambdaReadWrite\",\n            \"Effect\": \"Allow\",\n            \"Principal\": {\n                \"AWS\": \"arn:aws:iam::123456789012:role\u002FMyLambdaRole\"\n            },\n            \"Action\": [\"s3:GetObject\", \"s3:PutObject\", \"s3:DeleteObject\"],\n            \"Resource\": \"arn:aws:s3:::my-app-bucket\u002Fuploads\u002F*\"\n        }\n    ]\n}\n",[3432,9088,9089,9093,9103,9109,9113,9124,9134,9140,9150,9154,9175,9184,9188,9192],{"__ignoreMap":3569},[3603,9090,9091],{"class":3605,"line":3606},[3603,9092,5591],{"class":5541},[3603,9094,9095,9097,9099,9101],{"class":3605,"line":3612},[3603,9096,8990],{"class":5864},[3603,9098,5881],{"class":5541},[3603,9100,8995],{"class":5224},[3603,9102,5604],{"class":5541},[3603,9104,9105,9107],{"class":3605,"line":3618},[3603,9106,9002],{"class":5864},[3603,9108,5868],{"class":5541},[3603,9110,9111],{"class":3605,"line":3624},[3603,9112,5873],{"class":5541},[3603,9114,9115,9117,9119,9122],{"class":3605,"line":3631},[3603,9116,9013],{"class":5864},[3603,9118,5881],{"class":5541},[3603,9120,9121],{"class":5224},"\"LambdaReadWrite\"",[3603,9123,5604],{"class":5541},[3603,9125,9126,9128,9130,9132],{"class":3605,"line":3637},[3603,9127,9025],{"class":5864},[3603,9129,5881],{"class":5541},[3603,9131,9030],{"class":5224},[3603,9133,5604],{"class":5541},[3603,9135,9136,9138],{"class":3605,"line":3643},[3603,9137,9037],{"class":5864},[3603,9139,5906],{"class":5541},[3603,9141,9142,9145,9147],{"class":3605,"line":3649},[3603,9143,9144],{"class":5864},"                \"AWS\"",[3603,9146,5881],{"class":5541},[3603,9148,9149],{"class":5224},"\"arn:aws:iam::123456789012:role\u002FMyLambdaRole\"\n",[3603,9151,9152],{"class":3605,"line":3655},[3603,9153,5921],{"class":5541},[3603,9155,9156,9158,9160,9162,9164,9167,9169,9172],{"class":3605,"line":3661},[3603,9157,9049],{"class":5864},[3603,9159,7046],{"class":5541},[3603,9161,9054],{"class":5224},[3603,9163,3470],{"class":5541},[3603,9165,9166],{"class":5224},"\"s3:PutObject\"",[3603,9168,3470],{"class":5541},[3603,9170,9171],{"class":5224},"\"s3:DeleteObject\"",[3603,9173,9174],{"class":5541},"],\n",[3603,9176,9177,9179,9181],{"class":3605,"line":3667},[3603,9178,9061],{"class":5864},[3603,9180,5881],{"class":5541},[3603,9182,9183],{"class":5224},"\"arn:aws:s3:::my-app-bucket\u002Fuploads\u002F*\"\n",[3603,9185,9186],{"class":3605,"line":3673},[3603,9187,6050],{"class":5541},[3603,9189,9190],{"class":3605,"line":3678},[3603,9191,6055],{"class":5541},[3603,9193,9194],{"class":3605,"line":3684},[3603,9195,3670],{"class":5541},[3353,9197,9198],{},[3356,9199,9200],{},"Приклад 3: доступ лише з певного VPC (ізоляція private API):",[3561,9202,9204],{"className":5851,"code":9203,"language":5853,"meta":3569,"style":3569},"{\n    \"Version\": \"2012-10-17\",\n    \"Statement\": [\n        {\n            \"Sid\": \"DenyOutsideVPC\",\n            \"Effect\": \"Deny\",\n            \"Principal\": \"*\",\n            \"Action\": \"s3:*\",\n            \"Resource\": [\"arn:aws:s3:::private-bucket\", \"arn:aws:s3:::private-bucket\u002F*\"],\n            \"Condition\": {\n                \"StringNotEquals\": {\n                    \"aws:SourceVpc\": \"vpc-0a1b2c3d4e5f\"\n                }\n            }\n        }\n    ]\n}\n",[3432,9205,9206,9210,9220,9226,9230,9241,9252,9262,9273,9289,9296,9303,9313,9317,9321,9325,9329],{"__ignoreMap":3569},[3603,9207,9208],{"class":3605,"line":3606},[3603,9209,5591],{"class":5541},[3603,9211,9212,9214,9216,9218],{"class":3605,"line":3612},[3603,9213,8990],{"class":5864},[3603,9215,5881],{"class":5541},[3603,9217,8995],{"class":5224},[3603,9219,5604],{"class":5541},[3603,9221,9222,9224],{"class":3605,"line":3618},[3603,9223,9002],{"class":5864},[3603,9225,5868],{"class":5541},[3603,9227,9228],{"class":3605,"line":3624},[3603,9229,5873],{"class":5541},[3603,9231,9232,9234,9236,9239],{"class":3605,"line":3631},[3603,9233,9013],{"class":5864},[3603,9235,5881],{"class":5541},[3603,9237,9238],{"class":5224},"\"DenyOutsideVPC\"",[3603,9240,5604],{"class":5541},[3603,9242,9243,9245,9247,9250],{"class":3605,"line":3637},[3603,9244,9025],{"class":5864},[3603,9246,5881],{"class":5541},[3603,9248,9249],{"class":5224},"\"Deny\"",[3603,9251,5604],{"class":5541},[3603,9253,9254,9256,9258,9260],{"class":3605,"line":3643},[3603,9255,9037],{"class":5864},[3603,9257,5881],{"class":5541},[3603,9259,9042],{"class":5224},[3603,9261,5604],{"class":5541},[3603,9263,9264,9266,9268,9271],{"class":3605,"line":3649},[3603,9265,9049],{"class":5864},[3603,9267,5881],{"class":5541},[3603,9269,9270],{"class":5224},"\"s3:*\"",[3603,9272,5604],{"class":5541},[3603,9274,9275,9277,9279,9282,9284,9287],{"class":3605,"line":3655},[3603,9276,9061],{"class":5864},[3603,9278,7046],{"class":5541},[3603,9280,9281],{"class":5224},"\"arn:aws:s3:::private-bucket\"",[3603,9283,3470],{"class":5541},[3603,9285,9286],{"class":5224},"\"arn:aws:s3:::private-bucket\u002F*\"",[3603,9288,9174],{"class":5541},[3603,9290,9291,9294],{"class":3605,"line":3661},[3603,9292,9293],{"class":5864},"            \"Condition\"",[3603,9295,5906],{"class":5541},[3603,9297,9298,9301],{"class":3605,"line":3667},[3603,9299,9300],{"class":5864},"                \"StringNotEquals\"",[3603,9302,5906],{"class":5541},[3603,9304,9305,9308,9310],{"class":3605,"line":3673},[3603,9306,9307],{"class":5864},"                    \"aws:SourceVpc\"",[3603,9309,5881],{"class":5541},[3603,9311,9312],{"class":5224},"\"vpc-0a1b2c3d4e5f\"\n",[3603,9314,9315],{"class":3605,"line":3678},[3603,9316,6018],{"class":5541},[3603,9318,9319],{"class":3605,"line":3684},[3603,9320,6045],{"class":5541},[3603,9322,9323],{"class":3605,"line":3690},[3603,9324,6050],{"class":5541},[3603,9326,9327],{"class":3605,"line":3696},[3603,9328,6055],{"class":5541},[3603,9330,9331],{"class":3605,"line":3702},[3603,9332,3670],{"class":5541},[3353,9334,9335],{},[3356,9336,9337],{},"Приклад 4: обов'язкове шифрування при завантаженні (deny незашифрованих PUT):",[3561,9339,9341],{"className":5851,"code":9340,"language":5853,"meta":3569,"style":3569},"{\n    \"Version\": \"2012-10-17\",\n    \"Statement\": [\n        {\n            \"Sid\": \"DenyUnencryptedPut\",\n            \"Effect\": \"Deny\",\n            \"Principal\": \"*\",\n            \"Action\": \"s3:PutObject\",\n            \"Resource\": \"arn:aws:s3:::my-secure-bucket\u002F*\",\n            \"Condition\": {\n                \"StringNotEquals\": {\n                    \"s3:x-amz-server-side-encryption\": \"aws:kms\"\n                }\n            }\n        }\n    ]\n}\n",[3432,9342,9343,9347,9357,9363,9367,9378,9388,9398,9408,9419,9425,9431,9441,9445,9449,9453,9457],{"__ignoreMap":3569},[3603,9344,9345],{"class":3605,"line":3606},[3603,9346,5591],{"class":5541},[3603,9348,9349,9351,9353,9355],{"class":3605,"line":3612},[3603,9350,8990],{"class":5864},[3603,9352,5881],{"class":5541},[3603,9354,8995],{"class":5224},[3603,9356,5604],{"class":5541},[3603,9358,9359,9361],{"class":3605,"line":3618},[3603,9360,9002],{"class":5864},[3603,9362,5868],{"class":5541},[3603,9364,9365],{"class":3605,"line":3624},[3603,9366,5873],{"class":5541},[3603,9368,9369,9371,9373,9376],{"class":3605,"line":3631},[3603,9370,9013],{"class":5864},[3603,9372,5881],{"class":5541},[3603,9374,9375],{"class":5224},"\"DenyUnencryptedPut\"",[3603,9377,5604],{"class":5541},[3603,9379,9380,9382,9384,9386],{"class":3605,"line":3637},[3603,9381,9025],{"class":5864},[3603,9383,5881],{"class":5541},[3603,9385,9249],{"class":5224},[3603,9387,5604],{"class":5541},[3603,9389,9390,9392,9394,9396],{"class":3605,"line":3643},[3603,9391,9037],{"class":5864},[3603,9393,5881],{"class":5541},[3603,9395,9042],{"class":5224},[3603,9397,5604],{"class":5541},[3603,9399,9400,9402,9404,9406],{"class":3605,"line":3649},[3603,9401,9049],{"class":5864},[3603,9403,5881],{"class":5541},[3603,9405,9166],{"class":5224},[3603,9407,5604],{"class":5541},[3603,9409,9410,9412,9414,9417],{"class":3605,"line":3655},[3603,9411,9061],{"class":5864},[3603,9413,5881],{"class":5541},[3603,9415,9416],{"class":5224},"\"arn:aws:s3:::my-secure-bucket\u002F*\"",[3603,9418,5604],{"class":5541},[3603,9420,9421,9423],{"class":3605,"line":3661},[3603,9422,9293],{"class":5864},[3603,9424,5906],{"class":5541},[3603,9426,9427,9429],{"class":3605,"line":3667},[3603,9428,9300],{"class":5864},[3603,9430,5906],{"class":5541},[3603,9432,9433,9436,9438],{"class":3605,"line":3673},[3603,9434,9435],{"class":5864},"                    \"s3:x-amz-server-side-encryption\"",[3603,9437,5881],{"class":5541},[3603,9439,9440],{"class":5224},"\"aws:kms\"\n",[3603,9442,9443],{"class":3605,"line":3678},[3603,9444,6018],{"class":5541},[3603,9446,9447],{"class":3605,"line":3684},[3603,9448,6045],{"class":5541},[3603,9450,9451],{"class":3605,"line":3690},[3603,9452,6050],{"class":5541},[3603,9454,9455],{"class":3605,"line":3696},[3603,9456,6055],{"class":5541},[3603,9458,9459],{"class":3605,"line":3702},[3603,9460,3670],{"class":5541},[3353,9462,9463],{},[3356,9464,9465],{},"Документація полів Statement:",[7592,9467,9468,9472,9485,9505,9536,9552],{},[7595,9469,9471],{"name":9470,"type":7598},"Sid","Ідентифікатор statement (Statement ID). Довільний рядок без пробілів. Використовується для читабельності та debugging. Необов'язковий, але рекомендований — AWS Console і CloudTrail відображають його у подіях доступу.",[7595,9473,9475,6482,9478,9480,9481,9484],{"name":9474,"type":7598,"required":6905},"Effect",[3432,9476,9477],{},"Allow",[3432,9479,8967],{},". При конфлікті між Allow і Deny — ",[3356,9482,9483],{},"Deny завжди перемагає"," (explicit deny overrides everything). Використовуйте Deny для безумовних заборон, наприклад, заборона без MFA або за межами VPC.",[7595,9486,9489,9490,9492,9493,9496,9497,9500,9501,9504],{"name":9487,"type":9488,"required":6905},"Principal","string | object","Кому надається дозвіл. ",[3432,9491,9042],{}," — всі (публічно). ",[3432,9494,9495],{},"{\"AWS\": \"arn:...role\u002F...\"}"," — конкретна IAM роль. ",[3432,9498,9499],{},"{\"Service\": \"cloudfront.amazonaws.com\"}"," — AWS сервіс. ",[3432,9502,9503],{},"{\"AWS\": \"123456789012\"}"," — весь інший AWS акаунт (крос-акаунтний доступ).",[7595,9506,9509,9510,3470,9513,3470,9516,3470,9519,3470,9522,9525,9526,9528,9529,9531,9532,9535],{"name":9507,"type":9508,"required":6905},"Action","string | array","Масив дій S3 API. Наприклад: ",[3432,9511,9512],{},"s3:GetObject",[3432,9514,9515],{},"s3:PutObject",[3432,9517,9518],{},"s3:DeleteObject",[3432,9520,9521],{},"s3:ListBucket",[3432,9523,9524],{},"s3:*"," (всі). ",[3432,9527,9521],{}," застосовується до bucket ARN, ",[3432,9530,9512],{}," — до об'єктного ARN (",[3432,9533,9534],{},"bucket\u002F*",").",[7595,9537,9539,9540,9543,9544,9547,9548,9551],{"name":9538,"type":9508,"required":6905},"Resource","ARN ресурсу. ",[3432,9541,9542],{},"arn:aws:s3:::bucket-name"," — сам bucket (для ListBucket). ",[3432,9545,9546],{},"arn:aws:s3:::bucket-name\u002F*"," — всі об'єкти. ",[3432,9549,9550],{},"arn:aws:s3:::bucket-name\u002Fprefix\u002F*"," — об'єкти з префіксом.",[7595,9553,9555,9556,9559,9560,9563,9564,9567,9568,9571,9572,9575],{"name":9554,"type":7614},"Condition","Умовне застосування правила. Ключі: ",[3432,9557,9558],{},"aws:SourceIp"," (IP-адреса клієнта), ",[3432,9561,9562],{},"aws:SourceVpc"," (VPC ID), ",[3432,9565,9566],{},"aws:MultiFactorAuthPresent"," (наявність MFA), ",[3432,9569,9570],{},"s3:prefix"," (prefix запиту), ",[3432,9573,9574],{},"s3:x-amz-server-side-encryption"," (тип шифрування). Умови комбінуються логічним AND.",[5197,9577,9578,9627],{},[5200,9579,9580,9585,9613],{"label":6375},[3353,9581,9582],{},[3356,9583,9584],{},"Додавання Bucket Policy через Policy Editor:",[6382,9586,9587,9591,9599,9606,9609],{},[3372,9588,7694,9589],{},[3356,9590,8593],{},[3372,9592,9593,9594,3976,9597],{},"Прокрутіть до ",[3356,9595,9596],{},"Bucket policy",[3356,9598,6401],{},[3372,9600,9601,9602,9605],{},"Введіть JSON вручну або скористайтесь ",[3356,9603,9604],{},"Policy generator"," (кнопка праворуч)",[3372,9607,9608],{},"AWS Console підсвічує синтаксичні помилки в реальному часі",[3372,9610,9611],{},[3356,9612,6410],{},[3353,9614,9615,9618,9619,9622,9623,9626],{},[3356,9616,9617],{},"Порада:"," скористайтесь ",[3356,9620,9621],{},"AWS Policy Generator"," (кнопка у консолі) або ",[3356,9624,9625],{},"IAM Policy Simulator"," для тестування політики перед застосуванням.",[5200,9628,9629],{"label":6494},[3561,9630,9632],{"className":5205,"code":9631,"language":5207,"meta":3569,"style":3569},"BUCKET=\"my-app-bucket\"\nREGION=\"eu-central-1\"\nACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)\n\n# Зберегти Bucket Policy у файл\ncat > \u002Ftmp\u002Fbucket-policy.json \u003C\u003C EOF\n{\n    \"Version\": \"2012-10-17\",\n    \"Statement\": [\n        {\n            \"Sid\": \"LambdaReadWrite\",\n            \"Effect\": \"Allow\",\n            \"Principal\": {\n                \"AWS\": \"arn:aws:iam::${ACCOUNT_ID}:role\u002FMyLambdaRole\"\n            },\n            \"Action\": [\"s3:GetObject\", \"s3:PutObject\"],\n            \"Resource\": \"arn:aws:s3:::${BUCKET}\u002Fuploads\u002F*\"\n        }\n    ]\n}\nEOF\n\n# Застосувати Bucket Policy\naws s3api put-bucket-policy \\\n    --bucket \"$BUCKET\" \\\n    --policy file:\u002F\u002F\u002Ftmp\u002Fbucket-policy.json \\\n    --region \"$REGION\"\n\n# Переглянути поточну Bucket Policy\naws s3api get-bucket-policy \\\n    --bucket \"$BUCKET\" \\\n    --region \"$REGION\" \\\n    --query Policy --output text | python3 -m json.tool\n\n# Видалити Bucket Policy\naws s3api delete-bucket-policy \\\n    --bucket \"$BUCKET\" \\\n    --region \"$REGION\"\n",[3432,9633,9634,9642,9650,9672,9676,9681,9694,9698,9703,9708,9712,9717,9722,9727,9737,9741,9746,9756,9760,9764,9768,9772,9776,9781,9792,9804,9814,9824,9828,9833,9844,9856,9868,9891,9895,9900,9911,9923],{"__ignoreMap":3569},[3603,9635,9636,9638,9640],{"class":3605,"line":3606},[3603,9637,6504],{"class":5576},[3603,9639,6507],{"class":5541},[3603,9641,6510],{"class":5224},[3603,9643,9644,9646,9648],{"class":3605,"line":3612},[3603,9645,6515],{"class":5576},[3603,9647,6507],{"class":5541},[3603,9649,6520],{"class":5224},[3603,9651,9652,9654,9656,9658,9660,9662,9664,9666,9668,9670],{"class":3605,"line":3618},[3603,9653,8669],{"class":5576},[3603,9655,6847],{"class":5541},[3603,9657,5221],{"class":5220},[3603,9659,8676],{"class":5224},[3603,9661,8679],{"class":5224},[3603,9663,8682],{"class":5243},[3603,9665,8685],{"class":5224},[3603,9667,8688],{"class":5243},[3603,9669,6921],{"class":5224},[3603,9671,6924],{"class":5541},[3603,9673,9674],{"class":3605,"line":3624},[3603,9675,3628],{"emptyLinePlaceholder":3627},[3603,9677,9678],{"class":3605,"line":3631},[3603,9679,9680],{"class":5214},"# Зберегти Bucket Policy у файл\n",[3603,9682,9683,9685,9687,9690,9692],{"class":3605,"line":3637},[3603,9684,7831],{"class":5220},[3603,9686,7834],{"class":5541},[3603,9688,9689],{"class":5224},"\u002Ftmp\u002Fbucket-policy.json",[3603,9691,7840],{"class":5541},[3603,9693,7945],{"class":5541},[3603,9695,9696],{"class":3605,"line":3643},[3603,9697,5591],{"class":5224},[3603,9699,9700],{"class":3605,"line":3649},[3603,9701,9702],{"class":5224},"    \"Version\": \"2012-10-17\",\n",[3603,9704,9705],{"class":3605,"line":3655},[3603,9706,9707],{"class":5224},"    \"Statement\": [\n",[3603,9709,9710],{"class":3605,"line":3661},[3603,9711,5873],{"class":5224},[3603,9713,9714],{"class":3605,"line":3667},[3603,9715,9716],{"class":5224},"            \"Sid\": \"LambdaReadWrite\",\n",[3603,9718,9719],{"class":3605,"line":3673},[3603,9720,9721],{"class":5224},"            \"Effect\": \"Allow\",\n",[3603,9723,9724],{"class":3605,"line":3678},[3603,9725,9726],{"class":5224},"            \"Principal\": {\n",[3603,9728,9729,9732,9734],{"class":3605,"line":3684},[3603,9730,9731],{"class":5224},"                \"AWS\": \"arn:aws:iam::${",[3603,9733,8669],{"class":5576},[3603,9735,9736],{"class":5224},"}:role\u002FMyLambdaRole\"\n",[3603,9738,9739],{"class":3605,"line":3690},[3603,9740,5921],{"class":5224},[3603,9742,9743],{"class":3605,"line":3696},[3603,9744,9745],{"class":5224},"            \"Action\": [\"s3:GetObject\", \"s3:PutObject\"],\n",[3603,9747,9748,9751,9753],{"class":3605,"line":3702},[3603,9749,9750],{"class":5224},"            \"Resource\": \"arn:aws:s3:::${",[3603,9752,6504],{"class":5576},[3603,9754,9755],{"class":5224},"}\u002Fuploads\u002F*\"\n",[3603,9757,9758],{"class":3605,"line":3708},[3603,9759,6050],{"class":5224},[3603,9761,9762],{"class":3605,"line":4667},[3603,9763,6055],{"class":5224},[3603,9765,9766],{"class":3605,"line":4673},[3603,9767,3670],{"class":5224},[3603,9769,9770],{"class":3605,"line":4679},[3603,9771,7945],{"class":5541},[3603,9773,9774],{"class":3605,"line":4685},[3603,9775,3628],{"emptyLinePlaceholder":3627},[3603,9777,9778],{"class":3605,"line":4691},[3603,9779,9780],{"class":5214},"# Застосувати Bucket Policy\n",[3603,9782,9783,9785,9787,9790],{"class":3605,"line":4696},[3603,9784,5221],{"class":5220},[3603,9786,5365],{"class":5224},[3603,9788,9789],{"class":5224}," put-bucket-policy",[3603,9791,5238],{"class":5237},[3603,9793,9794,9796,9798,9800,9802],{"class":3605,"line":4701},[3603,9795,6545],{"class":5243},[3603,9797,6548],{"class":5224},[3603,9799,6551],{"class":5576},[3603,9801,6554],{"class":5224},[3603,9803,5238],{"class":5237},[3603,9805,9806,9809,9812],{"class":3605,"line":4707},[3603,9807,9808],{"class":5243},"    --policy",[3603,9810,9811],{"class":5224}," file:\u002F\u002F\u002Ftmp\u002Fbucket-policy.json",[3603,9813,5238],{"class":5237},[3603,9815,9816,9818,9820,9822],{"class":3605,"line":4713},[3603,9817,6571],{"class":5243},[3603,9819,6548],{"class":5224},[3603,9821,6576],{"class":5576},[3603,9823,6579],{"class":5224},[3603,9825,9826],{"class":3605,"line":4719},[3603,9827,3628],{"emptyLinePlaceholder":3627},[3603,9829,9830],{"class":3605,"line":4724},[3603,9831,9832],{"class":5214},"# Переглянути поточну Bucket Policy\n",[3603,9834,9835,9837,9839,9842],{"class":3605,"line":4730},[3603,9836,5221],{"class":5220},[3603,9838,5365],{"class":5224},[3603,9840,9841],{"class":5224}," get-bucket-policy",[3603,9843,5238],{"class":5237},[3603,9845,9846,9848,9850,9852,9854],{"class":3605,"line":4736},[3603,9847,6545],{"class":5243},[3603,9849,6548],{"class":5224},[3603,9851,6551],{"class":5576},[3603,9853,6554],{"class":5224},[3603,9855,5238],{"class":5237},[3603,9857,9858,9860,9862,9864,9866],{"class":3605,"line":4742},[3603,9859,6571],{"class":5243},[3603,9861,6548],{"class":5224},[3603,9863,6576],{"class":5576},[3603,9865,6554],{"class":5224},[3603,9867,5238],{"class":5237},[3603,9869,9870,9872,9875,9877,9879,9882,9885,9888],{"class":3605,"line":4748},[3603,9871,6677],{"class":5243},[3603,9873,9874],{"class":5224}," Policy",[3603,9876,8688],{"class":5243},[3603,9878,6921],{"class":5224},[3603,9880,9881],{"class":5541}," | ",[3603,9883,9884],{"class":5220},"python3",[3603,9886,9887],{"class":5243}," -m",[3603,9889,9890],{"class":5224}," json.tool\n",[3603,9892,9893],{"class":3605,"line":4753},[3603,9894,3628],{"emptyLinePlaceholder":3627},[3603,9896,9897],{"class":3605,"line":4758},[3603,9898,9899],{"class":5214},"# Видалити Bucket Policy\n",[3603,9901,9902,9904,9906,9909],{"class":3605,"line":6363},[3603,9903,5221],{"class":5220},[3603,9905,5365],{"class":5224},[3603,9907,9908],{"class":5224}," delete-bucket-policy",[3603,9910,5238],{"class":5237},[3603,9912,9913,9915,9917,9919,9921],{"class":3605,"line":6368},[3603,9914,6545],{"class":5243},[3603,9916,6548],{"class":5224},[3603,9918,6551],{"class":5576},[3603,9920,6554],{"class":5224},[3603,9922,5238],{"class":5237},[3603,9924,9925,9927,9929,9931],{"class":3605,"line":6830},[3603,9926,6571],{"class":5243},[3603,9928,6548],{"class":5224},[3603,9930,6576],{"class":5576},[3603,9932,6579],{"class":5224},[4801,9934,9936,9945,9948,9958,9965,9968,9978,9988,10003,10017,10028,10031,10034],{"title":9935},"aws s3api get-bucket-policy",[4805,9937,9939,4841,9942],{"className":9938},[3605],[3603,9940,7030],{"className":9941},[7029],[3356,9943,9944],{},"aws s3api get-bucket-policy --bucket my-app-bucket --query Policy --output text | python3 -m json.tool",[4805,9946,5618],{"className":9947},[3605],[4805,9949,7040,9951,5881,9955,7063],{"className":9950},[3605],[3603,9952,9954],{"className":9953},[7044],"\"Version\"",[3603,9956,8995],{"className":9957},[4811],[4805,9959,7040,9961,7046],{"className":9960},[3605],[3603,9962,9964],{"className":9963},[7044],"\"Statement\"",[4805,9966,7050],{"className":9967},[3605],[4805,9969,7054,9971,5881,9975,7063],{"className":9970},[3605],[3603,9972,9974],{"className":9973},[7044],"\"Sid\"",[3603,9976,9121],{"className":9977},[4811],[4805,9979,7054,9981,5881,9985,7063],{"className":9980},[3605],[3603,9982,9984],{"className":9983},[7044],"\"Effect\"",[3603,9986,9030],{"className":9987},[4811],[4805,9989,7054,9991,7439,9995,5881,9999,8140],{"className":9990},[3605],[3603,9992,9994],{"className":9993},[7044],"\"Principal\"",[3603,9996,9998],{"className":9997},[7044],"\"AWS\"",[3603,10000,10002],{"className":10001},[4811],"\"arn:aws:iam::123456789012:role\u002FMyLambdaRole\"",[4805,10004,7054,10006,7046,10010,3470,10013,10016],{"className":10005},[3605],[3603,10007,10009],{"className":10008},[7044],"\"Action\"",[3603,10011,9054],{"className":10012},[4811],[3603,10014,9166],{"className":10015},[4811],"],",[4805,10018,7054,10020,5881,10024],{"className":10019},[3605],[3603,10021,10023],{"className":10022},[7044],"\"Resource\"",[3603,10025,10027],{"className":10026},[4811],"\"arn:aws:s3:::my-app-bucket\u002Fuploads\u002F*\"",[4805,10029,7140],{"className":10030},[3605],[4805,10032,8203],{"className":10033},[3605],[4805,10035,5645],{"className":10036},[3605],[3405,10038],{},[3412,10040,10042],{"id":10041},"s3-encryption-шифрування-даних","S3 Encryption — шифрування даних",[3353,10044,10045],{},"Аmazon S3 підтримує три незалежні режими серверного шифрування (SSE — Server-Side Encryption), що відрізняються за тим, хто керує ключами та яка гнучкість управління ними надається.",[3593,10047,10048],{},[3561,10049,10051],{"className":3597,"code":10050,"language":3599,"meta":3569,"style":3569},"@startuml\nskinparam style plain\nskinparam backgroundColor #ffffff\ntitle \"SSE-KMS: схема шифрування та розшифрування об'єктів\"\n\nparticipant \".NET App\\n(EC2\u002FLambda)\" as APP #dbeafe\nparticipant \"Amazon S3\" as S3 #bbf7d0\nparticipant \"AWS KMS\" as KMS #fef3c7\nparticipant \"S3 Storage\\n(диск)\" as DISK #e5e7eb\n\n== PUT: завантаження об'єкту ==\n\nAPP -> S3 : PUT \u002Fbucket\u002Ffile.pdf\\nx-amz-server-side-encryption: aws:kms\\nx-amz-server-side-encryption-aws-kms-key-id: arn:kms:...\\n[дані в plaintext]\n\nS3 -> KMS : GenerateDataKey(KeyId=arn:kms:...)\nnote right: KMS генерує пару:\\n- Plaintext DEK (Data Encryption Key)\\n- Encrypted DEK\n\nKMS --> S3 : {plaintext_DEK, encrypted_DEK}\n\nS3 -> S3 : Шифрує файл за допомогою plaintext_DEK\\n(AES-256-GCM)\nnote right: plaintext_DEK видаляється\\nз пам'яті відразу після\\nшифрування!\n\nS3 -> DISK : Зберігає: encrypted_file + encrypted_DEK\nS3 --> APP : 200 OK\\nx-amz-server-side-encryption: aws:kms\n\n== GET: читання об'єкту ==\n\nAPP -> S3 : GET \u002Fbucket\u002Ffile.pdf\nS3 -> DISK : Читає encrypted_file + encrypted_DEK\nS3 -> KMS : Decrypt(encrypted_DEK)\\n[перевірка IAM прав на kms:Decrypt]\nKMS --> S3 : plaintext_DEK\nS3 -> S3 : Розшифровує файл\nS3 --> APP : 200 OK + plaintext файл\n\n@enduml\n",[3432,10052,10053,10057,10061,10065,10070,10074,10079,10084,10089,10094,10098,10103,10107,10112,10116,10121,10126,10130,10135,10139,10144,10149,10153,10158,10163,10167,10172,10176,10181,10186,10191,10196,10201,10206,10210],{"__ignoreMap":3569},[3603,10054,10055],{"class":3605,"line":3606},[3603,10056,3609],{},[3603,10058,10059],{"class":3605,"line":3612},[3603,10060,3615],{},[3603,10062,10063],{"class":3605,"line":3618},[3603,10064,3621],{},[3603,10066,10067],{"class":3605,"line":3624},[3603,10068,10069],{},"title \"SSE-KMS: схема шифрування та розшифрування об'єктів\"\n",[3603,10071,10072],{"class":3605,"line":3631},[3603,10073,3628],{"emptyLinePlaceholder":3627},[3603,10075,10076],{"class":3605,"line":3637},[3603,10077,10078],{},"participant \".NET App\\n(EC2\u002FLambda)\" as APP #dbeafe\n",[3603,10080,10081],{"class":3605,"line":3643},[3603,10082,10083],{},"participant \"Amazon S3\" as S3 #bbf7d0\n",[3603,10085,10086],{"class":3605,"line":3649},[3603,10087,10088],{},"participant \"AWS KMS\" as KMS #fef3c7\n",[3603,10090,10091],{"class":3605,"line":3655},[3603,10092,10093],{},"participant \"S3 Storage\\n(диск)\" as DISK #e5e7eb\n",[3603,10095,10096],{"class":3605,"line":3661},[3603,10097,3628],{"emptyLinePlaceholder":3627},[3603,10099,10100],{"class":3605,"line":3667},[3603,10101,10102],{},"== PUT: завантаження об'єкту ==\n",[3603,10104,10105],{"class":3605,"line":3673},[3603,10106,3628],{"emptyLinePlaceholder":3627},[3603,10108,10109],{"class":3605,"line":3678},[3603,10110,10111],{},"APP -> S3 : PUT \u002Fbucket\u002Ffile.pdf\\nx-amz-server-side-encryption: aws:kms\\nx-amz-server-side-encryption-aws-kms-key-id: arn:kms:...\\n[дані в plaintext]\n",[3603,10113,10114],{"class":3605,"line":3684},[3603,10115,3628],{"emptyLinePlaceholder":3627},[3603,10117,10118],{"class":3605,"line":3690},[3603,10119,10120],{},"S3 -> KMS : GenerateDataKey(KeyId=arn:kms:...)\n",[3603,10122,10123],{"class":3605,"line":3696},[3603,10124,10125],{},"note right: KMS генерує пару:\\n- Plaintext DEK (Data Encryption Key)\\n- Encrypted DEK\n",[3603,10127,10128],{"class":3605,"line":3702},[3603,10129,3628],{"emptyLinePlaceholder":3627},[3603,10131,10132],{"class":3605,"line":3708},[3603,10133,10134],{},"KMS --> S3 : {plaintext_DEK, encrypted_DEK}\n",[3603,10136,10137],{"class":3605,"line":4667},[3603,10138,3628],{"emptyLinePlaceholder":3627},[3603,10140,10141],{"class":3605,"line":4673},[3603,10142,10143],{},"S3 -> S3 : Шифрує файл за допомогою plaintext_DEK\\n(AES-256-GCM)\n",[3603,10145,10146],{"class":3605,"line":4679},[3603,10147,10148],{},"note right: plaintext_DEK видаляється\\nз пам'яті відразу після\\nшифрування!\n",[3603,10150,10151],{"class":3605,"line":4685},[3603,10152,3628],{"emptyLinePlaceholder":3627},[3603,10154,10155],{"class":3605,"line":4691},[3603,10156,10157],{},"S3 -> DISK : Зберігає: encrypted_file + encrypted_DEK\n",[3603,10159,10160],{"class":3605,"line":4696},[3603,10161,10162],{},"S3 --> APP : 200 OK\\nx-amz-server-side-encryption: aws:kms\n",[3603,10164,10165],{"class":3605,"line":4701},[3603,10166,3628],{"emptyLinePlaceholder":3627},[3603,10168,10169],{"class":3605,"line":4707},[3603,10170,10171],{},"== GET: читання об'єкту ==\n",[3603,10173,10174],{"class":3605,"line":4713},[3603,10175,3628],{"emptyLinePlaceholder":3627},[3603,10177,10178],{"class":3605,"line":4719},[3603,10179,10180],{},"APP -> S3 : GET \u002Fbucket\u002Ffile.pdf\n",[3603,10182,10183],{"class":3605,"line":4724},[3603,10184,10185],{},"S3 -> DISK : Читає encrypted_file + encrypted_DEK\n",[3603,10187,10188],{"class":3605,"line":4730},[3603,10189,10190],{},"S3 -> KMS : Decrypt(encrypted_DEK)\\n[перевірка IAM прав на kms:Decrypt]\n",[3603,10192,10193],{"class":3605,"line":4736},[3603,10194,10195],{},"KMS --> S3 : plaintext_DEK\n",[3603,10197,10198],{"class":3605,"line":4742},[3603,10199,10200],{},"S3 -> S3 : Розшифровує файл\n",[3603,10202,10203],{"class":3605,"line":4748},[3603,10204,10205],{},"S3 --> APP : 200 OK + plaintext файл\n",[3603,10207,10208],{"class":3605,"line":4753},[3603,10209,3628],{"emptyLinePlaceholder":3627},[3603,10211,10212],{"class":3605,"line":4758},[3603,10213,3711],{},[3902,10215,10216,10233],{},[3905,10217,10218],{},[3908,10219,10220,10222,10225,10228,10230],{},[3911,10221,4270],{},[3911,10223,10224],{},"Управління ключем",[3911,10226,10227],{},"Аудит KMS",[3911,10229,4188],{},[3911,10231,10232],{},"Рекомендація",[3924,10234,10235,10254,10273,10290],{},[3908,10236,10237,10242,10245,10248,10251],{},[3929,10238,10239],{},[3356,10240,10241],{},"SSE-S3",[3929,10243,10244],{},"AWS (автоматично)",[3929,10246,10247],{},"Немає",[3929,10249,10250],{},"Безкоштовно",[3929,10252,10253],{},"Базовий захист, всі bucket",[3908,10255,10256,10261,10264,10267,10270],{},[3929,10257,10258],{},[3356,10259,10260],{},"SSE-KMS",[3929,10262,10263],{},"Ви (через AWS KMS)",[3929,10265,10266],{},"CloudTrail",[3929,10268,10269],{},"$0.03\u002F10K запитів",[3929,10271,10272],{},"Чутливі дані",[3908,10274,10275,10280,10283,10285,10287],{},[3929,10276,10277],{},[3356,10278,10279],{},"SSE-C",[3929,10281,10282],{},"Ви (per-request)",[3929,10284,10247],{},[3929,10286,10250],{},[3929,10288,10289],{},"Рідко, особливі вимоги",[3908,10291,10292,10297,10300,10302,10305],{},[3929,10293,10294],{},[3356,10295,10296],{},"DSSE-KMS",[3929,10298,10299],{},"Ви (подвійне)",[3929,10301,10266],{},[3929,10303,10304],{},"$0.06\u002F10K запитів",[3929,10306,10307],{},"Compliance (FIPS 140-3)",[3353,10309,10310,10313,10314,10317],{},[3356,10311,10312],{},"SSE-S3 (AES-256):"," AWS автоматично шифрує всі об'єкти при завантаженні та розшифровує при читанні. Ключами керує AWS. Безкоштовно. ",[3356,10315,10316],{},"Рекомендовано вмикати на всіх bucket за замовчуванням."," Починаючи з 2023 року — увімкнено за замовчуванням для всіх нових bucket.",[3353,10319,10320,10323,10324,10327],{},[3356,10321,10322],{},"SSE-KMS:"," шифрування через AWS Key Management Service. Ви контролюєте ключ, маєте повний аудитний журнал кожного використання ключа в CloudTrail, можете відкликати доступ до ключа (що унеможливлює читання всіх зашифрованих даних). Підходить для чутливих даних (медична, фінансова інформація, PII). ",[3356,10325,10326],{},"BucketKeyEnabled"," — важливий параметр: замість виклику KMS для кожного об'єкту, S3 кешує DEK на рівні bucket, зменшуючи кількість KMS API викликів і, відповідно, вартість.",[3353,10329,10330,10333],{},[3356,10331,10332],{},"SSE-C:"," ви самі надаєте AES-256 ключ при кожному PUT\u002FGET запиті. AWS використовує ключ для шифрування\u002Fрозшифрування, але не зберігає його. Якщо ключ втрачено — дані назавжди недоступні. Рідко використовується через складність управління.",[3353,10335,10336,10339],{},[3356,10337,10338],{},"DSSE-KMS (Double-layer SSE-KMS):"," подвійне шифрування двома незалежними ключами KMS. Відповідає вимогам FIPS 140-3 Level 3 для defense-grade сумісності.",[10341,10342,10344],"h4",{"id":10343},"aws-kms-що-це-і-як-працює","AWS KMS — що це і як працює",[3353,10346,10347,10350,10351,10354],{},[3356,10348,10349],{},"AWS Key Management Service (KMS)"," — це керований сервіс для створення та управління криптографічними ключами. KMS не зберігає ваші дані — він зберігає тільки ",[3356,10352,10353],{},"Customer Master Keys (CMK)"," і використовує їх для шифрування\u002Fрозшифрування коротких шматків даних (Data Encryption Keys, DEK).",[3353,10356,10357],{},"Коли S3 шифрує об'єкт через SSE-KMS, відбувається таке:",[6382,10359,10360,10367,10374,10381,10384],{},[3372,10361,10362,10363,10366],{},"S3 просить KMS згенерувати ",[3356,10364,10365],{},"DEK"," (унікальний ключ для цього об'єкта)",[3372,10368,10369,10370,10373],{},"KMS повертає ",[3356,10371,10372],{},"два варіанти"," DEK: відкритий (plaintext) і зашифрований (encrypted)",[3372,10375,10376,10377,10380],{},"S3 шифрує об'єкт відкритим DEK, потім ",[3356,10378,10379],{},"знищує"," відкритий DEK з пам'яті",[3372,10382,10383],{},"На диску зберігаються тільки зашифровані дані + зашифрований DEK",[3372,10385,10386],{},"При читанні: S3 надсилає encrypted DEK у KMS → KMS розшифровує → S3 читає файл",[3353,10388,10389],{},"Таким чином, навіть якщо хтось отримає доступ до диску S3 — без KMS ключа дані марні.",[3353,10391,10392],{},[3356,10393,10394],{},"Типи KMS ключів:",[3902,10396,10397,10416],{},[3905,10398,10399],{},[3908,10400,10401,10404,10407,10410,10413],{},[3911,10402,10403],{},"Тип",[3911,10405,10406],{},"Управління",[3911,10408,10409],{},"Ротація",[3911,10411,10412],{},"Ціна",[3911,10414,10415],{},"Коли використовувати",[3924,10417,10418,10440,10459],{},[3908,10419,10420,10429,10432,10435,10437],{},[3929,10421,10422,10425,10426,6430],{},[3356,10423,10424],{},"AWS Managed Key"," (",[3432,10427,10428],{},"aws\u002Fs3",[3929,10430,10431],{},"Автоматично AWS",[3929,10433,10434],{},"Автоматична (щороку)",[3929,10436,10250],{},[3929,10438,10439],{},"Простий аудит без тонкого контролю",[3908,10441,10442,10447,10450,10453,10456],{},[3929,10443,10444],{},[3356,10445,10446],{},"Customer Managed Key (CMK)",[3929,10448,10449],{},"Ви (Key Policy)",[3929,10451,10452],{},"Ручна або авто",[3929,10454,10455],{},"$1\u002Fключ\u002Fмісяць + $0.03\u002F10K API",[3929,10457,10458],{},"Повний контроль, відкликання, cross-account",[3908,10460,10461,10466,10469,10472,10475],{},[3929,10462,10463],{},[3356,10464,10465],{},"Custom Key Store (CloudHSM)",[3929,10467,10468],{},"Ви (апаратний HSM)",[3929,10470,10471],{},"Ви контролюєте",[3929,10473,10474],{},"Дорого (~$1.45\u002Fгод за HSM)",[3929,10476,10477],{},"FIPS 140-2 Level 3, regulatory compliance",[3397,10479,10480,10485],{},[3353,10481,10482],{},[3356,10483,10484],{},"Що таке Key Policy?",[3353,10486,10487,10488,10491,10492,10495,10496,3470,10499,10502,10503,10506,10507,3470,10510,10513],{},"Кожен CMK має ",[3356,10489,10490],{},"Key Policy"," — JSON-документ, що визначає хто може ",[3356,10493,10494],{},"використовувати"," ключ (",[3432,10497,10498],{},"kms:GenerateDataKey",[3432,10500,10501],{},"kms:Decrypt",") і хто може ",[3356,10504,10505],{},"управляти"," ним (",[3432,10508,10509],{},"kms:CreateKey",[3432,10511,10512],{},"kms:ScheduleKeyDeletion","). Без явного дозволу в Key Policy — навіть адміністратор аккаунту не може використати ключ.",[4801,10515,10517,10524,10531,10540,10544,10548,10552,10555,10562,10565,10572,10583,10594,10604,10615,10618,10621,10624,10631,10638,10646,10653,10656,10659,10666,10673,10680,10683,10686,10693,10700,10704,10712,10719,10727,10735,10742,10749,10756,10763,10770,10773],{"title":10516},"Створення CMK та налаштування для S3 (SSE-KMS)",[4805,10518,10520],{"className":10519},[3605],[3603,10521,10523],{"className":10522},[4811],"# 1. Створити Customer Managed Key",[4805,10525,10527,10530],{"className":10526},[3605],[3603,10528,7030],{"className":10529},[7029]," aws kms create-key \\",[4805,10532,10534,10535,10539],{"className":10533},[3605],"    --description ",[3603,10536,10538],{"className":10537},[4811],"\"Key for my-app S3 bucket encryption\""," \\",[4805,10541,10543],{"className":10542},[3605],"    --key-usage ENCRYPT_DECRYPT \\",[4805,10545,10547],{"className":10546},[3605],"    --key-spec SYMMETRIC_DEFAULT \\",[4805,10549,10551],{"className":10550},[3605],"    --region eu-central-1",[4805,10553,4841],{"className":10554},[3605],[4805,10556,10558],{"className":10557},[3605],[3603,10559,10561],{"className":10560},[4819],"# Відповідь (скорочено):",[4805,10563,5618],{"className":10564},[3605],[4805,10566,7040,10568,8887],{"className":10567},[3605],[3603,10569,10571],{"className":10570},[7044],"\"KeyMetadata\"",[4805,10573,8891,10575,5881,10579,7063],{"className":10574},[3605],[3603,10576,10578],{"className":10577},[7044],"\"KeyId\"",[3603,10580,10582],{"className":10581},[4811],"\"mrk-0a1b2c3d4e5f6a7b8\"",[4805,10584,8891,10586,5881,10590,7063],{"className":10585},[3605],[3603,10587,10589],{"className":10588},[7044],"\"Arn\"",[3603,10591,10593],{"className":10592},[4811],"\"arn:aws:kms:eu-central-1:123456789012:key\u002Fmrk-0a1b2c3d4e5f6a7b8\"",[4805,10595,8891,10597,5881,10601,7063],{"className":10596},[3605],[3603,10598,10600],{"className":10599},[7044],"\"KeyState\"",[3603,10602,5896],{"className":10603},[4811],[4805,10605,8891,10607,5881,10611],{"className":10606},[3605],[3603,10608,10610],{"className":10609},[7044],"\"KeySpec\"",[3603,10612,10614],{"className":10613},[4811],"\"SYMMETRIC_DEFAULT\"",[4805,10616,8932],{"className":10617},[3605],[4805,10619,5645],{"className":10620},[3605],[4805,10622,4841],{"className":10623},[3605],[4805,10625,10627],{"className":10626},[3605],[3603,10628,10630],{"className":10629},[4811],"# 2. Створити зручний alias",[4805,10632,10634,10637],{"className":10633},[3605],[3603,10635,7030],{"className":10636},[7029]," aws kms create-alias \\",[4805,10639,10641,10642,10539],{"className":10640},[3605],"    --alias-name ",[3603,10643,10645],{"className":10644},[4811],"\"alias\u002Fmy-app-s3\"",[4805,10647,10649,10650,10539],{"className":10648},[3605],"    --target-key-id ",[3603,10651,10582],{"className":10652},[4811],[4805,10654,10551],{"className":10655},[3605],[4805,10657,4841],{"className":10658},[3605],[4805,10660,10662],{"className":10661},[3605],[3603,10663,10665],{"className":10664},[4811],"# 3. Увімкнути авторотацію ключа (щороку автоматично)",[4805,10667,10669,10672],{"className":10668},[3605],[3603,10670,7030],{"className":10671},[7029]," aws kms enable-key-rotation \\",[4805,10674,10676,10677,10539],{"className":10675},[3605],"    --key-id ",[3603,10678,10645],{"className":10679},[4811],[4805,10681,10551],{"className":10682},[3605],[4805,10684,4841],{"className":10685},[3605],[4805,10687,10689],{"className":10688},[3605],[3603,10690,10692],{"className":10691},[4811],"# 4. Призначити ключ як default encryption для bucket",[4805,10694,10696,10699],{"className":10695},[3605],[3603,10697,7030],{"className":10698},[7029]," aws s3api put-bucket-encryption \\",[4805,10701,10703],{"className":10702},[3605],"    --bucket my-app-bucket \\",[4805,10705,10707,10708],{"className":10706},[3605],"    --server-side-encryption-configuration ",[3603,10709,10711],{"className":10710},[4819],"'{",[4805,10713,7054,10715],{"className":10714},[3605],[3603,10716,10718],{"className":10717},[4819],"\"Rules\": [{",[4805,10720,10722,10723],{"className":10721},[3605],"        ",[3603,10724,10726],{"className":10725},[4819],"\"ApplyServerSideEncryptionByDefault\": {",[4805,10728,10730,10731],{"className":10729},[3605],"          ",[3603,10732,10734],{"className":10733},[4819],"\"SSEAlgorithm\": \"aws:kms\",",[4805,10736,10730,10738],{"className":10737},[3605],[3603,10739,10741],{"className":10740},[4819],"\"KMSMasterKeyID\": \"alias\u002Fmy-app-s3\"",[4805,10743,10722,10745],{"className":10744},[3605],[3603,10746,10748],{"className":10747},[4819],"},",[4805,10750,10722,10752],{"className":10751},[3605],[3603,10753,10755],{"className":10754},[4819],"\"BucketKeyEnabled\": true",[4805,10757,7054,10759],{"className":10758},[3605],[3603,10760,10762],{"className":10761},[4819],"}]",[4805,10764,8891,10766],{"className":10765},[3605],[3603,10767,10769],{"className":10768},[4819],"}'",[4805,10771,4841],{"className":10772},[3605],[4805,10774,10776],{"className":10775},[3605],[3603,10777,10779],{"className":10778},[4900],"# Готово. Всі нові об'єкти в bucket шифруються через alias\u002Fmy-app-s3",[4801,10781,10783,10790,10797,10800,10807,10815,10823,10831,10839,10842,10849,10856,10862,10870,10876,10883,10886,10893,10900],{"title":10782},"Вартість KMS: без BucketKey vs з BucketKey",[4805,10784,10786],{"className":10785},[3605],[3603,10787,10789],{"className":10788},[4811],"# Сценарій: завантажуємо 100 000 файлів на місяць",[4805,10791,10793],{"className":10792},[3605],[3603,10794,10796],{"className":10795},[4811],"# і 500 000 GET-запитів на місяць",[4805,10798,4841],{"className":10799},[3605],[4805,10801,10803],{"className":10802},[3605],[3603,10804,10806],{"className":10805},[4819],"Без BucketKey (кожен PUT та GET = 1 KMS API виклик):",[4805,10808,10810,10811],{"className":10809},[3605],"  PUT:  100 000 викликів × $0.03 \u002F 10 000 = ",[3603,10812,10814],{"className":10813},[4864],"$0.30",[4805,10816,10818,10819],{"className":10817},[3605],"  GET:  500 000 викликів × $0.03 \u002F 10 000 = ",[3603,10820,10822],{"className":10821},[4864],"$1.50",[4805,10824,10826,10827],{"className":10825},[3605],"  CMK ключ:                               = ",[3603,10828,10830],{"className":10829},[4864],"$1.00",[4805,10832,10834,10835],{"className":10833},[3605],"  Разом:                                  = ",[3603,10836,10838],{"className":10837},[4864],"$2.80\u002Fміс",[4805,10840,4841],{"className":10841},[3605],[4805,10843,10845],{"className":10844},[3605],[3603,10846,10848],{"className":10847},[4819],"З BucketKey (S3 кешує DEK на рівні bucket,",[4805,10850,10852],{"className":10851},[3605],[3603,10853,10855],{"className":10854},[4819]," 99% GET-запитів не йдуть у KMS):",[4805,10857,10810,10859],{"className":10858},[3605],[3603,10860,10814],{"className":10861},[4811],[4805,10863,10865,10866],{"className":10864},[3605],"  GET:  ~5 000 викликів  × $0.03 \u002F 10 000 = ",[3603,10867,10869],{"className":10868},[4811],"$0.015",[4805,10871,10826,10873],{"className":10872},[3605],[3603,10874,10830],{"className":10875},[4811],[4805,10877,10834,10879],{"className":10878},[3605],[3603,10880,10882],{"className":10881},[4811],"$1.315\u002Fміс",[4805,10884,4841],{"className":10885},[3605],[4805,10887,10889],{"className":10888},[3605],[3603,10890,10892],{"className":10891},[4900],"Економія: ~$1.49\u002Fміс → $17.88\u002Fрік",[4805,10894,10896],{"className":10895},[3605],[3603,10897,10899],{"className":10898},[4900],"На мільйон GET-запитів BucketKey економить ~$3\u002Fміс",[4805,10901,10903],{"className":10902},[3605],[3603,10904,10906],{"className":10905},[4900],"Завжди вмикайте BucketKeyEnabled: true!",[3353,10908,10909],{},[3356,10910,10911],{},"SSE-KMS у .NET SDK — завантаження та читання зашифрованих об'єктів:",[3561,10913,10915],{"className":5519,"code":10914,"language":5521,"meta":3569,"style":3569},"using Amazon.S3;\nusing Amazon.S3.Model;\n\npublic class EncryptedS3Service\n{\n    private readonly IAmazonS3 _s3;\n    \u002F\u002F ARN або alias KMS ключа: \"alias\u002Fmy-app-s3\"\n    private const string KmsKeyId = \"alias\u002Fmy-app-s3\";\n\n    public EncryptedS3Service(IAmazonS3 s3) => _s3 = s3;\n\n    \u002F\u002F Завантажити файл з явним SSE-KMS шифруванням\n    public async Task UploadEncryptedAsync(string bucket, string key, Stream data)\n    {\n        var request = new PutObjectRequest\n        {\n            BucketName = bucket,\n            Key = key,\n            InputStream = data,\n            \u002F\u002F Явно вказуємо SSE-KMS (перевизначає default encryption bucket'а)\n            ServerSideEncryptionMethod = ServerSideEncryptionMethod.AWSKMS,\n            ServerSideEncryptionKeyManagementServiceKeyId = KmsKeyId,\n        };\n\n        var response = await _s3.PutObjectAsync(request);\n\n        \u002F\u002F response.ServerSideEncryptionMethod == \"aws:kms\"\n        \u002F\u002F response.ServerSideEncryptionKeyManagementServiceKeyId — ARN ключа що використовувався\n    }\n\n    \u002F\u002F Читання — прозоре, SDK сам запитує KMS decrypt\n    \u002F\u002F IAM роль вашого застосунку ПОВИННА мати kms:Decrypt на цей ключ\n    public async Task\u003CStream> DownloadEncryptedAsync(string bucket, string key)\n    {\n        var response = await _s3.GetObjectAsync(bucket, key);\n        return response.ResponseStream; \u002F\u002F вже розшифровано S3\n    }\n\n    \u002F\u002F Перевірити яким ключем зашифровано об'єкт\n    public async Task\u003Cstring?> GetObjectEncryptionKeyAsync(string bucket, string key)\n    {\n        var meta = await _s3.GetObjectMetadataAsync(bucket, key);\n        return meta.ServerSideEncryptionKeyManagementServiceKeyId;\n        \u002F\u002F Поверне ARN KMS ключа або null якщо SSE-S3\n    }\n}\n",[3432,10916,10917,10929,10945,10949,10960,10964,10980,10985,11004,11008,11036,11040,11045,11082,11087,11101,11105,11117,11129,11141,11146,11163,11175,11180,11184,11208,11212,11217,11222,11226,11230,11235,11240,11273,11277,11304,11322,11326,11330,11335,11367,11371,11399,11412,11417,11421],{"__ignoreMap":3569},[3603,10918,10919,10921,10923,10925,10927],{"class":3605,"line":3606},[3603,10920,5534],{"class":5533},[3603,10922,5538],{"class":5537},[3603,10924,3453],{"class":5541},[3603,10926,5544],{"class":5537},[3603,10928,5547],{"class":5541},[3603,10930,10931,10933,10935,10937,10939,10941,10943],{"class":3605,"line":3612},[3603,10932,5534],{"class":5533},[3603,10934,5538],{"class":5537},[3603,10936,3453],{"class":5541},[3603,10938,5544],{"class":5537},[3603,10940,3453],{"class":5541},[3603,10942,5562],{"class":5537},[3603,10944,5547],{"class":5541},[3603,10946,10947],{"class":3605,"line":3618},[3603,10948,3628],{"emptyLinePlaceholder":3627},[3603,10950,10951,10954,10957],{"class":3605,"line":3624},[3603,10952,10953],{"class":5243},"public",[3603,10955,10956],{"class":5243}," class",[3603,10958,10959],{"class":5537}," EncryptedS3Service\n",[3603,10961,10962],{"class":3605,"line":3631},[3603,10963,5591],{"class":5541},[3603,10965,10966,10969,10972,10975,10978],{"class":3605,"line":3637},[3603,10967,10968],{"class":5243},"    private",[3603,10970,10971],{"class":5243}," readonly",[3603,10973,10974],{"class":5537}," IAmazonS3",[3603,10976,10977],{"class":5576}," _s3",[3603,10979,5547],{"class":5541},[3603,10981,10982],{"class":3605,"line":3643},[3603,10983,10984],{"class":5214},"    \u002F\u002F ARN або alias KMS ключа: \"alias\u002Fmy-app-s3\"\n",[3603,10986,10987,10989,10992,10995,10998,11000,11002],{"class":3605,"line":3649},[3603,10988,10968],{"class":5243},[3603,10990,10991],{"class":5243}," const",[3603,10993,10994],{"class":5243}," string",[3603,10996,10997],{"class":5576}," KmsKeyId",[3603,10999,5580],{"class":5541},[3603,11001,10645],{"class":5224},[3603,11003,5547],{"class":5541},[3603,11005,11006],{"class":3605,"line":3655},[3603,11007,3628],{"emptyLinePlaceholder":3627},[3603,11009,11010,11013,11016,11018,11021,11023,11026,11029,11031,11034],{"class":3605,"line":3661},[3603,11011,11012],{"class":5243},"    public",[3603,11014,11015],{"class":5220}," EncryptedS3Service",[3603,11017,5707],{"class":5541},[3603,11019,11020],{"class":5537},"IAmazonS3",[3603,11022,5225],{"class":5576},[3603,11024,11025],{"class":5541},") => ",[3603,11027,11028],{"class":5576},"_s3",[3603,11030,5580],{"class":5541},[3603,11032,11033],{"class":5576},"s3",[3603,11035,5547],{"class":5541},[3603,11037,11038],{"class":3605,"line":3667},[3603,11039,3628],{"emptyLinePlaceholder":3627},[3603,11041,11042],{"class":3605,"line":3673},[3603,11043,11044],{"class":5214},"    \u002F\u002F Завантажити файл з явним SSE-KMS шифруванням\n",[3603,11046,11047,11049,11052,11055,11058,11060,11062,11065,11067,11069,11072,11074,11077,11080],{"class":3605,"line":3678},[3603,11048,11012],{"class":5243},[3603,11050,11051],{"class":5243}," async",[3603,11053,11054],{"class":5537}," Task",[3603,11056,11057],{"class":5220}," UploadEncryptedAsync",[3603,11059,5707],{"class":5541},[3603,11061,7598],{"class":5243},[3603,11063,11064],{"class":5576}," bucket",[3603,11066,3470],{"class":5541},[3603,11068,7598],{"class":5243},[3603,11070,11071],{"class":5576}," key",[3603,11073,3470],{"class":5541},[3603,11075,11076],{"class":5537},"Stream",[3603,11078,11079],{"class":5576}," data",[3603,11081,6924],{"class":5541},[3603,11083,11084],{"class":3605,"line":3684},[3603,11085,11086],{"class":5541},"    {\n",[3603,11088,11089,11092,11095,11097,11099],{"class":3605,"line":3690},[3603,11090,11091],{"class":5243},"        var",[3603,11093,11094],{"class":5576}," request",[3603,11096,5580],{"class":5541},[3603,11098,5583],{"class":5243},[3603,11100,5586],{"class":5537},[3603,11102,11103],{"class":3605,"line":3696},[3603,11104,5873],{"class":5541},[3603,11106,11107,11110,11112,11115],{"class":3605,"line":3702},[3603,11108,11109],{"class":5576},"            BucketName",[3603,11111,5580],{"class":5541},[3603,11113,11114],{"class":5576},"bucket",[3603,11116,5604],{"class":5541},[3603,11118,11119,11122,11124,11127],{"class":3605,"line":3708},[3603,11120,11121],{"class":5576},"            Key",[3603,11123,5580],{"class":5541},[3603,11125,11126],{"class":5576},"key",[3603,11128,5604],{"class":5541},[3603,11130,11131,11134,11136,11139],{"class":3605,"line":4667},[3603,11132,11133],{"class":5576},"            InputStream",[3603,11135,5580],{"class":5541},[3603,11137,11138],{"class":5576},"data",[3603,11140,5604],{"class":5541},[3603,11142,11143],{"class":3605,"line":4673},[3603,11144,11145],{"class":5214},"            \u002F\u002F Явно вказуємо SSE-KMS (перевизначає default encryption bucket'а)\n",[3603,11147,11148,11151,11153,11156,11158,11161],{"class":3605,"line":4679},[3603,11149,11150],{"class":5576},"            ServerSideEncryptionMethod",[3603,11152,5580],{"class":5541},[3603,11154,11155],{"class":5576},"ServerSideEncryptionMethod",[3603,11157,3453],{"class":5541},[3603,11159,11160],{"class":5576},"AWSKMS",[3603,11162,5604],{"class":5541},[3603,11164,11165,11168,11170,11173],{"class":3605,"line":4685},[3603,11166,11167],{"class":5576},"            ServerSideEncryptionKeyManagementServiceKeyId",[3603,11169,5580],{"class":5541},[3603,11171,11172],{"class":5576},"KmsKeyId",[3603,11174,5604],{"class":5541},[3603,11176,11177],{"class":3605,"line":4691},[3603,11178,11179],{"class":5541},"        };\n",[3603,11181,11182],{"class":3605,"line":4696},[3603,11183,3628],{"emptyLinePlaceholder":3627},[3603,11185,11186,11188,11191,11193,11195,11197,11199,11201,11203,11206],{"class":3605,"line":4701},[3603,11187,11091],{"class":5243},[3603,11189,11190],{"class":5576}," response",[3603,11192,5580],{"class":5541},[3603,11194,5696],{"class":5243},[3603,11196,10977],{"class":5576},[3603,11198,3453],{"class":5541},[3603,11200,5704],{"class":5220},[3603,11202,5707],{"class":5541},[3603,11204,11205],{"class":5576},"request",[3603,11207,5713],{"class":5541},[3603,11209,11210],{"class":3605,"line":4707},[3603,11211,3628],{"emptyLinePlaceholder":3627},[3603,11213,11214],{"class":3605,"line":4713},[3603,11215,11216],{"class":5214},"        \u002F\u002F response.ServerSideEncryptionMethod == \"aws:kms\"\n",[3603,11218,11219],{"class":3605,"line":4719},[3603,11220,11221],{"class":5214},"        \u002F\u002F response.ServerSideEncryptionKeyManagementServiceKeyId — ARN ключа що використовувався\n",[3603,11223,11224],{"class":3605,"line":4724},[3603,11225,3664],{"class":5541},[3603,11227,11228],{"class":3605,"line":4730},[3603,11229,3628],{"emptyLinePlaceholder":3627},[3603,11231,11232],{"class":3605,"line":4736},[3603,11233,11234],{"class":5214},"    \u002F\u002F Читання — прозоре, SDK сам запитує KMS decrypt\n",[3603,11236,11237],{"class":3605,"line":4742},[3603,11238,11239],{"class":5214},"    \u002F\u002F IAM роль вашого застосунку ПОВИННА мати kms:Decrypt на цей ключ\n",[3603,11241,11242,11244,11246,11248,11251,11253,11256,11259,11261,11263,11265,11267,11269,11271],{"class":3605,"line":4748},[3603,11243,11012],{"class":5243},[3603,11245,11051],{"class":5243},[3603,11247,11054],{"class":5537},[3603,11249,11250],{"class":5541},"\u003C",[3603,11252,11076],{"class":5537},[3603,11254,11255],{"class":5541},"> ",[3603,11257,11258],{"class":5220},"DownloadEncryptedAsync",[3603,11260,5707],{"class":5541},[3603,11262,7598],{"class":5243},[3603,11264,11064],{"class":5576},[3603,11266,3470],{"class":5541},[3603,11268,7598],{"class":5243},[3603,11270,11071],{"class":5576},[3603,11272,6924],{"class":5541},[3603,11274,11275],{"class":3605,"line":4753},[3603,11276,11086],{"class":5541},[3603,11278,11279,11281,11283,11285,11287,11289,11291,11294,11296,11298,11300,11302],{"class":3605,"line":4758},[3603,11280,11091],{"class":5243},[3603,11282,11190],{"class":5576},[3603,11284,5580],{"class":5541},[3603,11286,5696],{"class":5243},[3603,11288,10977],{"class":5576},[3603,11290,3453],{"class":5541},[3603,11292,11293],{"class":5220},"GetObjectAsync",[3603,11295,5707],{"class":5541},[3603,11297,11114],{"class":5576},[3603,11299,3470],{"class":5541},[3603,11301,11126],{"class":5576},[3603,11303,5713],{"class":5541},[3603,11305,11306,11309,11311,11313,11316,11319],{"class":3605,"line":6363},[3603,11307,11308],{"class":5533},"        return",[3603,11310,11190],{"class":5576},[3603,11312,3453],{"class":5541},[3603,11314,11315],{"class":5576},"ResponseStream",[3603,11317,11318],{"class":5541},"; ",[3603,11320,11321],{"class":5214},"\u002F\u002F вже розшифровано S3\n",[3603,11323,11324],{"class":3605,"line":6368},[3603,11325,3664],{"class":5541},[3603,11327,11328],{"class":3605,"line":6830},[3603,11329,3628],{"emptyLinePlaceholder":3627},[3603,11331,11332],{"class":3605,"line":6835},[3603,11333,11334],{"class":5214},"    \u002F\u002F Перевірити яким ключем зашифровано об'єкт\n",[3603,11336,11337,11339,11341,11343,11345,11347,11350,11353,11355,11357,11359,11361,11363,11365],{"class":3605,"line":6841},[3603,11338,11012],{"class":5243},[3603,11340,11051],{"class":5243},[3603,11342,11054],{"class":5537},[3603,11344,11250],{"class":5541},[3603,11346,7598],{"class":5243},[3603,11348,11349],{"class":5541},"?> ",[3603,11351,11352],{"class":5220},"GetObjectEncryptionKeyAsync",[3603,11354,5707],{"class":5541},[3603,11356,7598],{"class":5243},[3603,11358,11064],{"class":5576},[3603,11360,3470],{"class":5541},[3603,11362,7598],{"class":5243},[3603,11364,11071],{"class":5576},[3603,11366,6924],{"class":5541},[3603,11368,11369],{"class":3605,"line":6858},[3603,11370,11086],{"class":5541},[3603,11372,11373,11375,11378,11380,11382,11384,11386,11389,11391,11393,11395,11397],{"class":3605,"line":6871},[3603,11374,11091],{"class":5243},[3603,11376,11377],{"class":5576}," meta",[3603,11379,5580],{"class":5541},[3603,11381,5696],{"class":5243},[3603,11383,10977],{"class":5576},[3603,11385,3453],{"class":5541},[3603,11387,11388],{"class":5220},"GetObjectMetadataAsync",[3603,11390,5707],{"class":5541},[3603,11392,11114],{"class":5576},[3603,11394,3470],{"class":5541},[3603,11396,11126],{"class":5576},[3603,11398,5713],{"class":5541},[3603,11400,11401,11403,11405,11407,11410],{"class":3605,"line":6881},[3603,11402,11308],{"class":5533},[3603,11404,11377],{"class":5576},[3603,11406,3453],{"class":5541},[3603,11408,11409],{"class":5576},"ServerSideEncryptionKeyManagementServiceKeyId",[3603,11411,5547],{"class":5541},[3603,11413,11414],{"class":3605,"line":6894},[3603,11415,11416],{"class":5214},"        \u002F\u002F Поверне ARN KMS ключа або null якщо SSE-S3\n",[3603,11418,11419],{"class":3605,"line":6915},[3603,11420,3664],{"class":5541},[3603,11422,11423],{"class":3605,"line":6927},[3603,11424,3670],{"class":5541},[3879,11426,11427,11437,11449],{},[3353,11428,11429,11432,11433,11436],{},[3356,11430,11431],{},"IAM дозволи для SSE-KMS:"," щоб ваш застосунок (EC2 роль, Lambda, ECS task role) міг завантажувати та читати зашифровані SSE-KMS об'єкти, IAM роль повинна мати ",[3356,11434,11435],{},"обидва"," дозволи:",[3369,11438,11439,11444],{},[3372,11440,11441,11443],{},[3432,11442,10498],{}," — для PUT (генерація DEK)",[3372,11445,11446,11448],{},[3432,11447,10501],{}," — для GET (розшифрування DEK)",[3353,11450,11451,11452,11454,11455,11458,11459,11461],{},"Без ",[3432,11453,10501],{}," читання поверне ",[3356,11456,11457],{},"403 Access Denied"," навіть якщо є ",[3432,11460,9512],{},". Це типова помилка при першому налаштуванні.",[5197,11463,11464,11533],{},[5200,11465,11466,11471],{"label":6375},[3353,11467,11468],{},[3356,11469,11470],{},"Налаштування шифрування за замовчуванням для bucket:",[6382,11472,11473,11477,11484,11503,11529],{},[3372,11474,7694,11475],{},[3356,11476,6392],{},[3372,11478,9593,11479,3976,11482],{},[3356,11480,11481],{},"Default encryption",[3356,11483,6401],{},[3372,11485,11486,11489],{},[3356,11487,11488],{},"Encryption type:",[3369,11490,11491,11497],{},[3372,11492,11493,11496],{},[3432,11494,11495],{},"Server-side encryption with Amazon S3 managed keys (SSE-S3)"," — для базового захисту",[3372,11498,11499,11502],{},[3432,11500,11501],{},"Server-side encryption with AWS Key Management Service keys (SSE-KMS)"," — для повного контролю",[3372,11504,11505,11506],{},"Якщо SSE-KMS:\n",[3369,11507,11508,11516,11522],{},[3372,11509,11510,10425,11513,11515],{},[3356,11511,11512],{},"AWS managed key",[3432,11514,10428],{},") — безкоштовний ключ AWS",[3372,11517,11518,11521],{},[3356,11519,11520],{},"Customer managed key"," — ваш власний KMS ключ (більше контролю)",[3372,11523,11524,11525,11528],{},"Увімкніть ",[3356,11526,11527],{},"Bucket Key"," (рекомендовано: зменшує витрати на KMS)",[3372,11530,11531],{},[3356,11532,6410],{},[5200,11534,11535],{"label":6494},[3561,11536,11538],{"className":5205,"code":11537,"language":5207,"meta":3569,"style":3569},"BUCKET=\"my-app-bucket\"\nREGION=\"eu-central-1\"\nKMS_KEY_ID=\"arn:aws:kms:eu-central-1:123456789012:key\u002Fmrk-abc123def456\"\n\n# Встановити SSE-S3 (AES-256) за замовчуванням\naws s3api put-bucket-encryption \\\n    --bucket \"$BUCKET\" \\\n    --server-side-encryption-configuration '{\n        \"Rules\": [{\n            \"ApplyServerSideEncryptionByDefault\": {\n                \"SSEAlgorithm\": \"AES256\"\n            },\n            \"BucketKeyEnabled\": false\n        }]\n    }' \\\n    --region \"$REGION\"\n\n# Встановити SSE-KMS з Customer Managed Key\naws s3api put-bucket-encryption \\\n    --bucket \"$BUCKET\" \\\n    --server-side-encryption-configuration \"{\n        \\\"Rules\\\": [{\n            \\\"ApplyServerSideEncryptionByDefault\\\": {\n                \\\"SSEAlgorithm\\\": \\\"aws:kms\\\",\n                \\\"KMSMasterKeyID\\\": \\\"$KMS_KEY_ID\\\"\n            },\n            \\\"BucketKeyEnabled\\\": true\n        }]\n    }\" \\\n    --region \"$REGION\"\n\n# Перевірити поточне шифрування\naws s3api get-bucket-encryption \\\n    --bucket \"$BUCKET\" \\\n    --region \"$REGION\"\n\n# Завантажити файл з явним SSE-KMS шифруванням\naws s3 cp sensitive-data.csv s3:\u002F\u002F\"$BUCKET\"\u002F \\\n    --sse aws:kms \\\n    --sse-kms-key-id \"$KMS_KEY_ID\" \\\n    --region \"$REGION\"\n",[3432,11539,11540,11548,11556,11566,11570,11575,11586,11598,11605,11610,11615,11620,11624,11629,11634,11641,11651,11655,11660,11670,11682,11688,11702,11714,11735,11754,11758,11769,11773,11780,11790,11794,11799,11810,11822,11832,11836,11841,11862,11872,11885],{"__ignoreMap":3569},[3603,11541,11542,11544,11546],{"class":3605,"line":3606},[3603,11543,6504],{"class":5576},[3603,11545,6507],{"class":5541},[3603,11547,6510],{"class":5224},[3603,11549,11550,11552,11554],{"class":3605,"line":3612},[3603,11551,6515],{"class":5576},[3603,11553,6507],{"class":5541},[3603,11555,6520],{"class":5224},[3603,11557,11558,11561,11563],{"class":3605,"line":3618},[3603,11559,11560],{"class":5576},"KMS_KEY_ID",[3603,11562,6507],{"class":5541},[3603,11564,11565],{"class":5224},"\"arn:aws:kms:eu-central-1:123456789012:key\u002Fmrk-abc123def456\"\n",[3603,11567,11568],{"class":3605,"line":3624},[3603,11569,3628],{"emptyLinePlaceholder":3627},[3603,11571,11572],{"class":3605,"line":3631},[3603,11573,11574],{"class":5214},"# Встановити SSE-S3 (AES-256) за замовчуванням\n",[3603,11576,11577,11579,11581,11584],{"class":3605,"line":3637},[3603,11578,5221],{"class":5220},[3603,11580,5365],{"class":5224},[3603,11582,11583],{"class":5224}," put-bucket-encryption",[3603,11585,5238],{"class":5237},[3603,11587,11588,11590,11592,11594,11596],{"class":3605,"line":3643},[3603,11589,6545],{"class":5243},[3603,11591,6548],{"class":5224},[3603,11593,6551],{"class":5576},[3603,11595,6554],{"class":5224},[3603,11597,5238],{"class":5237},[3603,11599,11600,11603],{"class":3605,"line":3649},[3603,11601,11602],{"class":5243},"    --server-side-encryption-configuration",[3603,11604,5442],{"class":5224},[3603,11606,11607],{"class":3605,"line":3655},[3603,11608,11609],{"class":5224},"        \"Rules\": [{\n",[3603,11611,11612],{"class":3605,"line":3661},[3603,11613,11614],{"class":5224},"            \"ApplyServerSideEncryptionByDefault\": {\n",[3603,11616,11617],{"class":3605,"line":3667},[3603,11618,11619],{"class":5224},"                \"SSEAlgorithm\": \"AES256\"\n",[3603,11621,11622],{"class":3605,"line":3673},[3603,11623,5921],{"class":5224},[3603,11625,11626],{"class":3605,"line":3678},[3603,11627,11628],{"class":5224},"            \"BucketKeyEnabled\": false\n",[3603,11630,11631],{"class":3605,"line":3684},[3603,11632,11633],{"class":5224},"        }]\n",[3603,11635,11636,11639],{"class":3605,"line":3690},[3603,11637,11638],{"class":5224},"    }'",[3603,11640,5238],{"class":5237},[3603,11642,11643,11645,11647,11649],{"class":3605,"line":3696},[3603,11644,6571],{"class":5243},[3603,11646,6548],{"class":5224},[3603,11648,6576],{"class":5576},[3603,11650,6579],{"class":5224},[3603,11652,11653],{"class":3605,"line":3702},[3603,11654,3628],{"emptyLinePlaceholder":3627},[3603,11656,11657],{"class":3605,"line":3708},[3603,11658,11659],{"class":5214},"# Встановити SSE-KMS з Customer Managed Key\n",[3603,11661,11662,11664,11666,11668],{"class":3605,"line":4667},[3603,11663,5221],{"class":5220},[3603,11665,5365],{"class":5224},[3603,11667,11583],{"class":5224},[3603,11669,5238],{"class":5237},[3603,11671,11672,11674,11676,11678,11680],{"class":3605,"line":4673},[3603,11673,6545],{"class":5243},[3603,11675,6548],{"class":5224},[3603,11677,6551],{"class":5576},[3603,11679,6554],{"class":5224},[3603,11681,5238],{"class":5237},[3603,11683,11684,11686],{"class":3605,"line":4679},[3603,11685,11602],{"class":5243},[3603,11687,6680],{"class":5224},[3603,11689,11690,11693,11696,11699],{"class":3605,"line":4685},[3603,11691,11692],{"class":5237},"        \\\"",[3603,11694,11695],{"class":5224},"Rules",[3603,11697,11698],{"class":5237},"\\\"",[3603,11700,11701],{"class":5224},": [{\n",[3603,11703,11704,11707,11710,11712],{"class":3605,"line":4691},[3603,11705,11706],{"class":5237},"            \\\"",[3603,11708,11709],{"class":5224},"ApplyServerSideEncryptionByDefault",[3603,11711,11698],{"class":5237},[3603,11713,5906],{"class":5224},[3603,11715,11716,11719,11722,11724,11726,11728,11731,11733],{"class":3605,"line":4696},[3603,11717,11718],{"class":5237},"                \\\"",[3603,11720,11721],{"class":5224},"SSEAlgorithm",[3603,11723,11698],{"class":5237},[3603,11725,5881],{"class":5224},[3603,11727,11698],{"class":5237},[3603,11729,11730],{"class":5224},"aws:kms",[3603,11732,11698],{"class":5237},[3603,11734,5604],{"class":5224},[3603,11736,11737,11739,11742,11744,11746,11748,11751],{"class":3605,"line":4701},[3603,11738,11718],{"class":5237},[3603,11740,11741],{"class":5224},"KMSMasterKeyID",[3603,11743,11698],{"class":5237},[3603,11745,5881],{"class":5224},[3603,11747,11698],{"class":5237},[3603,11749,11750],{"class":5576},"$KMS_KEY_ID",[3603,11752,11753],{"class":5237},"\\\"\n",[3603,11755,11756],{"class":3605,"line":4707},[3603,11757,5921],{"class":5224},[3603,11759,11760,11762,11764,11766],{"class":3605,"line":4713},[3603,11761,11706],{"class":5237},[3603,11763,10326],{"class":5224},[3603,11765,11698],{"class":5237},[3603,11767,11768],{"class":5224},": true\n",[3603,11770,11771],{"class":3605,"line":4719},[3603,11772,11633],{"class":5224},[3603,11774,11775,11778],{"class":3605,"line":4724},[3603,11776,11777],{"class":5224},"    }\"",[3603,11779,5238],{"class":5237},[3603,11781,11782,11784,11786,11788],{"class":3605,"line":4730},[3603,11783,6571],{"class":5243},[3603,11785,6548],{"class":5224},[3603,11787,6576],{"class":5576},[3603,11789,6579],{"class":5224},[3603,11791,11792],{"class":3605,"line":4736},[3603,11793,3628],{"emptyLinePlaceholder":3627},[3603,11795,11796],{"class":3605,"line":4742},[3603,11797,11798],{"class":5214},"# Перевірити поточне шифрування\n",[3603,11800,11801,11803,11805,11808],{"class":3605,"line":4748},[3603,11802,5221],{"class":5220},[3603,11804,5365],{"class":5224},[3603,11806,11807],{"class":5224}," get-bucket-encryption",[3603,11809,5238],{"class":5237},[3603,11811,11812,11814,11816,11818,11820],{"class":3605,"line":4753},[3603,11813,6545],{"class":5243},[3603,11815,6548],{"class":5224},[3603,11817,6551],{"class":5576},[3603,11819,6554],{"class":5224},[3603,11821,5238],{"class":5237},[3603,11823,11824,11826,11828,11830],{"class":3605,"line":4758},[3603,11825,6571],{"class":5243},[3603,11827,6548],{"class":5224},[3603,11829,6576],{"class":5576},[3603,11831,6579],{"class":5224},[3603,11833,11834],{"class":3605,"line":6363},[3603,11835,3628],{"emptyLinePlaceholder":3627},[3603,11837,11838],{"class":3605,"line":6368},[3603,11839,11840],{"class":5214},"# Завантажити файл з явним SSE-KMS шифруванням\n",[3603,11842,11843,11845,11847,11849,11852,11855,11857,11860],{"class":3605,"line":6830},[3603,11844,5221],{"class":5220},[3603,11846,5225],{"class":5224},[3603,11848,5228],{"class":5224},[3603,11850,11851],{"class":5224}," sensitive-data.csv",[3603,11853,11854],{"class":5224}," s3:\u002F\u002F\"",[3603,11856,6551],{"class":5576},[3603,11858,11859],{"class":5224},"\"\u002F",[3603,11861,5238],{"class":5237},[3603,11863,11864,11867,11870],{"class":3605,"line":6835},[3603,11865,11866],{"class":5243},"    --sse",[3603,11868,11869],{"class":5224}," aws:kms",[3603,11871,5238],{"class":5237},[3603,11873,11874,11877,11879,11881,11883],{"class":3605,"line":6841},[3603,11875,11876],{"class":5243},"    --sse-kms-key-id",[3603,11878,6548],{"class":5224},[3603,11880,11750],{"class":5576},[3603,11882,6554],{"class":5224},[3603,11884,5238],{"class":5237},[3603,11886,11887,11889,11891,11893],{"class":3605,"line":6858},[3603,11888,6571],{"class":5243},[3603,11890,6548],{"class":5224},[3603,11892,6576],{"class":5576},[3603,11894,6579],{"class":5224},[4801,11896,11898,11907,11910,11917,11923,11927,11934,11945,11956,11960,11970,11974,11978,11981],{"title":11897},"aws s3api get-bucket-encryption",[4805,11899,11901,4841,11904],{"className":11900},[3605],[3603,11902,7030],{"className":11903},[7029],[3356,11905,11906],{},"aws s3api get-bucket-encryption --bucket my-app-bucket --region eu-central-1",[4805,11908,5618],{"className":11909},[3605],[4805,11911,7040,11913,8887],{"className":11912},[3605],[3603,11914,11916],{"className":11915},[7044],"\"ServerSideEncryptionConfiguration\"",[4805,11918,8891,11920,7046],{"className":11919},[3605],[3603,11921,8103],{"className":11922},[7044],[4805,11924,11926],{"className":11925},[3605],"      {",[4805,11928,10722,11930,8887],{"className":11929},[3605],[3603,11931,11933],{"className":11932},[7044],"\"ApplyServerSideEncryptionByDefault\"",[4805,11935,10730,11937,5881,11941,7063],{"className":11936},[3605],[3603,11938,11940],{"className":11939},[7044],"\"SSEAlgorithm\"",[3603,11942,11944],{"className":11943},[4811],"\"aws:kms\"",[4805,11946,10730,11948,5881,11952],{"className":11947},[3605],[3603,11949,11951],{"className":11950},[7044],"\"KMSMasterKeyID\"",[3603,11953,11955],{"className":11954},[4811],"\"arn:aws:kms:eu-central-1:123456789012:key\u002Fmrk-abc123\"",[4805,11957,11959],{"className":11958},[3605],"        },",[4805,11961,10722,11963,5881,11967],{"className":11962},[3605],[3603,11964,11966],{"className":11965},[7044],"\"BucketKeyEnabled\"",[3603,11968,6905],{"className":11969},[7085],[4805,11971,11973],{"className":11972},[3605],"      }",[4805,11975,11977],{"className":11976},[3605],"    ]",[4805,11979,8932],{"className":11980},[3605],[4805,11982,5645],{"className":11983},[3605],[3405,11985],{},[3348,11987,11989],{"id":11988},"s3-presigned-urls-тимчасовий-доступ-до-приватних-обєктів","S3 Presigned URLs — тимчасовий доступ до приватних об'єктів",[3353,11991,11992,11995],{},[3356,11993,11994],{},"Presigned URL"," — це спеціально підписана URL-адреса, яка надає тимчасовий доступ до приватного S3 об'єкту без необхідності давати постійні права. Типова тривалість: від 1 хвилини до 7 днів.",[3353,11997,11998],{},[3356,11999,12000],{},"Навіщо це потрібно?",[3353,12002,12003,12004,12007],{},"S3 bucket приватний — ніхто ззовні не може читати чи записувати файли. Але іноді треба дати ",[3356,12005,12006],{},"тимчасовий"," доступ конкретній людині, не відкриваючи bucket публічно і не передаючи AWS-ключі клієнту. Presigned URL вирішує саме це.",[3353,12009,12010],{},[3356,12011,12012],{},"Три типових сценарії:",[3353,12014,12015],{},[3356,12016,12017],{},"1. Скачування приватного файлу (Presigned GET)",[3353,12019,12020],{},"Користувач натискає «Завантажити звіт» у вашому застосунку. Файл зберігається в закритому S3 bucket — напряму його не відкрити. Бекенд перевіряє права користувача, генерує Presigned GET URL (діє 15 хвилин) і повертає її фронтенду. Браузер відкриває цю URL і скачує файл напряму з S3 — трафік не йде через ваш сервер.",[3353,12022,12023],{},[3356,12024,12025],{},"2. Завантаження файлу від користувача напряму в S3 (Presigned PUT)",[3353,12027,12028,12029,12031],{},"Користувач хоче завантажити фото. Якщо передавати файл через бекенд — він займає пам'ять і пропускну здатність сервера. Натомість: бекенд генерує Presigned PUT URL (дозвіл записати один конкретний файл) і повертає її фронтенду. Фронтенд робить ",[3432,12030,6134],{}," запит напряму в S3 — бекенд файл взагалі не бачить.",[3353,12033,12034],{},[3356,12035,12036],{},"3. Посилання в email або месенджері",[3353,12038,12039],{},"Після генерації звіту (PDF, Excel) система відправляє email з кнопкою «Переглянути». За кнопкою — Presigned GET URL з терміном 24–72 години. Отримувач клікає і отримує файл. Якщо термін вийшов — посилання більше не працює, файл залишається закритим.",[4801,12041,12043,12050,12058,12062,12065,12072,12080,12084,12088,12092,12096,12100,12103,12110,12118,12122,12134,12142,12149,12156,12163,12170,12174,12177,12184,12192,12200,12203,12210,12216],{"title":12042},"Демо: Сценарій 1 — скачування приватного файлу",[4805,12044,12046],{"className":12045},[3605],[3603,12047,12049],{"className":12048},[4811],"# 1. Користувач натискає «Завантажити звіт» у браузері",[4805,12051,12053,12057],{"className":12052},[3605],[3603,12054,12056],{"className":12055},[4819],"Browser → Backend:","  GET \u002Fapi\u002Freports\u002F42\u002Fdownload",[4805,12059,12061],{"className":12060},[3605],"                    Authorization: Bearer eyJhbGci...",[4805,12063,4841],{"className":12064},[3605],[4805,12066,12068],{"className":12067},[3605],[3603,12069,12071],{"className":12070},[4811],"# 2. Бекенд перевіряє права і генерує Presigned URL",[4805,12073,12075,12079],{"className":12074},[3605],[3603,12076,12078],{"className":12077},[4819],"Backend → AWS S3:","   GeneratePresignedUrl(",[4805,12081,12083],{"className":12082},[3605],"                      bucket=\"reports-private\",",[4805,12085,12087],{"className":12086},[3605],"                      key=\"reports\u002F2026\u002Fq1-report.pdf\",",[4805,12089,12091],{"className":12090},[3605],"                      expires=15min,",[4805,12093,12095],{"className":12094},[3605],"                      verb=GET",[4805,12097,12099],{"className":12098},[3605],"                    )",[4805,12101,4841],{"className":12102},[3605],[4805,12104,12106],{"className":12105},[3605],[3603,12107,12109],{"className":12108},[4811],"# 3. Бекенд повертає URL фронтенду (файл ще НЕ відкрито)",[4805,12111,12113,12117],{"className":12112},[3605],[3603,12114,12116],{"className":12115},[4819],"Backend → Browser:","  200 OK",[4805,12119,12121],{"className":12120},[3605],"                    {",[4805,12123,12125,12126,5881,12130],{"className":12124},[3605],"                      ",[3603,12127,12129],{"className":12128},[7044],"\"url\"",[3603,12131,12133],{"className":12132},[4811],"\"https:\u002F\u002Freports-private.s3.amazonaws.com\u002Freports\u002F2026\u002Fq1-report.pdf",[4805,12135,12137,12138],{"className":12136},[3605],"                               ",[3603,12139,12141],{"className":12140},[4811],"?X-Amz-Algorithm=AWS4-HMAC-SHA256",[4805,12143,12137,12145],{"className":12144},[3605],[3603,12146,12148],{"className":12147},[4811],"&X-Amz-Credential=AKIA...%2F20260528%2Feu-central-1%2Fs3%2Faws4_request",[4805,12150,12137,12152],{"className":12151},[3605],[3603,12153,12155],{"className":12154},[4811],"&X-Amz-Date=20260528T140000Z",[4805,12157,12137,12159],{"className":12158},[3605],[3603,12160,12162],{"className":12161},[4811],"&X-Amz-Expires=900",[4805,12164,12137,12166],{"className":12165},[3605],[3603,12167,12169],{"className":12168},[4811],"&X-Amz-Signature=3ae45f...\"",[4805,12171,12173],{"className":12172},[3605],"                    }",[4805,12175,4841],{"className":12176},[3605],[4805,12178,12180],{"className":12179},[3605],[3603,12181,12183],{"className":12182},[4811],"# 4. Браузер відкриває URL — запит іде НАПРЯМУ в S3, минаючи бекенд",[4805,12185,12187,12191],{"className":12186},[3605],[3603,12188,12190],{"className":12189},[4819],"Browser → S3:","       GET \u002Freports\u002F2026\u002Fq1-report.pdf?X-Amz-Expires=900&...",[4805,12193,12195,12199],{"className":12194},[3605],[3603,12196,12198],{"className":12197},[4819],"S3 → Browser:","       200 OK + q1-report.pdf (2.4 MB)",[4805,12201,4841],{"className":12202},[3605],[4805,12204,12206],{"className":12205},[3605],[3603,12207,12209],{"className":12208},[4811],"# 5. Той самий запит через 16 хвилин",[4805,12211,12213,12191],{"className":12212},[3605],[3603,12214,12190],{"className":12215},[4819],[4805,12217,12219,12222,12223],{"className":12218},[3605],[3603,12220,12198],{"className":12221},[4819],"       ",[3603,12224,12226],{"className":12225},[4864],"403 Forbidden — Request has expired",[4801,12228,12230,12237,12244,12264,12267,12274,12282,12286,12289,12295,12298,12309,12321,12324,12327,12334,12341,12345,12349,12359,12362,12369,12376,12386,12394,12397,12404],{"title":12229},"Демо: Сценарій 2 — завантаження фото напряму в S3 (PUT)",[4805,12231,12233],{"className":12232},[3605],[3603,12234,12236],{"className":12235},[4811],"# 1. Користувач вибирає фото у формі",[4805,12238,12240,12243],{"className":12239},[3605],[3603,12241,12056],{"className":12242},[4819],"  POST \u002Fapi\u002Fusers\u002Fme\u002Favatar\u002Fupload-url",[4805,12245,12247,12248,5881,12252,3470,12256,5881,12260,8179],{"className":12246},[3605],"                    { ",[3603,12249,12251],{"className":12250},[7044],"\"filename\"",[3603,12253,12255],{"className":12254},[4811],"\"photo.jpg\"",[3603,12257,12259],{"className":12258},[7044],"\"contentType\"",[3603,12261,12263],{"className":12262},[4811],"\"image\u002Fjpeg\"",[4805,12265,4841],{"className":12266},[3605],[4805,12268,12270],{"className":12269},[3605],[3603,12271,12273],{"className":12272},[4811],"# 2. Бекенд генерує унікальний ключ та Presigned PUT URL",[4805,12275,12277,12281],{"className":12276},[3605],[3603,12278,12280],{"className":12279},[4819],"Backend logic:","      key = \"avatars\u002Fusers\u002Fusr_789\u002F2026-05-28_a3f1c.jpg\"",[4805,12283,12285],{"className":12284},[3605],"                    GeneratePresignedUrl(key, verb=PUT, expires=5min)",[4805,12287,4841],{"className":12288},[3605],[4805,12290,12292,12117],{"className":12291},[3605],[3603,12293,12116],{"className":12294},[4819],[4805,12296,12121],{"className":12297},[3605],[4805,12299,12125,12301,5881,12305,7063],{"className":12300},[3605],[3603,12302,12304],{"className":12303},[7044],"\"uploadUrl\"",[3603,12306,12308],{"className":12307},[4811],"\"https:\u002F\u002Fmy-bucket.s3.amazonaws.com\u002Favatars\u002Fusers\u002Fusr_789\u002F...\"",[4805,12310,12125,12312,12316,12317],{"className":12311},[3605],[3603,12313,12315],{"className":12314},[7044],"\"finalKey\"",":  ",[3603,12318,12320],{"className":12319},[4811],"\"avatars\u002Fusers\u002Fusr_789\u002F2026-05-28_a3f1c.jpg\"",[4805,12322,12173],{"className":12323},[3605],[4805,12325,4841],{"className":12326},[3605],[4805,12328,12330],{"className":12329},[3605],[3603,12331,12333],{"className":12332},[4811],"# 3. Браузер завантажує файл НАПРЯМУ в S3 — бекенд файл не бачить!",[4805,12335,12337,12340],{"className":12336},[3605],[3603,12338,12190],{"className":12339},[4819],"       PUT \u002Favatars\u002Fusers\u002Fusr_789\u002F2026-05-28_a3f1c.jpg?...",[4805,12342,12344],{"className":12343},[3605],"                    Content-Type: image\u002Fjpeg",[4805,12346,12348],{"className":12347},[3605],"                    [бінарні дані фото, 3.1 MB]",[4805,12350,12352,12222,12355],{"className":12351},[3605],[3603,12353,12198],{"className":12354},[4819],[3603,12356,12358],{"className":12357},[4811],"200 OK",[4805,12360,4841],{"className":12361},[3605],[4805,12363,12365],{"className":12364},[3605],[3603,12366,12368],{"className":12367},[4811],"# 4. Браузер повідомляє бекенд що upload завершено",[4805,12370,12372,12375],{"className":12371},[3605],[3603,12373,12056],{"className":12374},[4819],"  PATCH \u002Fapi\u002Fusers\u002Fme\u002Favatar",[4805,12377,12247,12379,5881,12383,8179],{"className":12378},[3605],[3603,12380,12382],{"className":12381},[7044],"\"key\"",[3603,12384,12320],{"className":12385},[4811],[4805,12387,12389,12393],{"className":12388},[3605],[3603,12390,12392],{"className":12391},[4819],"Backend:","            Зберігає ключ у БД, не торкаючись файлу",[4805,12395,4841],{"className":12396},[3605],[4805,12398,12400],{"className":12399},[3605],[3603,12401,12403],{"className":12402},[4900],"# Результат: 3.1 MB НЕ пройшли через бекенд-сервер",[4805,12405,12407],{"className":12406},[3605],[3603,12408,12410],{"className":12409},[4900],"# При 1000 завантажень\u002Fдень — це ~3 GB трафіку зекономлено на сервері",[4801,12412,12414,12421,12429,12433,12436,12443,12450,12454,12457,12464,12472,12476,12479,12486,12493,12503,12506,12513,12519,12528],{"title":12413},"Демо: Сценарій 3 — посилання у email",[4805,12415,12417],{"className":12416},[3605],[3603,12418,12420],{"className":12419},[4811],"# Тригер: генерація місячного звіту о 00:00",[4805,12422,12424,12428],{"className":12423},[3605],[3603,12425,12427],{"className":12426},[4819],"CronJob:","            GenerateMonthlyReport() → finance\u002F2026-04\u002Freport.xlsx",[4805,12430,12432],{"className":12431},[3605],"                    S3.PutObject(\"reports-private\", \"finance\u002F2026-04\u002Freport.xlsx\")",[4805,12434,4841],{"className":12435},[3605],[4805,12437,12439],{"className":12438},[3605],[3603,12440,12442],{"className":12441},[4811],"# Генерація Presigned URL та відправка email",[4805,12444,12446,12449],{"className":12445},[3605],[3603,12447,12392],{"className":12448},[4819],"            url = GeneratePresignedUrl(expires=72h)",[4805,12451,12453],{"className":12452},[3605],"                    SendEmail(to=\"cfo@company.com\", template=\"report-ready\", url=url)",[4805,12455,4841],{"className":12456},[3605],[4805,12458,12460],{"className":12459},[3605],[3603,12461,12463],{"className":12462},[4811],"# Email отримано (через 2 години після генерації)",[4805,12465,12467,12471],{"className":12466},[3605],[3603,12468,12470],{"className":12469},[4819],"Email text:","         \"Звіт за квітень готовий.\"",[4805,12473,12475],{"className":12474},[3605],"                    [Переглянути звіт →] → https:\u002F\u002Freports-private.s3.amazonaws.com\u002F...&X-Amz-Expires=259200",[4805,12477,4841],{"className":12478},[3605],[4805,12480,12482],{"className":12481},[3605],[3603,12483,12485],{"className":12484},[4811],"# CFO клікає — 70 годин від генерації, ще в межах 72h",[4805,12487,12489,12492],{"className":12488},[3605],[3603,12490,12190],{"className":12491},[4819],"       GET \u002Ffinance\u002F2026-04\u002Freport.xlsx?...",[4805,12494,12496,12222,12499,12502],{"className":12495},[3605],[3603,12497,12198],{"className":12498},[4819],[3603,12500,12358],{"className":12501},[4811]," + report.xlsx (580 KB)",[4805,12504,4841],{"className":12505},[3605],[4805,12507,12509],{"className":12508},[3605],[3603,12510,12512],{"className":12511},[4811],"# Помічник намагається відкрити той самий лист через 3 дні",[4805,12514,12516,12492],{"className":12515},[3605],[3603,12517,12190],{"className":12518},[4819],[4805,12520,12522,12222,12525],{"className":12521},[3605],[3603,12523,12198],{"className":12524},[4819],[3603,12526,12226],{"className":12527},[4864],[4805,12529,12531,12532],{"className":12530},[3605],"                    ",[3603,12533,12535],{"className":12534},[4900],"Файл у bucket цілий, але URL вже не діє. Bucket залишається закритим.",[3561,12537,12539],{"className":5519,"code":12538,"language":5521,"meta":3569,"style":3569},"\u002F\u002F AWS SDK for .NET — генерація Presigned URL\nusing Amazon.S3;\nusing Amazon.S3.Model;\n\npublic class S3Service\n{\n    private readonly IAmazonS3 _s3;\n\n    public S3Service(IAmazonS3 s3) => _s3 = s3;\n\n    \u002F\u002F Presigned GET URL — для завантаження файлу\n    public string GenerateDownloadUrl(string bucket, string key, int expiresInMinutes = 15)\n    {\n        var request = new GetPreSignedUrlRequest\n        {\n            BucketName = bucket,\n            Key = key,\n            Expires = DateTime.UtcNow.AddMinutes(expiresInMinutes),\n            Verb = HttpVerb.GET\n        };\n        return _s3.GetPreSignedURL(request);\n    }\n\n    \u002F\u002F Presigned PUT URL — для завантаження файлу напряму з браузера\n    public string GenerateUploadUrl(string bucket, string key,\n        string contentType, int expiresInMinutes = 5)\n    {\n        var request = new GetPreSignedUrlRequest\n        {\n            BucketName = bucket,\n            Key = key,\n            Expires = DateTime.UtcNow.AddMinutes(expiresInMinutes),\n            Verb = HttpVerb.PUT,\n            ContentType = contentType\n        };\n        return _s3.GetPreSignedURL(request);\n    }\n}\n",[3432,12540,12541,12546,12558,12574,12578,12587,12591,12603,12607,12630,12634,12639,12675,12679,12692,12696,12706,12716,12742,12757,12761,12778,12782,12786,12791,12814,12835,12839,12851,12855,12865,12875,12897,12911,12921,12925,12941,12945],{"__ignoreMap":3569},[3603,12542,12543],{"class":3605,"line":3606},[3603,12544,12545],{"class":5214},"\u002F\u002F AWS SDK for .NET — генерація Presigned URL\n",[3603,12547,12548,12550,12552,12554,12556],{"class":3605,"line":3612},[3603,12549,5534],{"class":5533},[3603,12551,5538],{"class":5537},[3603,12553,3453],{"class":5541},[3603,12555,5544],{"class":5537},[3603,12557,5547],{"class":5541},[3603,12559,12560,12562,12564,12566,12568,12570,12572],{"class":3605,"line":3618},[3603,12561,5534],{"class":5533},[3603,12563,5538],{"class":5537},[3603,12565,3453],{"class":5541},[3603,12567,5544],{"class":5537},[3603,12569,3453],{"class":5541},[3603,12571,5562],{"class":5537},[3603,12573,5547],{"class":5541},[3603,12575,12576],{"class":3605,"line":3624},[3603,12577,3628],{"emptyLinePlaceholder":3627},[3603,12579,12580,12582,12584],{"class":3605,"line":3631},[3603,12581,10953],{"class":5243},[3603,12583,10956],{"class":5243},[3603,12585,12586],{"class":5537}," S3Service\n",[3603,12588,12589],{"class":3605,"line":3637},[3603,12590,5591],{"class":5541},[3603,12592,12593,12595,12597,12599,12601],{"class":3605,"line":3643},[3603,12594,10968],{"class":5243},[3603,12596,10971],{"class":5243},[3603,12598,10974],{"class":5537},[3603,12600,10977],{"class":5576},[3603,12602,5547],{"class":5541},[3603,12604,12605],{"class":3605,"line":3649},[3603,12606,3628],{"emptyLinePlaceholder":3627},[3603,12608,12609,12611,12614,12616,12618,12620,12622,12624,12626,12628],{"class":3605,"line":3655},[3603,12610,11012],{"class":5243},[3603,12612,12613],{"class":5220}," S3Service",[3603,12615,5707],{"class":5541},[3603,12617,11020],{"class":5537},[3603,12619,5225],{"class":5576},[3603,12621,11025],{"class":5541},[3603,12623,11028],{"class":5576},[3603,12625,5580],{"class":5541},[3603,12627,11033],{"class":5576},[3603,12629,5547],{"class":5541},[3603,12631,12632],{"class":3605,"line":3661},[3603,12633,3628],{"emptyLinePlaceholder":3627},[3603,12635,12636],{"class":3605,"line":3667},[3603,12637,12638],{"class":5214},"    \u002F\u002F Presigned GET URL — для завантаження файлу\n",[3603,12640,12641,12643,12645,12648,12650,12652,12654,12656,12658,12660,12662,12665,12668,12670,12673],{"class":3605,"line":3673},[3603,12642,11012],{"class":5243},[3603,12644,10994],{"class":5243},[3603,12646,12647],{"class":5220}," GenerateDownloadUrl",[3603,12649,5707],{"class":5541},[3603,12651,7598],{"class":5243},[3603,12653,11064],{"class":5576},[3603,12655,3470],{"class":5541},[3603,12657,7598],{"class":5243},[3603,12659,11071],{"class":5576},[3603,12661,3470],{"class":5541},[3603,12663,12664],{"class":5243},"int",[3603,12666,12667],{"class":5576}," expiresInMinutes",[3603,12669,5580],{"class":5541},[3603,12671,12672],{"class":5943},"15",[3603,12674,6924],{"class":5541},[3603,12676,12677],{"class":3605,"line":3678},[3603,12678,11086],{"class":5541},[3603,12680,12681,12683,12685,12687,12689],{"class":3605,"line":3684},[3603,12682,11091],{"class":5243},[3603,12684,11094],{"class":5576},[3603,12686,5580],{"class":5541},[3603,12688,5583],{"class":5243},[3603,12690,12691],{"class":5537}," GetPreSignedUrlRequest\n",[3603,12693,12694],{"class":3605,"line":3690},[3603,12695,5873],{"class":5541},[3603,12697,12698,12700,12702,12704],{"class":3605,"line":3696},[3603,12699,11109],{"class":5576},[3603,12701,5580],{"class":5541},[3603,12703,11114],{"class":5576},[3603,12705,5604],{"class":5541},[3603,12707,12708,12710,12712,12714],{"class":3605,"line":3702},[3603,12709,11121],{"class":5576},[3603,12711,5580],{"class":5541},[3603,12713,11126],{"class":5576},[3603,12715,5604],{"class":5541},[3603,12717,12718,12721,12723,12725,12727,12729,12731,12734,12736,12739],{"class":3605,"line":3708},[3603,12719,12720],{"class":5576},"            Expires",[3603,12722,5580],{"class":5541},[3603,12724,5621],{"class":5576},[3603,12726,3453],{"class":5541},[3603,12728,5626],{"class":5576},[3603,12730,3453],{"class":5541},[3603,12732,12733],{"class":5220},"AddMinutes",[3603,12735,5707],{"class":5541},[3603,12737,12738],{"class":5576},"expiresInMinutes",[3603,12740,12741],{"class":5541},"),\n",[3603,12743,12744,12747,12749,12752,12754],{"class":3605,"line":4667},[3603,12745,12746],{"class":5576},"            Verb",[3603,12748,5580],{"class":5541},[3603,12750,12751],{"class":5576},"HttpVerb",[3603,12753,3453],{"class":5541},[3603,12755,12756],{"class":5576},"GET\n",[3603,12758,12759],{"class":3605,"line":4673},[3603,12760,11179],{"class":5541},[3603,12762,12763,12765,12767,12769,12772,12774,12776],{"class":3605,"line":4679},[3603,12764,11308],{"class":5533},[3603,12766,10977],{"class":5576},[3603,12768,3453],{"class":5541},[3603,12770,12771],{"class":5220},"GetPreSignedURL",[3603,12773,5707],{"class":5541},[3603,12775,11205],{"class":5576},[3603,12777,5713],{"class":5541},[3603,12779,12780],{"class":3605,"line":4685},[3603,12781,3664],{"class":5541},[3603,12783,12784],{"class":3605,"line":4691},[3603,12785,3628],{"emptyLinePlaceholder":3627},[3603,12787,12788],{"class":3605,"line":4696},[3603,12789,12790],{"class":5214},"    \u002F\u002F Presigned PUT URL — для завантаження файлу напряму з браузера\n",[3603,12792,12793,12795,12797,12800,12802,12804,12806,12808,12810,12812],{"class":3605,"line":4701},[3603,12794,11012],{"class":5243},[3603,12796,10994],{"class":5243},[3603,12798,12799],{"class":5220}," GenerateUploadUrl",[3603,12801,5707],{"class":5541},[3603,12803,7598],{"class":5243},[3603,12805,11064],{"class":5576},[3603,12807,3470],{"class":5541},[3603,12809,7598],{"class":5243},[3603,12811,11071],{"class":5576},[3603,12813,5604],{"class":5541},[3603,12815,12816,12819,12822,12824,12826,12828,12830,12833],{"class":3605,"line":4707},[3603,12817,12818],{"class":5243},"        string",[3603,12820,12821],{"class":5576}," contentType",[3603,12823,3470],{"class":5541},[3603,12825,12664],{"class":5243},[3603,12827,12667],{"class":5576},[3603,12829,5580],{"class":5541},[3603,12831,12832],{"class":5943},"5",[3603,12834,6924],{"class":5541},[3603,12836,12837],{"class":3605,"line":4713},[3603,12838,11086],{"class":5541},[3603,12840,12841,12843,12845,12847,12849],{"class":3605,"line":4719},[3603,12842,11091],{"class":5243},[3603,12844,11094],{"class":5576},[3603,12846,5580],{"class":5541},[3603,12848,5583],{"class":5243},[3603,12850,12691],{"class":5537},[3603,12852,12853],{"class":3605,"line":4724},[3603,12854,5873],{"class":5541},[3603,12856,12857,12859,12861,12863],{"class":3605,"line":4730},[3603,12858,11109],{"class":5576},[3603,12860,5580],{"class":5541},[3603,12862,11114],{"class":5576},[3603,12864,5604],{"class":5541},[3603,12866,12867,12869,12871,12873],{"class":3605,"line":4736},[3603,12868,11121],{"class":5576},[3603,12870,5580],{"class":5541},[3603,12872,11126],{"class":5576},[3603,12874,5604],{"class":5541},[3603,12876,12877,12879,12881,12883,12885,12887,12889,12891,12893,12895],{"class":3605,"line":4742},[3603,12878,12720],{"class":5576},[3603,12880,5580],{"class":5541},[3603,12882,5621],{"class":5576},[3603,12884,3453],{"class":5541},[3603,12886,5626],{"class":5576},[3603,12888,3453],{"class":5541},[3603,12890,12733],{"class":5220},[3603,12892,5707],{"class":5541},[3603,12894,12738],{"class":5576},[3603,12896,12741],{"class":5541},[3603,12898,12899,12901,12903,12905,12907,12909],{"class":3605,"line":4748},[3603,12900,12746],{"class":5576},[3603,12902,5580],{"class":5541},[3603,12904,12751],{"class":5576},[3603,12906,3453],{"class":5541},[3603,12908,6134],{"class":5576},[3603,12910,5604],{"class":5541},[3603,12912,12913,12916,12918],{"class":3605,"line":4753},[3603,12914,12915],{"class":5576},"            ContentType",[3603,12917,5580],{"class":5541},[3603,12919,12920],{"class":5576},"contentType\n",[3603,12922,12923],{"class":3605,"line":4758},[3603,12924,11179],{"class":5541},[3603,12926,12927,12929,12931,12933,12935,12937,12939],{"class":3605,"line":6363},[3603,12928,11308],{"class":5533},[3603,12930,10977],{"class":5576},[3603,12932,3453],{"class":5541},[3603,12934,12771],{"class":5220},[3603,12936,5707],{"class":5541},[3603,12938,11205],{"class":5576},[3603,12940,5713],{"class":5541},[3603,12942,12943],{"class":3605,"line":6368},[3603,12944,3664],{"class":5541},[3603,12946,12947],{"class":3605,"line":6830},[3603,12948,3670],{"class":5541},[3353,12950,12951],{},[3356,12952,12953],{},"Використання Presigned PUT URL у JavaScript:",[3561,12955,12959],{"className":12956,"code":12957,"language":12958,"meta":3569,"style":3569},"language-javascript shiki shiki-themes light-plus dark-plus dark-plus","\u002F\u002F Отримуємо URL з бекенду\nconst { uploadUrl } = await fetch('\u002Fapi\u002Ffiles\u002Fupload-url?filename=photo.jpg')\n\n\u002F\u002F Завантажуємо файл напряму в S3 — бекенд не торкається файлу!\nawait fetch(uploadUrl, {\n    method: 'PUT',\n    body: file,\n    headers: { 'Content-Type': 'image\u002Fjpeg' },\n})\n","javascript",[3432,12960,12961,12966,12993,12997,13002,13015,13025,13035,13052],{"__ignoreMap":3569},[3603,12962,12963],{"class":3605,"line":3606},[3603,12964,12965],{"class":5214},"\u002F\u002F Отримуємо URL з бекенду\n",[3603,12967,12968,12971,12974,12978,12981,12983,12986,12988,12991],{"class":3605,"line":3612},[3603,12969,12970],{"class":5243},"const",[3603,12972,12973],{"class":5541}," { ",[3603,12975,12977],{"class":12976},"s-QsJ","uploadUrl",[3603,12979,12980],{"class":5541}," } = ",[3603,12982,5696],{"class":5533},[3603,12984,12985],{"class":5220}," fetch",[3603,12987,5707],{"class":5541},[3603,12989,12990],{"class":5224},"'\u002Fapi\u002Ffiles\u002Fupload-url?filename=photo.jpg'",[3603,12992,6924],{"class":5541},[3603,12994,12995],{"class":3605,"line":3618},[3603,12996,3628],{"emptyLinePlaceholder":3627},[3603,12998,12999],{"class":3605,"line":3624},[3603,13000,13001],{"class":5214},"\u002F\u002F Завантажуємо файл напряму в S3 — бекенд не торкається файлу!\n",[3603,13003,13004,13006,13008,13010,13012],{"class":3605,"line":3631},[3603,13005,5696],{"class":5533},[3603,13007,12985],{"class":5220},[3603,13009,5707],{"class":5541},[3603,13011,12977],{"class":5576},[3603,13013,13014],{"class":5541},", {\n",[3603,13016,13017,13020,13023],{"class":3605,"line":3637},[3603,13018,13019],{"class":5576},"    method:",[3603,13021,13022],{"class":5224}," 'PUT'",[3603,13024,5604],{"class":5541},[3603,13026,13027,13030,13033],{"class":3605,"line":3643},[3603,13028,13029],{"class":5576},"    body:",[3603,13031,13032],{"class":5576}," file",[3603,13034,5604],{"class":5541},[3603,13036,13037,13040,13042,13045,13047,13050],{"class":3605,"line":3649},[3603,13038,13039],{"class":5576},"    headers:",[3603,13041,12973],{"class":5541},[3603,13043,13044],{"class":5224},"'Content-Type'",[3603,13046,5629],{"class":5576},[3603,13048,13049],{"class":5224}," 'image\u002Fjpeg'",[3603,13051,7450],{"class":5541},[3603,13053,13054],{"class":3605,"line":3655},[3603,13055,13056],{"class":5541},"})\n",[3405,13058],{},[3348,13060,13062],{"id":13061},"s3-static-website-hosting","S3 Static Website Hosting",[3353,13064,13065,13067],{},[3356,13066,13062],{}," — можливість використовувати S3 як хостинг для статичних веб-сайтів: HTML, CSS, JavaScript, зображення. Це ідеально для React\u002FVue\u002FAngular SPA (Single Page Application).",[3353,13069,13070],{},[3356,13071,13072],{},"Переваги перед традиційним хостингом:",[3369,13074,13075,13080,13086],{},[3372,13076,13077,13079],{},[3356,13078,3732],{}," S3 Static Hosting коштує копійки (зберігання + трафік), немає витрат на сервер",[3372,13081,13082,13085],{},[3356,13083,13084],{},"Масштабованість:"," S3 витримає будь-яке навантаження без конфігурації",[3372,13087,13088,13091],{},[3356,13089,13090],{},"Надійність:"," 99.99% доступність без жодних зусиль",[3353,13093,13094],{},[3356,13095,13096],{},"Особливість SPA — проблема з прямими посиланнями",[3353,13098,13099,13100,3976,13103,3470,13106,3976,13109,13112],{},"У звичайному сайті кожна сторінка — це окремий файл: ",[3432,13101,13102],{},"\u002Fabout",[3432,13104,13105],{},"about.html",[3432,13107,13108],{},"\u002Fcontact",[3432,13110,13111],{},"contact.html",". Сервер шукає файл за URL і повертає його.",[3353,13114,13115,13116,13119,13120,13123,13124,3470,13126,3470,13129,13132,13133,3453],{},"У React\u002FVue\u002FAngular SPA на диску є ",[3356,13117,13118],{},"тільки один файл"," — ",[3432,13121,13122],{},"index.html",". Всі «сторінки» (",[3432,13125,13102],{},[3432,13127,13128],{},"\u002Fusers\u002F123",[3432,13130,13131],{},"\u002Fdashboard\u002Fsettings",") — це JavaScript-маршрути, які React Router обробляє вже у браузері після завантаження ",[3432,13134,13122],{},[3353,13136,13137],{},"Проблема виникає в двох випадках:",[3369,13139,13140,13150],{},[3372,13141,13142,13143,4841,13146,13149],{},"Користувач ",[3356,13144,13145],{},"відкрив пряме посилання",[3432,13147,13148],{},"mysite.com\u002Fusers\u002F123"," у новій вкладці",[3372,13151,13152,13153,13156,13157],{},"Користувач натиснув ",[3356,13154,13155],{},"F5"," (оновити сторінку) перебуваючи на будь-якому маршруті крім ",[3432,13158,3575],{},[3353,13160,13161,13162,13164,13165,3453],{},"В обох випадках браузер іде в S3 і питає: «дай мені файл ",[3432,13163,13128],{},"». S3 шукає в bucket файл з таким ключем — і не знаходить, бо такого файлу немає. Повертає ",[3356,13166,13167],{},"404",[4801,13169,13171,13178,13185,13192,13196,13199,13206,13214,13218,13221,13228,13235,13246],{"title":13170},"Що відбувається без правильного налаштування",[4805,13172,13174],{"className":13173},[3605],[3603,13175,13177],{"className":13176},[4811],"# Користувач відкриває головну — все добре",[4805,13179,13181,13184],{"className":13180},[3605],[3603,13182,12190],{"className":13183},[4819],"  GET \u002F",[4805,13186,13188,13191],{"className":13187},[3605],[3603,13189,12198],{"className":13190},[4819],"  200 OK → index.html → React завантажується",[4805,13193,13195],{"className":13194},[3605],"               React Router бачить \"\u002F\" → рендерить HomePage ✓",[4805,13197,4841],{"className":13198},[3605],[4805,13200,13202],{"className":13201},[3605],[3603,13203,13205],{"className":13204},[4811],"# Користувач клікає посилання \u002Fusers\u002F123 у меню — все добре",[4805,13207,13209,13213],{"className":13208},[3605],[3603,13210,13212],{"className":13211},[4819],"Browser:","       React Router перехоплює клік, змінює URL без запиту до S3",[4805,13215,13217],{"className":13216},[3605],"               Рендерить UserPage — S3 не питається взагалі ✓",[4805,13219,4841],{"className":13220},[3605],[4805,13222,13224],{"className":13223},[3605],[3603,13225,13227],{"className":13226},[4811],"# Користувач натискає F5 на сторінці \u002Fusers\u002F123 — ПОМИЛКА",[4805,13229,13231,13234],{"className":13230},[3605],[3603,13232,12190],{"className":13233},[4819],"  GET \u002Fusers\u002F123",[4805,13236,13238,7040,13241,13245],{"className":13237},[3605],[3603,13239,12198],{"className":13240},[4819],[3603,13242,13244],{"className":13243},[4864],"404 Not Found"," (файлу \"users\u002F123\" в bucket не існує)",[4805,13247,13249],{"className":13248},[3605],"               Біла сторінка з помилкою. Користувач розгублений ✗",[3353,13251,13252],{},[3356,13253,13254,13255],{},"Рішення: Error Document = ",[3432,13256,13122],{},[3353,13258,13259,13260,13263,13264,13266,13267,13269],{},"В налаштуваннях Static Hosting є поле ",[3356,13261,13262],{},"Error document"," — файл, який S3 повертає замість помилки 404. Якщо вказати ",[3432,13265,13122],{},", то замість «Not Found» S3 завжди повертатиме головний файл застосунку. React завантажиться, React Router прочитає поточний URL (",[3432,13268,13128],{},") і відрендерить потрібну сторінку — так, ніби нічого не сталося.",[4801,13271,13273,13280,13286,13294,13304,13308,13312,13315,13322,13329,13336,13339,13346,13353],{"title":13272},"Те саме після налаштування Error Document = index.html",[4805,13274,13276],{"className":13275},[3605],[3603,13277,13279],{"className":13278},[4811],"# Користувач натискає F5 на сторінці \u002Fusers\u002F123",[4805,13281,13283,13234],{"className":13282},[3605],[3603,13284,12190],{"className":13285},[4819],[4805,13287,13289,13293],{"className":13288},[3605],[3603,13290,13292],{"className":13291},[4819],"S3:","            Файл не знайдено → але Error Document = index.html",[4805,13295,13297,7040,13300],{"className":13296},[3605],[3603,13298,12198],{"className":13299},[4819],[3603,13301,13303],{"className":13302},[4819],"200 OK* → index.html",[4805,13305,13307],{"className":13306},[3605],"               React завантажується, бачить URL \"\u002Fusers\u002F123\"",[4805,13309,13311],{"className":13310},[3605],"               React Router рендерить UserPage ✓",[4805,13313,4841],{"className":13314},[3605],[4805,13316,13318],{"className":13317},[3605],[3603,13319,13321],{"className":13320},[4811],"# Користувач надсилає другу посилання \u002Fdashboard\u002Freports\u002Fapril",[4805,13323,13325,13328],{"className":13324},[3605],[3603,13326,12190],{"className":13327},[4819],"  GET \u002Fdashboard\u002Freports\u002Fapril",[4805,13330,13332,13335],{"className":13331},[3605],[3603,13333,12198],{"className":13334},[4819],"  index.html → React Router → рендерить ReportsPage ✓",[4805,13337,4841],{"className":13338},[3605],[4805,13340,13342],{"className":13341},[3605],[3603,13343,13345],{"className":13344},[4900],"* Технічна деталь: S3 повертає HTTP 404 з тілом index.html.",[4805,13347,13349],{"className":13348},[3605],[3603,13350,13352],{"className":13351},[4900],"  CloudFront може перетворити це на 200 через custom error pages —",[4805,13354,13356],{"className":13355},[3605],[3603,13357,13359],{"className":13358},[4900],"  важливо для SEO та моніторингу помилок.",[3353,13361,13362],{},[6142,13363],{"alt":13364,"className":13365,"src":13366},"Amazon S3 static website delivery through CloudFront diagram",[6146],"\u002Fimages\u002Faws\u002Fs3\u002F05.png",[3561,13368,13370],{"className":5205,"code":13369,"language":5207,"meta":3569,"style":3569},"# Налаштувати Static Website Hosting\naws s3api put-bucket-website \\\n    --bucket my-react-app \\\n    --website-configuration '{\n        \"IndexDocument\": {\"Suffix\": \"index.html\"},\n        \"ErrorDocument\": {\"Key\": \"index.html\"}\n    }' \\\n    --region eu-central-1\n\n# Після цього сайт доступний за:\n# http:\u002F\u002Fmy-react-app.s3-website.eu-central-1.amazonaws.com\n",[3432,13371,13372,13377,13388,13397,13404,13409,13414,13420,13427,13431,13436],{"__ignoreMap":3569},[3603,13373,13374],{"class":3605,"line":3606},[3603,13375,13376],{"class":5214},"# Налаштувати Static Website Hosting\n",[3603,13378,13379,13381,13383,13386],{"class":3605,"line":3612},[3603,13380,5221],{"class":5220},[3603,13382,5365],{"class":5224},[3603,13384,13385],{"class":5224}," put-bucket-website",[3603,13387,5238],{"class":5237},[3603,13389,13390,13392,13395],{"class":3605,"line":3618},[3603,13391,6545],{"class":5243},[3603,13393,13394],{"class":5224}," my-react-app",[3603,13396,5238],{"class":5237},[3603,13398,13399,13402],{"class":3605,"line":3624},[3603,13400,13401],{"class":5243},"    --website-configuration",[3603,13403,5442],{"class":5224},[3603,13405,13406],{"class":3605,"line":3631},[3603,13407,13408],{"class":5224},"        \"IndexDocument\": {\"Suffix\": \"index.html\"},\n",[3603,13410,13411],{"class":3605,"line":3637},[3603,13412,13413],{"class":5224},"        \"ErrorDocument\": {\"Key\": \"index.html\"}\n",[3603,13415,13416,13418],{"class":3605,"line":3643},[3603,13417,11638],{"class":5224},[3603,13419,5238],{"class":5237},[3603,13421,13422,13424],{"class":3605,"line":3649},[3603,13423,6571],{"class":5243},[3603,13425,13426],{"class":5224}," eu-central-1\n",[3603,13428,13429],{"class":3605,"line":3655},[3603,13430,3628],{"emptyLinePlaceholder":3627},[3603,13432,13433],{"class":3605,"line":3661},[3603,13434,13435],{"class":5214},"# Після цього сайт доступний за:\n",[3603,13437,13438],{"class":3605,"line":3667},[3603,13439,13440],{"class":5214},"# http:\u002F\u002Fmy-react-app.s3-website.eu-central-1.amazonaws.com\n",[3353,13442,13443,13446,13447,13450,13451,13454],{},[3356,13444,13445],{},"Важливо:"," S3 Static Hosting URL — це ",[3432,13448,13449],{},"http:\u002F\u002F"," (без S). Для HTTPS використовуйте ",[3356,13452,13453],{},"CloudFront"," як CDN перед S3 — це стандартна production-архітектура.",[3405,13456],{},[3348,13458,13460],{"id":13459},"s3-cors-cross-origin-resource-sharing","S3 CORS — Cross-Origin Resource Sharing",[3353,13462,13463,13466],{},[3356,13464,13465],{},"CORS (Cross-Origin Resource Sharing)"," — це браузерна політика безпеки, яка забороняє JavaScript на одному домені робити запити до іншого домену без явного дозволу. Це не S3-специфічна проблема — це стандарт усіх браузерів.",[3353,13468,13469],{},[3356,13470,13471],{},"Що таке «інший домен» з точки зору CORS:",[3353,13473,13474],{},"Будь-яка відмінність у протоколі, хості або порті — це вже інший origin:",[3902,13476,13477,13490],{},[3905,13478,13479],{},[3908,13480,13481,13484,13487],{},[3911,13482,13483],{},"Ваш сайт",[3911,13485,13486],{},"Запит до",[3911,13488,13489],{},"CORS?",[3924,13491,13492,13507,13520],{},[3908,13493,13494,13499,13504],{},[3929,13495,13496],{},[3432,13497,13498],{},"https:\u002F\u002Fapp.example.com",[3929,13500,13501],{},[3432,13502,13503],{},"https:\u002F\u002Fbucket.s3.amazonaws.com",[3929,13505,13506],{},"✗ Заблоковано",[3908,13508,13509,13514,13518],{},[3929,13510,13511],{},[3432,13512,13513],{},"http:\u002F\u002Flocalhost:3000",[3929,13515,13516],{},[3432,13517,13503],{},[3929,13519,13506],{},[3908,13521,13522,13526,13531],{},[3929,13523,13524],{},[3432,13525,13498],{},[3929,13527,13528],{},[3432,13529,13530],{},"https:\u002F\u002Fapp.example.com\u002Fapi",[3929,13532,13533],{},"✓ Той самий origin",[3353,13535,13536],{},[3356,13537,13538],{},"Коли виникає при роботі з S3:",[3369,13540,13541,13551,13556],{},[3372,13542,13543,13544,6482,13547,13550],{},"Фронтенд робить ",[3432,13545,13546],{},"fetch()",[3432,13548,13549],{},"axios.put()"," на Presigned PUT URL (завантаження файлу напряму в S3)",[3372,13552,13543,13553,13555],{},[3432,13554,13546],{}," на S3 URL щоб прочитати JSON або завантажити файл",[3372,13557,13558],{},"Static website, захостований на S3, звертається до API на іншому домені",[4801,13560,13562,13569,13577,13580,13587,13594,13598,13602,13605,13612,13619,13627,13630,13637,13644,13651,13658,13665,13668],{"title":13561},"Демо: CORS-помилка при завантаженні файлу в S3 (без налаштування)",[4805,13563,13565],{"className":13564},[3605],[3603,13566,13568],{"className":13567},[4811],"# Фронтенд (app.example.com) отримав Presigned PUT URL і робить запит",[4805,13570,13572,13576],{"className":13571},[3605],[3603,13573,13575],{"className":13574},[4819],"JavaScript:","   await fetch(presignedUrl, { method: 'PUT', body: file })",[4805,13578,4841],{"className":13579},[3605],[4805,13581,13583],{"className":13582},[3605],[3603,13584,13586],{"className":13585},[4811],"# Браузер спочатку робить preflight-запит (OPTIONS)",[4805,13588,13590,13593],{"className":13589},[3605],[3603,13591,12190],{"className":13592},[4819]," OPTIONS \u002Fuploads\u002Fphoto.jpg?X-Amz-...",[4805,13595,13597],{"className":13596},[3605],"              Origin: https:\u002F\u002Fapp.example.com",[4805,13599,13601],{"className":13600},[3605],"              Access-Control-Request-Method: PUT",[4805,13603,4841],{"className":13604},[3605],[4805,13606,13608],{"className":13607},[3605],[3603,13609,13611],{"className":13610},[4811],"# S3 відповідає без CORS заголовків (бо не налаштовано)",[4805,13613,13615,13618],{"className":13614},[3605],[3603,13616,12198],{"className":13617},[4819]," 200 OK",[4805,13620,13622,13623],{"className":13621},[3605],"              ",[3603,13624,13626],{"className":13625},[4864],"(немає Access-Control-Allow-Origin)",[4805,13628,4841],{"className":13629},[3605],[4805,13631,13633],{"className":13632},[3605],[3603,13634,13636],{"className":13635},[4811],"# Браузер бачить відсутність дозволу і БЛОКУЄ основний запит",[4805,13638,13640],{"className":13639},[3605],[3603,13641,13643],{"className":13642},[4864],"Console Error: Access to fetch at 'https:\u002F\u002Fmy-bucket.s3.amazonaws.com\u002F...'",[4805,13645,13647],{"className":13646},[3605],[3603,13648,13650],{"className":13649},[4864],"               from origin 'https:\u002F\u002Fapp.example.com' has been blocked by CORS policy:",[4805,13652,13654],{"className":13653},[3605],[3603,13655,13657],{"className":13656},[4864],"               Response to preflight request doesn't pass access control check:",[4805,13659,13661],{"className":13660},[3605],[3603,13662,13664],{"className":13663},[4864],"               No 'Access-Control-Allow-Origin' header is present on the requested resource.",[4805,13666,4841],{"className":13667},[3605],[4805,13669,13671],{"className":13670},[3605],[3603,13672,13674],{"className":13673},[4900],"# PUT запит до S3 взагалі не відбувся — браузер заблокував ще на preflight",[4801,13676,13678,13685,13691,13694,13697,13700,13707,13713,13720,13727,13734,13737,13744,13751,13755,13759,13769,13772,13779],{"title":13677},"Демо: те саме після налаштування CORS на bucket",[4805,13679,13681],{"className":13680},[3605],[3603,13682,13684],{"className":13683},[4811],"# Браузер знову робить preflight (OPTIONS)",[4805,13686,13688,13593],{"className":13687},[3605],[3603,13689,12190],{"className":13690},[4819],[4805,13692,13597],{"className":13693},[3605],[4805,13695,13601],{"className":13696},[3605],[4805,13698,4841],{"className":13699},[3605],[4805,13701,13703],{"className":13702},[3605],[3603,13704,13706],{"className":13705},[4811],"# Тепер S3 відповідає з CORS заголовками",[4805,13708,13710,13618],{"className":13709},[3605],[3603,13711,12198],{"className":13712},[4819],[4805,13714,13622,13716],{"className":13715},[3605],[3603,13717,13719],{"className":13718},[4811],"Access-Control-Allow-Origin: https:\u002F\u002Fapp.example.com",[4805,13721,13622,13723],{"className":13722},[3605],[3603,13724,13726],{"className":13725},[4811],"Access-Control-Allow-Methods: GET, PUT, POST, DELETE, HEAD",[4805,13728,13622,13730],{"className":13729},[3605],[3603,13731,13733],{"className":13732},[4811],"Access-Control-Max-Age: 3000",[4805,13735,4841],{"className":13736},[3605],[4805,13738,13740],{"className":13739},[3605],[3603,13741,13743],{"className":13742},[4811],"# Браузер бачить дозвіл і виконує основний PUT запит",[4805,13745,13747,13750],{"className":13746},[3605],[3603,13748,12190],{"className":13749},[4819]," PUT \u002Fuploads\u002Fphoto.jpg?X-Amz-...",[4805,13752,13754],{"className":13753},[3605],"              Content-Type: image\u002Fjpeg",[4805,13756,13758],{"className":13757},[3605],"              [бінарні дані, 3.1 MB]",[4805,13760,13762,4841,13765],{"className":13761},[3605],[3603,13763,12198],{"className":13764},[4819],[3603,13766,13768],{"className":13767},[4811],"200 OK — файл завантажено ✓",[4805,13770,4841],{"className":13771},[3605],[4805,13773,13775],{"className":13774},[3605],[3603,13776,13778],{"className":13777},[4900],"# MaxAge: 3000 — браузер кешує preflight на 50 хвилин",[4805,13780,13782],{"className":13781},[3605],[3603,13783,13785],{"className":13784},[4900],"# Наступні 50 хвилин PUT-запити йдуть без preflight → швидше",[3353,13787,13788],{},[3356,13789,13790],{},"Коли потрібен CORS на S3:",[3369,13792,13793,13796,13801],{},[3372,13794,13795],{},"Фронтенд завантажує файли напряму в S3 через Presigned PUT URL",[3372,13797,13798,13799],{},"Фронтенд читає файли з S3 напряму (зображення, JSON) через ",[3432,13800,13546],{},[3372,13802,13803],{},"Static website на S3 робить API запити до іншого домену",[3561,13805,13807],{"className":5851,"code":13806,"language":5853,"meta":3569,"style":3569},"[\n    {\n        \"AllowedHeaders\": [\"*\"],\n        \"AllowedMethods\": [\"GET\", \"PUT\", \"POST\", \"DELETE\", \"HEAD\"],\n        \"AllowedOrigins\": [\"https:\u002F\u002Fapp.example.com\", \"http:\u002F\u002Flocalhost:3000\"],\n        \"ExposeHeaders\": [\"ETag\", \"x-amz-version-id\"],\n        \"MaxAgeSeconds\": 3000\n    }\n]\n",[3432,13808,13809,13814,13818,13829,13861,13878,13895,13905,13909],{"__ignoreMap":3569},[3603,13810,13811],{"class":3605,"line":3606},[3603,13812,13813],{"class":5541},"[\n",[3603,13815,13816],{"class":3605,"line":3612},[3603,13817,11086],{"class":5541},[3603,13819,13820,13823,13825,13827],{"class":3605,"line":3618},[3603,13821,13822],{"class":5864},"        \"AllowedHeaders\"",[3603,13824,7046],{"class":5541},[3603,13826,9042],{"class":5224},[3603,13828,9174],{"class":5541},[3603,13830,13831,13834,13836,13839,13841,13844,13846,13849,13851,13854,13856,13859],{"class":3605,"line":3624},[3603,13832,13833],{"class":5864},"        \"AllowedMethods\"",[3603,13835,7046],{"class":5541},[3603,13837,13838],{"class":5224},"\"GET\"",[3603,13840,3470],{"class":5541},[3603,13842,13843],{"class":5224},"\"PUT\"",[3603,13845,3470],{"class":5541},[3603,13847,13848],{"class":5224},"\"POST\"",[3603,13850,3470],{"class":5541},[3603,13852,13853],{"class":5224},"\"DELETE\"",[3603,13855,3470],{"class":5541},[3603,13857,13858],{"class":5224},"\"HEAD\"",[3603,13860,9174],{"class":5541},[3603,13862,13863,13866,13868,13871,13873,13876],{"class":3605,"line":3631},[3603,13864,13865],{"class":5864},"        \"AllowedOrigins\"",[3603,13867,7046],{"class":5541},[3603,13869,13870],{"class":5224},"\"https:\u002F\u002Fapp.example.com\"",[3603,13872,3470],{"class":5541},[3603,13874,13875],{"class":5224},"\"http:\u002F\u002Flocalhost:3000\"",[3603,13877,9174],{"class":5541},[3603,13879,13880,13883,13885,13888,13890,13893],{"class":3605,"line":3637},[3603,13881,13882],{"class":5864},"        \"ExposeHeaders\"",[3603,13884,7046],{"class":5541},[3603,13886,13887],{"class":5224},"\"ETag\"",[3603,13889,3470],{"class":5541},[3603,13891,13892],{"class":5224},"\"x-amz-version-id\"",[3603,13894,9174],{"class":5541},[3603,13896,13897,13900,13902],{"class":3605,"line":3643},[3603,13898,13899],{"class":5864},"        \"MaxAgeSeconds\"",[3603,13901,5881],{"class":5541},[3603,13903,13904],{"class":5943},"3000\n",[3603,13906,13907],{"class":3605,"line":3649},[3603,13908,3664],{"class":5541},[3603,13910,13911],{"class":3605,"line":3655},[3603,13912,13913],{"class":5541},"]\n",[3561,13915,13917],{"className":5205,"code":13916,"language":5207,"meta":3569,"style":3569},"# Застосувати CORS конфігурацію\naws s3api put-bucket-cors \\\n    --bucket my-app-bucket \\\n    --cors-configuration file:\u002F\u002Fcors.json \\\n    --region eu-central-1\n",[3432,13918,13919,13924,13935,13944,13954],{"__ignoreMap":3569},[3603,13920,13921],{"class":3605,"line":3606},[3603,13922,13923],{"class":5214},"# Застосувати CORS конфігурацію\n",[3603,13925,13926,13928,13930,13933],{"class":3605,"line":3612},[3603,13927,5221],{"class":5220},[3603,13929,5365],{"class":5224},[3603,13931,13932],{"class":5224}," put-bucket-cors",[3603,13934,5238],{"class":5237},[3603,13936,13937,13939,13942],{"class":3605,"line":3618},[3603,13938,6545],{"class":5243},[3603,13940,13941],{"class":5224}," my-app-bucket",[3603,13943,5238],{"class":5237},[3603,13945,13946,13949,13952],{"class":3605,"line":3624},[3603,13947,13948],{"class":5243},"    --cors-configuration",[3603,13950,13951],{"class":5224}," file:\u002F\u002Fcors.json",[3603,13953,5238],{"class":5237},[3603,13955,13956,13958],{"class":3605,"line":3631},[3603,13957,6571],{"class":5243},[3603,13959,13426],{"class":5224},[3353,13961,13962],{},[3356,13963,13964],{},"Пояснення полів:",[3369,13966,13967,13983,13995,14004,14013],{},[3372,13968,13969,13972,13973,13975,13976],{},[3432,13970,13971],{},"AllowedOrigins",": які домени можуть звертатись до bucket. Для розробки — ",[3432,13974,13513],{},". Для production — ваш реальний домен. ",[3356,13977,13978,13979,13982],{},"Ніколи не використовуйте ",[3432,13980,13981],{},"*"," для bucket з приватними даними.",[3372,13984,13985,13988,13989,13991,13992,13994],{},[3432,13986,13987],{},"AllowedMethods",": які HTTP методи дозволені. ",[3432,13990,6134],{}," — для завантаження, ",[3432,13993,6107],{}," — для читання.",[3372,13996,13997,14000,14001,14003],{},[3432,13998,13999],{},"AllowedHeaders",": які заголовки браузер може надіслати. ",[3432,14002,13981],{}," — всі.",[3372,14005,14006,14009,14010,14012],{},[3432,14007,14008],{},"ExposeHeaders",": які заголовки відповіді браузер може читати з JavaScript. ",[3432,14011,3531],{}," — для верифікації завантаженого файлу.",[3372,14014,14015,14018],{},[3432,14016,14017],{},"MaxAgeSeconds",": як довго браузер кешує CORS preflight відповідь (3000 сек = 50 хв).",[3405,14020],{},[3348,14022,14024],{"id":14023},"s3-transfer-acceleration","S3 Transfer Acceleration",[3353,14026,14027,14029,14030,14033],{},[3356,14028,14024],{}," — це функція, яка прискорює завантаження та скачування великих файлів завдяки використанню ",[3356,14031,14032],{},"глобальної мережі edge locations AWS (CloudFront)",". Замість прямого підключення до S3 у регіоні, дані проходять через найближчий CloudFront edge-вузол, а далі через оптимізовану внутрішню мережу AWS.",[3353,14035,14036],{},[3356,14037,14038],{},"Чому звичайне з'єднання повільне на великих відстанях",[3353,14040,14041],{},"Коли ваш S3 bucket у Франкфурті, а користувач в Токіо — пакети даних мандрують через публічний інтернет: десятки роутерів, підводні кабелі, різні провайдери. Кожен стрибок додає затримку. На відстані 9 000+ км RTT (round-trip time) може досягати 250–350 мс, а TCP slow start і втрати пакетів ще більше знижують реальну швидкість.",[3353,14043,14044],{},[3356,14045,14046],{},"Як Transfer Acceleration вирішує це",[3353,14048,14049],{},"Замість того щоб дані йшли весь шлях через публічний інтернет, вони:",[6382,14051,14052,14055],{},[3372,14053,14054],{},"Потрапляють у найближчий edge location AWS (їх 400+ по всьому світу)",[3372,14056,14057,14058,14061],{},"Далі йдуть до S3 по ",[3356,14059,14060],{},"приватній оптимізованій магістралі AWS"," — не через публічний інтернет",[4801,14063,14065,14072,14079,14086,14093,14097,14100,14107,14114,14118,14124,14128,14131,14138],{"title":14064},"Демо: порівняння маршрутів — Токіо → S3 у Франкфурті",[4805,14066,14068],{"className":14067},[3605],[3603,14069,14071],{"className":14070},[4811],"# Без Transfer Acceleration — публічний інтернет",[4805,14073,14075],{"className":14074},[3605],[3603,14076,14078],{"className":14077},[4819],"Токіо → [ISP] → [підводний кабель] → [Сінгапур] → [Індія]",[4805,14080,14082],{"className":14081},[3605],[3603,14083,14085],{"className":14084},[4819],"      → [Близький Схід] → [Єгипет] → [Середземне море]",[4805,14087,14089],{"className":14088},[3605],[3603,14090,14092],{"className":14091},[4819],"      → [Франкфурт AWS S3]",[4805,14094,14096],{"className":14095},[3605],"RTT: ~270 мс | Швидкість завантаження 500 MB: ~4–6 хв",[4805,14098,4841],{"className":14099},[3605],[4805,14101,14103],{"className":14102},[3605],[3603,14104,14106],{"className":14105},[4811],"# З Transfer Acceleration — через найближчий edge",[4805,14108,14110],{"className":14109},[3605],[3603,14111,14113],{"className":14112},[4819],"Токіо → [AWS Edge: Tokyo ap-northeast-1]",[4805,14115,14117],{"className":14116},[3605],"             ↓ (приватна мережа AWS, оптимізована)",[4805,14119,14121],{"className":14120},[3605],[3603,14122,14092],{"className":14123},[4819],[4805,14125,14127],{"className":14126},[3605],"RTT до edge: ~5 мс | Швидкість завантаження 500 MB: ~1–2 хв",[4805,14129,4841],{"className":14130},[3605],[4805,14132,14134],{"className":14133},[3605],[3603,14135,14137],{"className":14136},[4900],"Прискорення: 2–5x на великих відстанях",[4805,14139,14141],{"className":14140},[3605],[3603,14142,14144],{"className":14143},[4900],"Доплата: $0.04\u002FGB (Americas\u002FEurope) або $0.08\u002FGB (всі інші регіони)",[3353,14146,14147],{},[3356,14148,14149],{},"Коли вигідно \u002F невигідно:",[3902,14151,14152,14161],{},[3905,14153,14154],{},[3908,14155,14156,14159],{},[3911,14157,14158],{},"Сценарій",[3911,14160,10232],{},[3924,14162,14163,14171,14178,14185,14197,14205],{},[3908,14164,14165,14168],{},[3929,14166,14167],{},"Користувачі по всьому світу завантажують файли 50+ MB",[3929,14169,14170],{},"✓ Вмикайте",[3908,14172,14173,14176],{},[3929,14174,14175],{},"Відеоплатформа приймає завантаження від creators у різних країнах",[3929,14177,14170],{},[3908,14179,14180,14183],{},[3929,14181,14182],{},"SaaS: синхронізація великих файлів між офісами на різних континентах",[3929,14184,14170],{},[3908,14186,14187,14194],{},[3929,14188,14189,14190,14193],{},"Bucket у ",[3432,14191,14192],{},"eu-central-1",", всі користувачі в Україні\u002FПольщі\u002FНімеччині",[3929,14195,14196],{},"✗ Не потрібне",[3908,14198,14199,14202],{},[3929,14200,14201],{},"Файли \u003C 1 MB (іконки, JSON, невеликі документи)",[3929,14203,14204],{},"✗ Не допоможе",[3908,14206,14207,14210],{},[3929,14208,14209],{},"Бекенд-сервер в тому ж регіоні що і S3",[3929,14211,14196],{},[3397,14213,14214,14215,14222],{},"AWS надає ",[14216,14217,14221],"a",{"href":14218,"rel":14219},"https:\u002F\u002Fs3-accelerate-speedtest.s3-accelerate.amazonaws.com\u002Fen\u002Faccelerate-speed-comparsion.html",[14220],"nofollow","безкоштовний інструмент порівняння швидкості"," — можна протестувати реальний приріст для вашого регіону і bucket перед увімкненням.",[4801,14224,14226,14233,14240,14243,14247,14250,14253,14260,14268,14276,14279,14286,14290,14293,14300,14307,14311,14318,14321,14328,14334,14337],{"title":14225},"Увімкнення та використання Transfer Acceleration",[4805,14227,14229],{"className":14228},[3605],[3603,14230,14232],{"className":14231},[4811],"# 1. Увімкнути Transfer Acceleration на bucket",[4805,14234,14236,14239],{"className":14235},[3605],[3603,14237,7030],{"className":14238},[7029]," aws s3api put-bucket-accelerate-configuration \\",[4805,14241,10703],{"className":14242},[3605],[4805,14244,14246],{"className":14245},[3605],"    --accelerate-configuration Status=Enabled \\",[4805,14248,10551],{"className":14249},[3605],[4805,14251,4841],{"className":14252},[3605],[4805,14254,14256],{"className":14255},[3605],[3603,14257,14259],{"className":14258},[4811],"# 2. Завантажити через accelerated endpoint (URL відрізняється!)",[4805,14261,14263,14267],{"className":14262},[3605],[3603,14264,14266],{"className":14265},[4819],"Стандартний endpoint:"," my-app-bucket.s3.eu-central-1.amazonaws.com",[4805,14269,14271,14275],{"className":14270},[3605],[3603,14272,14274],{"className":14273},[4819],"Accelerated endpoint:"," my-app-bucket.s3-accelerate.amazonaws.com",[4805,14277,4841],{"className":14278},[3605],[4805,14280,14282,14285],{"className":14281},[3605],[3603,14283,7030],{"className":14284},[7029]," aws s3 cp large-video.mp4 s3:\u002F\u002Fmy-app-bucket\u002F \\",[4805,14287,14289],{"className":14288},[3605],"    --endpoint-url https:\u002F\u002Fs3-accelerate.amazonaws.com",[4805,14291,4841],{"className":14292},[3605],[4805,14294,14296],{"className":14295},[3605],[3603,14297,14299],{"className":14298},[4811],"# 3. Перевірити статус",[4805,14301,14303,14306],{"className":14302},[3605],[3603,14304,7030],{"className":14305},[7029]," aws s3api get-bucket-accelerate-configuration \\",[4805,14308,14310],{"className":14309},[3605],"    --bucket my-app-bucket --region eu-central-1",[4805,14312,14314],{"className":14313},[3605],[3603,14315,14317],{"className":14316},[4819],"# {\"Status\": \"Enabled\"}",[4805,14319,4841],{"className":14320},[3605],[4805,14322,14324],{"className":14323},[3605],[3603,14325,14327],{"className":14326},[4811],"# 4. Вимкнути якщо не потрібне",[4805,14329,14331,14239],{"className":14330},[3605],[3603,14332,7030],{"className":14333},[7029],[4805,14335,10703],{"className":14336},[3605],[4805,14338,14340],{"className":14339},[3605],"    --accelerate-configuration Status=Suspended",[3353,14342,14343],{},"У .NET SDK:",[3561,14345,14347],{"className":5519,"code":14346,"language":5521,"meta":3569,"style":3569},"var config = new AmazonS3Config\n{\n    UseAccelerateEndpoint = true, \u002F\u002F Увімкнути Transfer Acceleration\n    RegionEndpoint = RegionEndpoint.EUCentral1\n};\nvar s3 = new AmazonS3Client(config);\n",[3432,14348,14349,14363,14367,14381,14396,14400],{"__ignoreMap":3569},[3603,14350,14351,14353,14356,14358,14360],{"class":3605,"line":3606},[3603,14352,5573],{"class":5243},[3603,14354,14355],{"class":5576}," config",[3603,14357,5580],{"class":5541},[3603,14359,5583],{"class":5243},[3603,14361,14362],{"class":5537}," AmazonS3Config\n",[3603,14364,14365],{"class":3605,"line":3612},[3603,14366,5591],{"class":5541},[3603,14368,14369,14372,14374,14376,14378],{"class":3605,"line":3618},[3603,14370,14371],{"class":5576},"    UseAccelerateEndpoint",[3603,14373,5580],{"class":5541},[3603,14375,6905],{"class":5243},[3603,14377,3470],{"class":5541},[3603,14379,14380],{"class":5214},"\u002F\u002F Увімкнути Transfer Acceleration\n",[3603,14382,14383,14386,14388,14391,14393],{"class":3605,"line":3624},[3603,14384,14385],{"class":5576},"    RegionEndpoint",[3603,14387,5580],{"class":5541},[3603,14389,14390],{"class":5576},"RegionEndpoint",[3603,14392,3453],{"class":5541},[3603,14394,14395],{"class":5576},"EUCentral1\n",[3603,14397,14398],{"class":3605,"line":3631},[3603,14399,5687],{"class":5541},[3603,14401,14402,14404,14406,14408,14410,14413,14415,14418],{"class":3605,"line":3637},[3603,14403,5573],{"class":5243},[3603,14405,5225],{"class":5576},[3603,14407,5580],{"class":5541},[3603,14409,5583],{"class":5243},[3603,14411,14412],{"class":5537}," AmazonS3Client",[3603,14414,5707],{"class":5541},[3603,14416,14417],{"class":5576},"config",[3603,14419,5713],{"class":5541},[3405,14421],{},[3348,14423,14425],{"id":14424},"юзкейс-медіа-файли-та-hlsdash-відео-стрімінг","Юзкейс: Медіа файли та HLS\u002FDASH відео-стрімінг",[3353,14427,14428],{},"Це один із найважливіших та найпоширеніших use cases для S3 у сучасних застосунках. Розберемо детально.",[3412,14430,14432],{"id":14431},"що-таке-hls-та-dash","Що таке HLS та DASH",[3353,14434,14435,14438,14439,6482,14442,14445,14446,10425,14449,14452],{},[3356,14436,14437],{},"HLS (HTTP Live Streaming)"," — протокол відео-стрімінгу розроблений Apple. Відео нарізається на короткі сегменти (зазвичай 6–10 секунд) у форматі ",[3432,14440,14441],{},".ts",[3432,14443,14444],{},".fmp4",". Плеєр завантажує ",[3356,14447,14448],{},"manifest-файл",[3432,14450,14451],{},".m3u8","), який описує всі сегменти та доступні якості, потім послідовно завантажує сегменти. Підтримується нативно у Safari та iOS, в інших браузерах — через бібліотеки (hls.js).",[3353,14454,14455,14458,14459,14462,14463,14466],{},[3356,14456,14457],{},"DASH (Dynamic Adaptive Streaming over HTTP)"," — відкритий стандарт (MPEG-DASH), аналогічний HLS. Manifest-файл (",[3432,14460,14461],{},".mpd","), сегменти (",[3432,14464,14465],{},".m4s","). Підтримується у більшості сучасних браузерів через dash.js.",[3353,14468,14469,14472],{},[3356,14470,14471],{},"Adaptive Bitrate (ABR):"," і HLS, і DASH підтримують кілька якостей одного відео (240p, 480p, 720p, 1080p). Плеєр автоматично перемикається між якостями залежно від швидкості з'єднання користувача — якість не заморожується, а плавно деградує.",[3412,14474,14476],{"id":14475},"архітектура-відео-стрімінгу-на-s3-cloudfront","Архітектура відео-стрімінгу на S3 + CloudFront",[3561,14478,14481],{"className":14479,"code":14480,"language":3566},[3564],"Відеофайл → AWS Elemental MediaConvert → S3 (HLS\u002FDASH сегменти) → CloudFront → Користувач\n",[3432,14482,14480],{"__ignoreMap":3569},[3353,14484,14485],{},[6142,14486],{"alt":14487,"className":14488,"src":14489},"AWS CloudFront S3 video streaming architecture diagram",[6146],"\u002Fimages\u002Faws\u002Fs3\u002F06.png",[3593,14491,14492],{},[3561,14493,14495],{"className":3597,"code":14494,"language":3599,"meta":3569,"style":3569},"@startuml\nskinparam style plain\nskinparam backgroundColor #ffffff\n\nactor \"Контент-менеджер\" as CM\nactor \"Глядач (UA)\" as UA\nactor \"Глядач (US)\" as US\n\nqueue \"S3: raw-videos\" as S3raw #fef3c7\n\nrectangle \"AWS Elemental MediaConvert\" as MC #dbeafe {\n    note right\n      Транскодує відео у:\n      HLS (240p\u002F480p\u002F720p\u002F1080p)\n      DASH (240p\u002F480p\u002F720p\u002F1080p)\n    end note\n}\n\npackage \"S3: video-assets\" as S3out #d1fae5 {\n    file \"master.m3u8 (HLS manifest)\" as HLS\n    file \"manifest.mpd (DASH manifest)\" as DASH\n    file \"segment_001.ts ... (сегменти)\" as SEG\n}\n\nrectangle \"Amazon CloudFront (CDN)\" as CF #bbf7d0 {\n    rectangle \"Edge: Frankfurt\" as EF\n    rectangle \"Edge: New York\" as EN\n}\n\nCM -right-> S3raw : upload .mp4\nS3raw -right-> MC : EventBridge trigger\nMC -right-> S3out : конвертовані файли\nS3out -down-> CF : origin\nCF -left-> UA : edge Frankfurt (~10ms)\nCF -right-> US : edge New York (~10ms)\n\n@enduml\n",[3432,14496,14497,14501,14505,14509,14513,14518,14523,14528,14532,14537,14541,14546,14551,14556,14561,14566,14571,14575,14579,14584,14589,14594,14599,14603,14607,14612,14617,14622,14626,14630,14635,14640,14645,14650,14655,14660,14664],{"__ignoreMap":3569},[3603,14498,14499],{"class":3605,"line":3606},[3603,14500,3609],{},[3603,14502,14503],{"class":3605,"line":3612},[3603,14504,3615],{},[3603,14506,14507],{"class":3605,"line":3618},[3603,14508,3621],{},[3603,14510,14511],{"class":3605,"line":3624},[3603,14512,3628],{"emptyLinePlaceholder":3627},[3603,14514,14515],{"class":3605,"line":3631},[3603,14516,14517],{},"actor \"Контент-менеджер\" as CM\n",[3603,14519,14520],{"class":3605,"line":3637},[3603,14521,14522],{},"actor \"Глядач (UA)\" as UA\n",[3603,14524,14525],{"class":3605,"line":3643},[3603,14526,14527],{},"actor \"Глядач (US)\" as US\n",[3603,14529,14530],{"class":3605,"line":3649},[3603,14531,3628],{"emptyLinePlaceholder":3627},[3603,14533,14534],{"class":3605,"line":3655},[3603,14535,14536],{},"queue \"S3: raw-videos\" as S3raw #fef3c7\n",[3603,14538,14539],{"class":3605,"line":3661},[3603,14540,3628],{"emptyLinePlaceholder":3627},[3603,14542,14543],{"class":3605,"line":3667},[3603,14544,14545],{},"rectangle \"AWS Elemental MediaConvert\" as MC #dbeafe {\n",[3603,14547,14548],{"class":3605,"line":3673},[3603,14549,14550],{},"    note right\n",[3603,14552,14553],{"class":3605,"line":3678},[3603,14554,14555],{},"      Транскодує відео у:\n",[3603,14557,14558],{"class":3605,"line":3684},[3603,14559,14560],{},"      HLS (240p\u002F480p\u002F720p\u002F1080p)\n",[3603,14562,14563],{"class":3605,"line":3690},[3603,14564,14565],{},"      DASH (240p\u002F480p\u002F720p\u002F1080p)\n",[3603,14567,14568],{"class":3605,"line":3696},[3603,14569,14570],{},"    end note\n",[3603,14572,14573],{"class":3605,"line":3702},[3603,14574,3670],{},[3603,14576,14577],{"class":3605,"line":3708},[3603,14578,3628],{"emptyLinePlaceholder":3627},[3603,14580,14581],{"class":3605,"line":4667},[3603,14582,14583],{},"package \"S3: video-assets\" as S3out #d1fae5 {\n",[3603,14585,14586],{"class":3605,"line":4673},[3603,14587,14588],{},"    file \"master.m3u8 (HLS manifest)\" as HLS\n",[3603,14590,14591],{"class":3605,"line":4679},[3603,14592,14593],{},"    file \"manifest.mpd (DASH manifest)\" as DASH\n",[3603,14595,14596],{"class":3605,"line":4685},[3603,14597,14598],{},"    file \"segment_001.ts ... (сегменти)\" as SEG\n",[3603,14600,14601],{"class":3605,"line":4691},[3603,14602,3670],{},[3603,14604,14605],{"class":3605,"line":4696},[3603,14606,3628],{"emptyLinePlaceholder":3627},[3603,14608,14609],{"class":3605,"line":4701},[3603,14610,14611],{},"rectangle \"Amazon CloudFront (CDN)\" as CF #bbf7d0 {\n",[3603,14613,14614],{"class":3605,"line":4707},[3603,14615,14616],{},"    rectangle \"Edge: Frankfurt\" as EF\n",[3603,14618,14619],{"class":3605,"line":4713},[3603,14620,14621],{},"    rectangle \"Edge: New York\" as EN\n",[3603,14623,14624],{"class":3605,"line":4719},[3603,14625,3670],{},[3603,14627,14628],{"class":3605,"line":4724},[3603,14629,3628],{"emptyLinePlaceholder":3627},[3603,14631,14632],{"class":3605,"line":4730},[3603,14633,14634],{},"CM -right-> S3raw : upload .mp4\n",[3603,14636,14637],{"class":3605,"line":4736},[3603,14638,14639],{},"S3raw -right-> MC : EventBridge trigger\n",[3603,14641,14642],{"class":3605,"line":4742},[3603,14643,14644],{},"MC -right-> S3out : конвертовані файли\n",[3603,14646,14647],{"class":3605,"line":4748},[3603,14648,14649],{},"S3out -down-> CF : origin\n",[3603,14651,14652],{"class":3605,"line":4753},[3603,14653,14654],{},"CF -left-> UA : edge Frankfurt (~10ms)\n",[3603,14656,14657],{"class":3605,"line":4758},[3603,14658,14659],{},"CF -right-> US : edge New York (~10ms)\n",[3603,14661,14662],{"class":3605,"line":6363},[3603,14663,3628],{"emptyLinePlaceholder":3627},[3603,14665,14666],{"class":3605,"line":6368},[3603,14667,3711],{},[3412,14669,14671],{"id":14670},"структура-файлів-hls-у-s3","Структура файлів HLS у S3",[3353,14673,14674],{},"Після конвертації через MediaConvert у S3 bucket утворюється наступна структура:",[3561,14676,14679],{"className":14677,"code":14678,"language":3566},[3564],"s3:\u002F\u002Fvideo-assets\u002F\n└── videos\u002F\n    └── movie-id-abc123\u002F\n        ├── hls\u002F\n        │   ├── master.m3u8              ← головний manifest (посилання на якості)\n        │   ├── 1080p\u002F\n        │   │   ├── stream.m3u8          ← playlist для 1080p\n        │   │   ├── segment_000.ts       ← сегмент 0 (0-6 сек)\n        │   │   ├── segment_001.ts       ← сегмент 1 (6-12 сек)\n        │   │   └── ...\n        │   ├── 720p\u002F\n        │   │   ├── stream.m3u8\n        │   │   └── ...\n        │   └── 360p\u002F\n        │       ├── stream.m3u8\n        │       └── ...\n        └── dash\u002F\n            ├── manifest.mpd\n            └── ...\n",[3432,14680,14678],{"__ignoreMap":3569},[3353,14682,14683,14684,5629],{},"Вміст ",[3432,14685,14686],{},"master.m3u8",[3561,14688,14692],{"className":14689,"code":14690,"language":14691,"meta":3569,"style":3569},"language-m3u8 shiki shiki-themes light-plus dark-plus dark-plus","#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080\n1080p\u002Fstream.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=2800000,RESOLUTION=1280x720\n720p\u002Fstream.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360\n360p\u002Fstream.m3u8\n","m3u8",[3432,14693,14694,14699,14704,14709,14714,14719,14724],{"__ignoreMap":3569},[3603,14695,14696],{"class":3605,"line":3606},[3603,14697,14698],{},"#EXTM3U\n",[3603,14700,14701],{"class":3605,"line":3612},[3603,14702,14703],{},"#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080\n",[3603,14705,14706],{"class":3605,"line":3618},[3603,14707,14708],{},"1080p\u002Fstream.m3u8\n",[3603,14710,14711],{"class":3605,"line":3624},[3603,14712,14713],{},"#EXT-X-STREAM-INF:BANDWIDTH=2800000,RESOLUTION=1280x720\n",[3603,14715,14716],{"class":3605,"line":3631},[3603,14717,14718],{},"720p\u002Fstream.m3u8\n",[3603,14720,14721],{"class":3605,"line":3637},[3603,14722,14723],{},"#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360\n",[3603,14725,14726],{"class":3605,"line":3643},[3603,14727,14728],{},"360p\u002Fstream.m3u8\n",[3353,14730,14731,14732,14734,14735,14738,14739,14741],{},"Плеєр завантажує ",[3432,14733,14686],{},", бачить три якості, обирає відповідну до швидкості з'єднання і завантажує відповідний ",[3432,14736,14737],{},"stream.m3u8",", а потім послідовно всі ",[3432,14740,14441],{}," сегменти.",[3412,14743,14745],{"id":14744},"cloudfront-для-відео-стрімінгу","CloudFront для відео-стрімінгу",[3353,14747,14748,14749,14752],{},"Відео через S3 напряму — ",[3356,14750,14751],{},"не рекомендовано"," для production з кількох причин:",[3369,14754,14755,14758,14761],{},[3372,14756,14757],{},"Висока latency для глядачів далеко від регіону S3",[3372,14759,14760],{},"Дорогий трафік ($0.09\u002FGB з S3, порівняно з $0.015\u002FGB з CloudFront)",[3372,14762,14763],{},"Немає кешування сегментів — кожен запит іде до S3",[3353,14765,14766,14768],{},[3356,14767,13453],{}," (CDN від AWS) кешує сегменти у 400+ edge locations по всьому світу. Перший глядач у Токіо завантажить сегмент з S3 у Франкфурті — але CloudFront закешує його в Токіо. Всі наступні глядачі в Токіо отримають цей сегмент за мілісекунди.",[3412,14770,14772],{"id":14771},"захист-медіа-через-signed-urls-cookies","Захист медіа через Signed URLs \u002F Cookies",[3353,14774,14775],{},"Якщо відео-контент платний або приватний — потрібно захистити доступ. CloudFront підтримує два механізми:",[3353,14777,14778,14781],{},[3356,14779,14780],{},"CloudFront Signed URLs:"," кожен URL підписується і містить термін дії, дозволений IP, дозволені шляхи. Аналог S3 Presigned URL.",[3353,14783,14784,14787],{},[3356,14785,14786],{},"CloudFront Signed Cookies:"," встановлюється cookie з правами доступу до цілої «директорії» в S3 (наприклад, всі сегменти конкретного відео). Зручніше для відео — не потрібно підписувати кожен сегмент окремо.",[3561,14789,14791],{"className":5519,"code":14790,"language":5521,"meta":3569,"style":3569},"\u002F\u002F .NET: генерація CloudFront Signed Cookie для відео\nusing Amazon.CloudFront;\n\npublic string GenerateSignedCookiePolicy(\n    string resourceUrl,      \u002F\u002F \"https:\u002F\u002Fcdn.example.com\u002Fvideos\u002Fmovie-id\u002F*\"\n    string cloudFrontKeyId,  \u002F\u002F ID ключа CloudFront\n    RSA privateKey,          \u002F\u002F RSA приватний ключ\n    DateTime expiresAt)\n{\n    var policy = new CloudFrontCannedPolicy\n    {\n        Resource = resourceUrl,\n        DateLessThan = expiresAt\n    };\n    return AmazonCloudFrontUrlSigner.BuildPolicyForSignedUrl(\n        cloudFrontKeyId, privateKey, policy);\n}\n",[3432,14792,14793,14798,14810,14814,14826,14840,14853,14867,14877,14881,14896,14900,14912,14922,14927,14942,14959],{"__ignoreMap":3569},[3603,14794,14795],{"class":3605,"line":3606},[3603,14796,14797],{"class":5214},"\u002F\u002F .NET: генерація CloudFront Signed Cookie для відео\n",[3603,14799,14800,14802,14804,14806,14808],{"class":3605,"line":3612},[3603,14801,5534],{"class":5533},[3603,14803,5538],{"class":5537},[3603,14805,3453],{"class":5541},[3603,14807,13453],{"class":5537},[3603,14809,5547],{"class":5541},[3603,14811,14812],{"class":3605,"line":3618},[3603,14813,3628],{"emptyLinePlaceholder":3627},[3603,14815,14816,14818,14820,14823],{"class":3605,"line":3624},[3603,14817,10953],{"class":5243},[3603,14819,10994],{"class":5243},[3603,14821,14822],{"class":5220}," GenerateSignedCookiePolicy",[3603,14824,14825],{"class":5541},"(\n",[3603,14827,14828,14831,14834,14837],{"class":3605,"line":3631},[3603,14829,14830],{"class":5243},"    string",[3603,14832,14833],{"class":5576}," resourceUrl",[3603,14835,14836],{"class":5541},",      ",[3603,14838,14839],{"class":5214},"\u002F\u002F \"https:\u002F\u002Fcdn.example.com\u002Fvideos\u002Fmovie-id\u002F*\"\n",[3603,14841,14842,14844,14847,14850],{"class":3605,"line":3637},[3603,14843,14830],{"class":5243},[3603,14845,14846],{"class":5576}," cloudFrontKeyId",[3603,14848,14849],{"class":5541},",  ",[3603,14851,14852],{"class":5214},"\u002F\u002F ID ключа CloudFront\n",[3603,14854,14855,14858,14861,14864],{"class":3605,"line":3643},[3603,14856,14857],{"class":5537},"    RSA",[3603,14859,14860],{"class":5576}," privateKey",[3603,14862,14863],{"class":5541},",          ",[3603,14865,14866],{"class":5214},"\u002F\u002F RSA приватний ключ\n",[3603,14868,14869,14872,14875],{"class":3605,"line":3649},[3603,14870,14871],{"class":5537},"    DateTime",[3603,14873,14874],{"class":5576}," expiresAt",[3603,14876,6924],{"class":5541},[3603,14878,14879],{"class":3605,"line":3655},[3603,14880,5591],{"class":5541},[3603,14882,14883,14886,14889,14891,14893],{"class":3605,"line":3661},[3603,14884,14885],{"class":5243},"    var",[3603,14887,14888],{"class":5576}," policy",[3603,14890,5580],{"class":5541},[3603,14892,5583],{"class":5243},[3603,14894,14895],{"class":5537}," CloudFrontCannedPolicy\n",[3603,14897,14898],{"class":3605,"line":3667},[3603,14899,11086],{"class":5541},[3603,14901,14902,14905,14907,14910],{"class":3605,"line":3673},[3603,14903,14904],{"class":5576},"        Resource",[3603,14906,5580],{"class":5541},[3603,14908,14909],{"class":5576},"resourceUrl",[3603,14911,5604],{"class":5541},[3603,14913,14914,14917,14919],{"class":3605,"line":3678},[3603,14915,14916],{"class":5576},"        DateLessThan",[3603,14918,5580],{"class":5541},[3603,14920,14921],{"class":5576},"expiresAt\n",[3603,14923,14924],{"class":3605,"line":3684},[3603,14925,14926],{"class":5541},"    };\n",[3603,14928,14929,14932,14935,14937,14940],{"class":3605,"line":3690},[3603,14930,14931],{"class":5533},"    return",[3603,14933,14934],{"class":5576}," AmazonCloudFrontUrlSigner",[3603,14936,3453],{"class":5541},[3603,14938,14939],{"class":5220},"BuildPolicyForSignedUrl",[3603,14941,14825],{"class":5541},[3603,14943,14944,14947,14949,14952,14954,14957],{"class":3605,"line":3696},[3603,14945,14946],{"class":5576},"        cloudFrontKeyId",[3603,14948,3470],{"class":5541},[3603,14950,14951],{"class":5576},"privateKey",[3603,14953,3470],{"class":5541},[3603,14955,14956],{"class":5576},"policy",[3603,14958,5713],{"class":5541},[3603,14960,14961],{"class":3605,"line":3702},[3603,14962,3670],{"class":5541},[3412,14964,14966],{"id":14965},"реалізація-відео-плеєра-у-react-з-hlsjs","Реалізація відео-плеєра у React з hls.js",[3561,14968,14972],{"className":14969,"code":14970,"language":14971,"meta":3569,"style":3569},"language-jsx shiki shiki-themes light-plus dark-plus dark-plus","import { useEffect, useRef } from 'react'\nimport Hls from 'hls.js'\n\nfunction VideoPlayer({ manifestUrl }) {\n    const videoRef = useRef(null)\n\n    useEffect(() => {\n        const video = videoRef.current\n        if (!video) return\n\n        if (Hls.isSupported()) {\n            \u002F\u002F Для більшості браузерів — використовуємо hls.js\n            const hls = new Hls({\n                \u002F\u002F Починати з нижчої якості для швидкого старту\n                startLevel: -1, \u002F\u002F -1 = auto\n                \u002F\u002F Кешувати в пам'яті не більше 30 сек контенту\n                maxBufferLength: 30,\n            })\n            hls.loadSource(manifestUrl) \u002F\u002F URL до master.m3u8\n            hls.attachMedia(video)\n            hls.on(Hls.Events.MANIFEST_PARSED, () => video.play())\n\n            return () => hls.destroy()\n        } else if (video.canPlayType('application\u002Fvnd.apple.mpegurl')) {\n            \u002F\u002F Safari — нативна підтримка HLS\n            video.src = manifestUrl\n            video.play()\n        }\n    }, [manifestUrl])\n\n    return \u003Cvideo ref={videoRef} controls style={{ width: '100%', maxHeight: '600px' }} \u002F>\n}\n\n\u002F\u002F Використання:\n\u002F\u002F \u003CVideoPlayer manifestUrl=\"https:\u002F\u002Fcdn.example.com\u002Fvideos\u002Fmovie-id\u002Fhls\u002Fmaster.m3u8\" \u002F>\n","jsx",[3432,14973,14974,14998,15011,15015,15032,15050,15054,15068,15086,15103,15107,15124,15129,15146,15151,15167,15172,15182,15187,15206,15221,15258,15262,15282,15310,15315,15330,15340,15344,15354,15358,15415,15419,15423,15428],{"__ignoreMap":3569},[3603,14975,14976,14979,14981,14984,14986,14989,14992,14995],{"class":3605,"line":3606},[3603,14977,14978],{"class":5533},"import",[3603,14980,12973],{"class":5541},[3603,14982,14983],{"class":5576},"useEffect",[3603,14985,3470],{"class":5541},[3603,14987,14988],{"class":5576},"useRef",[3603,14990,14991],{"class":5541}," } ",[3603,14993,14994],{"class":5533},"from",[3603,14996,14997],{"class":5224}," 'react'\n",[3603,14999,15000,15002,15005,15008],{"class":3605,"line":3612},[3603,15001,14978],{"class":5533},[3603,15003,15004],{"class":5576}," Hls",[3603,15006,15007],{"class":5533}," from",[3603,15009,15010],{"class":5224}," 'hls.js'\n",[3603,15012,15013],{"class":3605,"line":3618},[3603,15014,3628],{"emptyLinePlaceholder":3627},[3603,15016,15017,15020,15023,15026,15029],{"class":3605,"line":3624},[3603,15018,15019],{"class":5243},"function",[3603,15021,15022],{"class":5220}," VideoPlayer",[3603,15024,15025],{"class":5541},"({ ",[3603,15027,15028],{"class":5576},"manifestUrl",[3603,15030,15031],{"class":5541}," }) {\n",[3603,15033,15034,15037,15040,15042,15044,15046,15048],{"class":3605,"line":3631},[3603,15035,15036],{"class":5243},"    const",[3603,15038,15039],{"class":12976}," videoRef",[3603,15041,5580],{"class":5541},[3603,15043,14988],{"class":5220},[3603,15045,5707],{"class":5541},[3603,15047,6124],{"class":5243},[3603,15049,6924],{"class":5541},[3603,15051,15052],{"class":3605,"line":3637},[3603,15053,3628],{"emptyLinePlaceholder":3627},[3603,15055,15056,15059,15062,15065],{"class":3605,"line":3643},[3603,15057,15058],{"class":5220},"    useEffect",[3603,15060,15061],{"class":5541},"(() ",[3603,15063,15064],{"class":5243},"=>",[3603,15066,15067],{"class":5541}," {\n",[3603,15069,15070,15073,15076,15078,15081,15083],{"class":3605,"line":3649},[3603,15071,15072],{"class":5243},"        const",[3603,15074,15075],{"class":12976}," video",[3603,15077,5580],{"class":5541},[3603,15079,15080],{"class":5576},"videoRef",[3603,15082,3453],{"class":5541},[3603,15084,15085],{"class":5576},"current\n",[3603,15087,15088,15091,15094,15097,15100],{"class":3605,"line":3655},[3603,15089,15090],{"class":5533},"        if",[3603,15092,15093],{"class":5541}," (!",[3603,15095,15096],{"class":5576},"video",[3603,15098,15099],{"class":5541},") ",[3603,15101,15102],{"class":5533},"return\n",[3603,15104,15105],{"class":3605,"line":3661},[3603,15106,3628],{"emptyLinePlaceholder":3627},[3603,15108,15109,15111,15113,15116,15118,15121],{"class":3605,"line":3667},[3603,15110,15090],{"class":5533},[3603,15112,10425],{"class":5541},[3603,15114,15115],{"class":5576},"Hls",[3603,15117,3453],{"class":5541},[3603,15119,15120],{"class":5220},"isSupported",[3603,15122,15123],{"class":5541},"()) {\n",[3603,15125,15126],{"class":3605,"line":3673},[3603,15127,15128],{"class":5214},"            \u002F\u002F Для більшості браузерів — використовуємо hls.js\n",[3603,15130,15131,15134,15137,15139,15141,15143],{"class":3605,"line":3678},[3603,15132,15133],{"class":5243},"            const",[3603,15135,15136],{"class":12976}," hls",[3603,15138,5580],{"class":5541},[3603,15140,5583],{"class":5243},[3603,15142,15004],{"class":5220},[3603,15144,15145],{"class":5541},"({\n",[3603,15147,15148],{"class":3605,"line":3684},[3603,15149,15150],{"class":5214},"                \u002F\u002F Починати з нижчої якості для швидкого старту\n",[3603,15152,15153,15156,15159,15162,15164],{"class":3605,"line":3690},[3603,15154,15155],{"class":5576},"                startLevel:",[3603,15157,15158],{"class":5541}," -",[3603,15160,15161],{"class":5943},"1",[3603,15163,3470],{"class":5541},[3603,15165,15166],{"class":5214},"\u002F\u002F -1 = auto\n",[3603,15168,15169],{"class":3605,"line":3696},[3603,15170,15171],{"class":5214},"                \u002F\u002F Кешувати в пам'яті не більше 30 сек контенту\n",[3603,15173,15174,15177,15180],{"class":3605,"line":3702},[3603,15175,15176],{"class":5576},"                maxBufferLength:",[3603,15178,15179],{"class":5943}," 30",[3603,15181,5604],{"class":5541},[3603,15183,15184],{"class":3605,"line":3708},[3603,15185,15186],{"class":5541},"            })\n",[3603,15188,15189,15192,15194,15197,15199,15201,15203],{"class":3605,"line":4667},[3603,15190,15191],{"class":5576},"            hls",[3603,15193,3453],{"class":5541},[3603,15195,15196],{"class":5220},"loadSource",[3603,15198,5707],{"class":5541},[3603,15200,15028],{"class":5576},[3603,15202,15099],{"class":5541},[3603,15204,15205],{"class":5214},"\u002F\u002F URL до master.m3u8\n",[3603,15207,15208,15210,15212,15215,15217,15219],{"class":3605,"line":4673},[3603,15209,15191],{"class":5576},[3603,15211,3453],{"class":5541},[3603,15213,15214],{"class":5220},"attachMedia",[3603,15216,5707],{"class":5541},[3603,15218,15096],{"class":5576},[3603,15220,6924],{"class":5541},[3603,15222,15223,15225,15227,15230,15232,15234,15236,15238,15240,15243,15246,15248,15250,15252,15255],{"class":3605,"line":4679},[3603,15224,15191],{"class":5576},[3603,15226,3453],{"class":5541},[3603,15228,15229],{"class":5220},"on",[3603,15231,5707],{"class":5541},[3603,15233,15115],{"class":5576},[3603,15235,3453],{"class":5541},[3603,15237,1911],{"class":5576},[3603,15239,3453],{"class":5541},[3603,15241,15242],{"class":12976},"MANIFEST_PARSED",[3603,15244,15245],{"class":5541},", () ",[3603,15247,15064],{"class":5243},[3603,15249,15075],{"class":5576},[3603,15251,3453],{"class":5541},[3603,15253,15254],{"class":5220},"play",[3603,15256,15257],{"class":5541},"())\n",[3603,15259,15260],{"class":3605,"line":4685},[3603,15261,3628],{"emptyLinePlaceholder":3627},[3603,15263,15264,15267,15270,15272,15274,15276,15279],{"class":3605,"line":4691},[3603,15265,15266],{"class":5533},"            return",[3603,15268,15269],{"class":5541}," () ",[3603,15271,15064],{"class":5243},[3603,15273,15136],{"class":5576},[3603,15275,3453],{"class":5541},[3603,15277,15278],{"class":5220},"destroy",[3603,15280,15281],{"class":5541},"()\n",[3603,15283,15284,15287,15290,15293,15295,15297,15299,15302,15304,15307],{"class":3605,"line":4696},[3603,15285,15286],{"class":5541},"        } ",[3603,15288,15289],{"class":5533},"else",[3603,15291,15292],{"class":5533}," if",[3603,15294,10425],{"class":5541},[3603,15296,15096],{"class":5576},[3603,15298,3453],{"class":5541},[3603,15300,15301],{"class":5220},"canPlayType",[3603,15303,5707],{"class":5541},[3603,15305,15306],{"class":5224},"'application\u002Fvnd.apple.mpegurl'",[3603,15308,15309],{"class":5541},")) {\n",[3603,15311,15312],{"class":3605,"line":4701},[3603,15313,15314],{"class":5214},"            \u002F\u002F Safari — нативна підтримка HLS\n",[3603,15316,15317,15320,15322,15325,15327],{"class":3605,"line":4707},[3603,15318,15319],{"class":5576},"            video",[3603,15321,3453],{"class":5541},[3603,15323,15324],{"class":5576},"src",[3603,15326,5580],{"class":5541},[3603,15328,15329],{"class":5576},"manifestUrl\n",[3603,15331,15332,15334,15336,15338],{"class":3605,"line":4713},[3603,15333,15319],{"class":5576},[3603,15335,3453],{"class":5541},[3603,15337,15254],{"class":5220},[3603,15339,15281],{"class":5541},[3603,15341,15342],{"class":3605,"line":4719},[3603,15343,6050],{"class":5541},[3603,15345,15346,15349,15351],{"class":3605,"line":4724},[3603,15347,15348],{"class":5541},"    }, [",[3603,15350,15028],{"class":5576},[3603,15352,15353],{"class":5541},"])\n",[3603,15355,15356],{"class":3605,"line":4730},[3603,15357,3628],{"emptyLinePlaceholder":3627},[3603,15359,15360,15362,15366,15369,15373,15375,15377,15379,15381,15384,15387,15389,15391,15394,15397,15400,15402,15405,15408,15410,15412],{"class":3605,"line":4736},[3603,15361,14931],{"class":5533},[3603,15363,15365],{"class":15364},"s0P7L"," \u003C",[3603,15367,15096],{"class":15368},"sKtos",[3603,15370,15372],{"class":15371},"sa4r_"," ref",[3603,15374,6507],{"class":5541},[3603,15376,5618],{"class":5243},[3603,15378,15080],{"class":5576},[3603,15380,5645],{"class":5243},[3603,15382,15383],{"class":15371}," controls",[3603,15385,15386],{"class":15371}," style",[3603,15388,6507],{"class":5541},[3603,15390,5618],{"class":5243},[3603,15392,15393],{"class":5617},"{ ",[3603,15395,15396],{"class":5576},"width:",[3603,15398,15399],{"class":5224}," '100%'",[3603,15401,3470],{"class":5617},[3603,15403,15404],{"class":5576},"maxHeight:",[3603,15406,15407],{"class":5224}," '600px'",[3603,15409,8179],{"class":5617},[3603,15411,5645],{"class":5243},[3603,15413,15414],{"class":15364}," \u002F>\n",[3603,15416,15417],{"class":3605,"line":4742},[3603,15418,3670],{"class":5541},[3603,15420,15421],{"class":3605,"line":4748},[3603,15422,3628],{"emptyLinePlaceholder":3627},[3603,15424,15425],{"class":3605,"line":4753},[3603,15426,15427],{"class":5214},"\u002F\u002F Використання:\n",[3603,15429,15430],{"class":3605,"line":4758},[3603,15431,15432],{"class":5214},"\u002F\u002F \u003CVideoPlayer manifestUrl=\"https:\u002F\u002Fcdn.example.com\u002Fvideos\u002Fmovie-id\u002Fhls\u002Fmaster.m3u8\" \u002F>\n",[3412,15434,15436],{"id":15435},"storage-class-для-відео-файлів","Storage Class для відео-файлів",[3353,15438,15439],{},"Різні типи відеофайлів вимагають різних Storage Class:",[3902,15441,15442,15454],{},[3905,15443,15444],{},[3908,15445,15446,15449,15451],{},[3911,15447,15448],{},"Тип файлу",[3911,15450,3722],{},[3911,15452,15453],{},"Причина",[3924,15455,15456,15467,15477,15487,15498],{},[3908,15457,15458,15461,15464],{},[3929,15459,15460],{},"Raw відео (оригінал)",[3929,15462,15463],{},"S3 Standard → через 90 днів Glacier",[3929,15465,15466],{},"Рідко потрібен після конвертації",[3908,15468,15469,15472,15474],{},[3929,15470,15471],{},"HLS\u002FDASH manifest (.m3u8, .mpd)",[3929,15473,4442],{},[3929,15475,15476],{},"Часто запитується плеєром",[3908,15478,15479,15482,15484],{},[3929,15480,15481],{},"HLS\u002FDASH сегменти (.ts, .m4s)",[3929,15483,4442],{},[3929,15485,15486],{},"Активно читаються під час перегляду",[3908,15488,15489,15492,15495],{},[3929,15490,15491],{},"Старі сезони серіалу",[3929,15493,15494],{},"S3 Intelligent-Tiering",[3929,15496,15497],{},"Патерн доступу непередбачуваний",[3908,15499,15500,15503,15506],{},[3929,15501,15502],{},"Архівні відео",[3929,15504,15505],{},"S3 Glacier Instant Retrieval",[3929,15507,15508],{},"Потрібні рідко, але потрібен швидкий доступ",[3405,15510],{},[3348,15512,15514],{"id":15513},"aws-sdk-for-net-повна-інтеграція-з-s3","AWS SDK for .NET — повна інтеграція з S3",[3412,15516,15518],{"id":15517},"встановлення-та-налаштування","Встановлення та налаштування",[3353,15520,15521],{},"Для роботи з S3 з .NET потрібні два пакети:",[3369,15523,15524,15532],{},[3372,15525,15526,15531],{},[3356,15527,15528],{},[3432,15529,15530],{},"AWSSDK.S3"," — основний клієнт: upload, download, presigned URLs, bucket operations",[3372,15533,15534,15539,15540,15543,15544,15546,15547],{},[3356,15535,15536],{},[3432,15537,15538],{},"AWSSDK.Extensions.NETCore.Setup"," — інтеграція з DI ASP.NET Core: ",[3432,15541,15542],{},"AddAWSService\u003CT>()"," реєструє ",[3432,15545,11020],{}," як singleton і автоматично читає регіон з ",[3432,15548,15549],{},"appsettings.json",[3561,15551,15553],{"className":5205,"code":15552,"language":5207,"meta":3569,"style":3569},"# Встановіть NuGet пакет\ndotnet add package AWSSDK.S3\ndotnet add package AWSSDK.Extensions.NETCore.Setup\n",[3432,15554,15555,15560,15574],{"__ignoreMap":3569},[3603,15556,15557],{"class":3605,"line":3606},[3603,15558,15559],{"class":5214},"# Встановіть NuGet пакет\n",[3603,15561,15562,15565,15568,15571],{"class":3605,"line":3612},[3603,15563,15564],{"class":5220},"dotnet",[3603,15566,15567],{"class":5224}," add",[3603,15569,15570],{"class":5224}," package",[3603,15572,15573],{"class":5224}," AWSSDK.S3\n",[3603,15575,15576,15578,15580,15582],{"class":3605,"line":3618},[3603,15577,15564],{"class":5220},[3603,15579,15567],{"class":5224},[3603,15581,15570],{"class":5224},[3603,15583,15584],{"class":5224}," AWSSDK.Extensions.NETCore.Setup\n",[3353,15586,15587,15590],{},[3356,15588,15589],{},"Як SDK знаходить AWS credentials"," — SDK перебирає джерела по черзі, зупиняється на першому знайденому:",[3561,15592,15594],{"className":5519,"code":15593,"language":5521,"meta":3569,"style":3569},"\u002F\u002F Program.cs — реєстрація S3 через DI\nbuilder.Services.AddAWSService\u003CIAmazonS3>();\n\u002F\u002F SDK автоматично читає credentials з:\n\u002F\u002F 1. Змінних середовища (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)\n\u002F\u002F 2. ~\u002F.aws\u002Fcredentials (локально)\n\u002F\u002F 3. IAM Role (на EC2\u002FECS\u002FLambda) ← рекомендовано для production\n",[3432,15595,15596,15601,15623,15628,15633,15638],{"__ignoreMap":3569},[3603,15597,15598],{"class":3605,"line":3606},[3603,15599,15600],{"class":5214},"\u002F\u002F Program.cs — реєстрація S3 через DI\n",[3603,15602,15603,15606,15608,15611,15613,15616,15618,15620],{"class":3605,"line":3612},[3603,15604,15605],{"class":5576},"builder",[3603,15607,3453],{"class":5541},[3603,15609,15610],{"class":5576},"Services",[3603,15612,3453],{"class":5541},[3603,15614,15615],{"class":5220},"AddAWSService",[3603,15617,11250],{"class":5541},[3603,15619,11020],{"class":5537},[3603,15621,15622],{"class":5541},">();\n",[3603,15624,15625],{"class":3605,"line":3618},[3603,15626,15627],{"class":5214},"\u002F\u002F SDK автоматично читає credentials з:\n",[3603,15629,15630],{"class":3605,"line":3624},[3603,15631,15632],{"class":5214},"\u002F\u002F 1. Змінних середовища (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)\n",[3603,15634,15635],{"class":3605,"line":3631},[3603,15636,15637],{"class":5214},"\u002F\u002F 2. ~\u002F.aws\u002Fcredentials (локально)\n",[3603,15639,15640],{"class":3605,"line":3637},[3603,15641,15642],{"class":5214},"\u002F\u002F 3. IAM Role (на EC2\u002FECS\u002FLambda) ← рекомендовано для production\n",[3353,15644,15645,15646,15649,15650,15653,15654,15657,15658,3470,15660,15662],{},"Локально зручно використовувати ",[3432,15647,15648],{},"~\u002F.aws\u002Fcredentials"," після ",[3432,15651,15652],{},"aws configure",". На сервері (EC2, ECS, Lambda) — ",[3356,15655,15656],{},"ніколи не кладіть ключі у змінні середовища або код",": натомість призначте IAM Role з ",[3432,15659,9512],{},[3432,15661,9515],{}," на потрібний bucket — SDK підхопить їх автоматично.",[3561,15664,15666],{"className":5851,"code":15665,"language":5853,"meta":3569,"style":3569},"\u002F\u002F appsettings.json\n{\n    \"AWS\": {\n        \"Region\": \"eu-central-1\"\n    },\n    \"S3\": {\n        \"BucketName\": \"my-app-bucket\"\n    }\n}\n",[3432,15667,15668,15673,15677,15684,15693,15698,15705,15714,15718],{"__ignoreMap":3569},[3603,15669,15670],{"class":3605,"line":3606},[3603,15671,15672],{"class":5214},"\u002F\u002F appsettings.json\n",[3603,15674,15675],{"class":3605,"line":3612},[3603,15676,5591],{"class":5541},[3603,15678,15679,15682],{"class":3605,"line":3618},[3603,15680,15681],{"class":5864},"    \"AWS\"",[3603,15683,5906],{"class":5541},[3603,15685,15686,15689,15691],{"class":3605,"line":3624},[3603,15687,15688],{"class":5864},"        \"Region\"",[3603,15690,5881],{"class":5541},[3603,15692,6520],{"class":5224},[3603,15694,15695],{"class":3605,"line":3631},[3603,15696,15697],{"class":5541},"    },\n",[3603,15699,15700,15703],{"class":3605,"line":3637},[3603,15701,15702],{"class":5864},"    \"S3\"",[3603,15704,5906],{"class":5541},[3603,15706,15707,15710,15712],{"class":3605,"line":3643},[3603,15708,15709],{"class":5864},"        \"BucketName\"",[3603,15711,5881],{"class":5541},[3603,15713,6510],{"class":5224},[3603,15715,15716],{"class":3605,"line":3649},[3603,15717,3664],{"class":5541},[3603,15719,15720],{"class":3605,"line":3655},[3603,15721,3670],{"class":5541},[3412,15723,15725],{"id":15724},"повний-s3service-для-net","Повний S3Service для .NET",[3353,15727,15728,15729,15731,15732,15735],{},"Замість того щоб викликати ",[3432,15730,11020],{}," напряму в контролерах, виносимо всю логіку роботи з S3 в окремий сервіс ",[3432,15733,15734],{},"S3StorageService",". Це дає три переваги:",[3369,15737,15738,15744,15756],{},[3372,15739,15740,15743],{},[3356,15741,15742],{},"Єдине місце"," для зміни bucket, регіону, storage class — не шукати по всьому проєкту",[3372,15745,15746,15749,15750,15752,15753,15755],{},[3356,15747,15748],{},"Тестованість"," — можна замокати ",[3432,15751,11020],{}," або весь ",[3432,15754,15734],{}," в unit-тестах",[3372,15757,15758,15761],{},[3356,15759,15760],{},"Інкапсуляція деталей"," — контролер не знає про S3, він працює з абстракцією «зберегти файл»",[3353,15763,15764,15765,15770,15771,15774],{},"У сервісі використовується ",[3356,15766,15767],{},[3432,15768,15769],{},"TransferUtility"," — обгортка над сирим S3 API, яка автоматично вмикає ",[3356,15772,15773],{},"Multipart Upload"," для файлів > 16 MB (розбиває на частини, завантажує паралельно) і повторює спроби при тимчасових помилках мережі. Для більшості задач краще використовувати саме її.",[3561,15776,15778],{"className":5519,"code":15777,"language":5521,"meta":3569,"style":3569},"using Amazon.S3;\nusing Amazon.S3.Model;\nusing Amazon.S3.Transfer;\nusing Microsoft.Extensions.Configuration;\n\npublic class S3StorageService\n{\n    private readonly IAmazonS3 _s3;\n    private readonly string _bucket;\n\n    public S3StorageService(IAmazonS3 s3, IConfiguration config)\n    {\n        _s3 = s3;\n        \u002F\u002F ЗАМІНІТЬ значення \"S3:BucketName\" реальним іменем вашого bucket\n        _bucket = config[\"S3:BucketName\"] ?? throw new InvalidOperationException(\"S3:BucketName not configured\");\n    }\n\n    \u002F\u002F Завантаження файлу в S3\n    \u002F\u002F key — шлях у bucket, наприклад \"uploads\u002Fusers\u002F123\u002Favatar.jpg\"\n    public async Task\u003Cstring> UploadAsync(Stream fileStream, string key, string contentType)\n    {\n        using var transferUtility = new TransferUtility(_s3);\n\n        await transferUtility.UploadAsync(new TransferUtilityUploadRequest\n        {\n            BucketName = _bucket,\n            Key = key,\n            InputStream = fileStream,\n            ContentType = contentType,\n            \u002F\u002F Встановити Storage Class (за замовчуванням Standard)\n            StorageClass = S3StorageClass.Standard,\n            \u002F\u002F Метадані об'єкту\n            Metadata =\n            {\n                [\"x-amz-meta-uploaded-at\"] = DateTime.UtcNow.ToString(\"O\"),\n                [\"x-amz-meta-content-type\"] = contentType\n            }\n        });\n\n        return $\"https:\u002F\u002F{_bucket}.s3.eu-central-1.amazonaws.com\u002F{key}\";\n    }\n\n    \u002F\u002F Завантаження файлу з S3\n    public async Task\u003CStream> DownloadAsync(string key)\n    {\n        var response = await _s3.GetObjectAsync(new GetObjectRequest\n        {\n            BucketName = _bucket,\n            Key = key\n        });\n        return response.ResponseStream;\n    }\n\n    \u002F\u002F Перелік файлів за префіксом (аналог \"директорії\")\n    public async Task\u003CList\u003Cstring>> ListFilesAsync(string prefix)\n    {\n        var response = await _s3.ListObjectsV2Async(new ListObjectsV2Request\n        {\n            BucketName = _bucket,\n            Prefix = prefix,\n            MaxKeys = 1000\n        });\n        return response.S3Objects.Select(o => o.Key).ToList();\n    }\n\n    \u002F\u002F Видалення об'єкту\n    public async Task DeleteAsync(string key)\n    {\n        await _s3.DeleteObjectAsync(_bucket, key);\n    }\n\n    \u002F\u002F Presigned URL для завантаження (GET)\n    public string GetPresignedDownloadUrl(string key, int expiresInMinutes = 60)\n    {\n        return _s3.GetPreSignedURL(new GetPreSignedUrlRequest\n        {\n            BucketName = _bucket,\n            Key = key,\n            Expires = DateTime.UtcNow.AddMinutes(expiresInMinutes),\n            Verb = HttpVerb.GET\n        });\n    }\n\n    \u002F\u002F Presigned URL для прямого завантаження з браузера (PUT)\n    public string GetPresignedUploadUrl(string key, string contentType,\n        int expiresInMinutes = 10)\n    {\n        return _s3.GetPreSignedURL(new GetPreSignedUrlRequest\n        {\n            BucketName = _bucket,\n            Key = key,\n            Expires = DateTime.UtcNow.AddMinutes(expiresInMinutes),\n            Verb = HttpVerb.PUT,\n            ContentType = contentType\n        });\n    }\n\n    \u002F\u002F Multipart upload для великих файлів (> 100 MB)\n    \u002F\u002F TransferUtility автоматично розбиває файл на PartSize-частини\n    \u002F\u002F і завантажує їх паралельно (ThreadsCount потоків).\n    \u002F\u002F Якщо завантаження перервалось — можна відновити з місця зупинки\n    \u002F\u002F через InitiateMultipartUploadRequest \u002F UploadPartRequest вручну.\n    \u002F\u002F 500 MB при ThreadsCount=5 завантажуються приблизно в 5x швидше ніж послідовно.\n    public async Task UploadLargeFileAsync(string filePath, string key)\n    {\n        using var transferUtility = new TransferUtility(_s3);\n        await transferUtility.UploadAsync(new TransferUtilityUploadRequest\n        {\n            BucketName = _bucket,\n            Key = key,\n            FilePath = filePath,\n            \u002F\u002F Multipart: частини по 50 MB, до 5 паралельних потоків\n            PartSize = 50 * 1024 * 1024,\n            ThreadsCount = 5\n        });\n    }\n\n    \u002F\u002F Копіювання об'єкту всередині S3 (без завантаження на клієнт)\n    \u002F\u002F CopyObjectRequest — S3 копіює файл на своєму боці, трафік не йде через ваш сервер.\n    \u002F\u002F Використовується для: перейменування (copy + delete), зміни storage class,\n    \u002F\u002F переміщення між bucket'ами, або дублювання файлу при збереженні версії.\n    public async Task CopyAsync(string sourceKey, string destinationKey)\n    {\n        await _s3.CopyObjectAsync(new CopyObjectRequest\n        {\n            SourceBucket = _bucket,\n            SourceKey = sourceKey,\n            DestinationBucket = _bucket,\n            DestinationKey = destinationKey\n        });\n    }\n}\n",[3432,15779,15780,15792,15808,15825,15844,15848,15857,15861,15873,15886,15890,15912,15916,15927,15932,15966,15970,15974,15979,15984,16022,16026,16050,16054,16072,16076,16087,16097,16108,16119,16124,16139,16144,16152,16157,16186,16197,16201,16206,16210,16236,16240,16244,16249,16274,16278,16301,16305,16315,16324,16328,16340,16344,16348,16353,16385,16389,16413,16417,16427,16439,16449,16453,16491,16496,16501,16507,16527,16532,16554,16559,16564,16570,16599,16604,16621,16626,16637,16648,16671,16684,16689,16694,16699,16705,16729,16744,16749,16766,16771,16782,16793,16816,16831,16840,16845,16850,16855,16861,16867,16873,16879,16885,16891,16918,16923,16944,16961,16966,16977,16988,17001,17007,17030,17040,17045,17050,17055,17061,17067,17073,17079,17107,17112,17129,17134,17146,17159,17171,17182,17187,17192],{"__ignoreMap":3569},[3603,15781,15782,15784,15786,15788,15790],{"class":3605,"line":3606},[3603,15783,5534],{"class":5533},[3603,15785,5538],{"class":5537},[3603,15787,3453],{"class":5541},[3603,15789,5544],{"class":5537},[3603,15791,5547],{"class":5541},[3603,15793,15794,15796,15798,15800,15802,15804,15806],{"class":3605,"line":3612},[3603,15795,5534],{"class":5533},[3603,15797,5538],{"class":5537},[3603,15799,3453],{"class":5541},[3603,15801,5544],{"class":5537},[3603,15803,3453],{"class":5541},[3603,15805,5562],{"class":5537},[3603,15807,5547],{"class":5541},[3603,15809,15810,15812,15814,15816,15818,15820,15823],{"class":3605,"line":3618},[3603,15811,5534],{"class":5533},[3603,15813,5538],{"class":5537},[3603,15815,3453],{"class":5541},[3603,15817,5544],{"class":5537},[3603,15819,3453],{"class":5541},[3603,15821,15822],{"class":5537},"Transfer",[3603,15824,5547],{"class":5541},[3603,15826,15827,15829,15832,15834,15837,15839,15842],{"class":3605,"line":3624},[3603,15828,5534],{"class":5533},[3603,15830,15831],{"class":5537}," Microsoft",[3603,15833,3453],{"class":5541},[3603,15835,15836],{"class":5537},"Extensions",[3603,15838,3453],{"class":5541},[3603,15840,15841],{"class":5537},"Configuration",[3603,15843,5547],{"class":5541},[3603,15845,15846],{"class":3605,"line":3631},[3603,15847,3628],{"emptyLinePlaceholder":3627},[3603,15849,15850,15852,15854],{"class":3605,"line":3637},[3603,15851,10953],{"class":5243},[3603,15853,10956],{"class":5243},[3603,15855,15856],{"class":5537}," S3StorageService\n",[3603,15858,15859],{"class":3605,"line":3643},[3603,15860,5591],{"class":5541},[3603,15862,15863,15865,15867,15869,15871],{"class":3605,"line":3649},[3603,15864,10968],{"class":5243},[3603,15866,10971],{"class":5243},[3603,15868,10974],{"class":5537},[3603,15870,10977],{"class":5576},[3603,15872,5547],{"class":5541},[3603,15874,15875,15877,15879,15881,15884],{"class":3605,"line":3655},[3603,15876,10968],{"class":5243},[3603,15878,10971],{"class":5243},[3603,15880,10994],{"class":5243},[3603,15882,15883],{"class":5576}," _bucket",[3603,15885,5547],{"class":5541},[3603,15887,15888],{"class":3605,"line":3661},[3603,15889,3628],{"emptyLinePlaceholder":3627},[3603,15891,15892,15894,15897,15899,15901,15903,15905,15908,15910],{"class":3605,"line":3667},[3603,15893,11012],{"class":5243},[3603,15895,15896],{"class":5220}," S3StorageService",[3603,15898,5707],{"class":5541},[3603,15900,11020],{"class":5537},[3603,15902,5225],{"class":5576},[3603,15904,3470],{"class":5541},[3603,15906,15907],{"class":5537},"IConfiguration",[3603,15909,14355],{"class":5576},[3603,15911,6924],{"class":5541},[3603,15913,15914],{"class":3605,"line":3673},[3603,15915,11086],{"class":5541},[3603,15917,15918,15921,15923,15925],{"class":3605,"line":3678},[3603,15919,15920],{"class":5576},"        _s3",[3603,15922,5580],{"class":5541},[3603,15924,11033],{"class":5576},[3603,15926,5547],{"class":5541},[3603,15928,15929],{"class":3605,"line":3684},[3603,15930,15931],{"class":5214},"        \u002F\u002F ЗАМІНІТЬ значення \"S3:BucketName\" реальним іменем вашого bucket\n",[3603,15933,15934,15937,15939,15941,15944,15947,15950,15953,15956,15959,15961,15964],{"class":3605,"line":3690},[3603,15935,15936],{"class":5576},"        _bucket",[3603,15938,5580],{"class":5541},[3603,15940,14417],{"class":5576},[3603,15942,15943],{"class":5541},"[",[3603,15945,15946],{"class":5224},"\"S3:BucketName\"",[3603,15948,15949],{"class":5541},"] ?? ",[3603,15951,15952],{"class":5533},"throw",[3603,15954,15955],{"class":5243}," new",[3603,15957,15958],{"class":5537}," InvalidOperationException",[3603,15960,5707],{"class":5541},[3603,15962,15963],{"class":5224},"\"S3:BucketName not configured\"",[3603,15965,5713],{"class":5541},[3603,15967,15968],{"class":3605,"line":3696},[3603,15969,3664],{"class":5541},[3603,15971,15972],{"class":3605,"line":3702},[3603,15973,3628],{"emptyLinePlaceholder":3627},[3603,15975,15976],{"class":3605,"line":3708},[3603,15977,15978],{"class":5214},"    \u002F\u002F Завантаження файлу в S3\n",[3603,15980,15981],{"class":3605,"line":4667},[3603,15982,15983],{"class":5214},"    \u002F\u002F key — шлях у bucket, наприклад \"uploads\u002Fusers\u002F123\u002Favatar.jpg\"\n",[3603,15985,15986,15988,15990,15992,15994,15996,15998,16001,16003,16005,16008,16010,16012,16014,16016,16018,16020],{"class":3605,"line":4673},[3603,15987,11012],{"class":5243},[3603,15989,11051],{"class":5243},[3603,15991,11054],{"class":5537},[3603,15993,11250],{"class":5541},[3603,15995,7598],{"class":5243},[3603,15997,11255],{"class":5541},[3603,15999,16000],{"class":5220},"UploadAsync",[3603,16002,5707],{"class":5541},[3603,16004,11076],{"class":5537},[3603,16006,16007],{"class":5576}," fileStream",[3603,16009,3470],{"class":5541},[3603,16011,7598],{"class":5243},[3603,16013,11071],{"class":5576},[3603,16015,3470],{"class":5541},[3603,16017,7598],{"class":5243},[3603,16019,12821],{"class":5576},[3603,16021,6924],{"class":5541},[3603,16023,16024],{"class":3605,"line":4679},[3603,16025,11086],{"class":5541},[3603,16027,16028,16031,16034,16037,16039,16041,16044,16046,16048],{"class":3605,"line":4685},[3603,16029,16030],{"class":5533},"        using",[3603,16032,16033],{"class":5243}," var",[3603,16035,16036],{"class":5576}," transferUtility",[3603,16038,5580],{"class":5541},[3603,16040,5583],{"class":5243},[3603,16042,16043],{"class":5537}," TransferUtility",[3603,16045,5707],{"class":5541},[3603,16047,11028],{"class":5576},[3603,16049,5713],{"class":5541},[3603,16051,16052],{"class":3605,"line":4691},[3603,16053,3628],{"emptyLinePlaceholder":3627},[3603,16055,16056,16059,16061,16063,16065,16067,16069],{"class":3605,"line":4696},[3603,16057,16058],{"class":5243},"        await",[3603,16060,16036],{"class":5576},[3603,16062,3453],{"class":5541},[3603,16064,16000],{"class":5220},[3603,16066,5707],{"class":5541},[3603,16068,5583],{"class":5243},[3603,16070,16071],{"class":5537}," TransferUtilityUploadRequest\n",[3603,16073,16074],{"class":3605,"line":4701},[3603,16075,5873],{"class":5541},[3603,16077,16078,16080,16082,16085],{"class":3605,"line":4707},[3603,16079,11109],{"class":5576},[3603,16081,5580],{"class":5541},[3603,16083,16084],{"class":5576},"_bucket",[3603,16086,5604],{"class":5541},[3603,16088,16089,16091,16093,16095],{"class":3605,"line":4713},[3603,16090,11121],{"class":5576},[3603,16092,5580],{"class":5541},[3603,16094,11126],{"class":5576},[3603,16096,5604],{"class":5541},[3603,16098,16099,16101,16103,16106],{"class":3605,"line":4719},[3603,16100,11133],{"class":5576},[3603,16102,5580],{"class":5541},[3603,16104,16105],{"class":5576},"fileStream",[3603,16107,5604],{"class":5541},[3603,16109,16110,16112,16114,16117],{"class":3605,"line":4724},[3603,16111,12915],{"class":5576},[3603,16113,5580],{"class":5541},[3603,16115,16116],{"class":5576},"contentType",[3603,16118,5604],{"class":5541},[3603,16120,16121],{"class":3605,"line":4730},[3603,16122,16123],{"class":5214},"            \u002F\u002F Встановити Storage Class (за замовчуванням Standard)\n",[3603,16125,16126,16129,16131,16133,16135,16137],{"class":3605,"line":4736},[3603,16127,16128],{"class":5576},"            StorageClass",[3603,16130,5580],{"class":5541},[3603,16132,5677],{"class":5576},[3603,16134,3453],{"class":5541},[3603,16136,3931],{"class":5576},[3603,16138,5604],{"class":5541},[3603,16140,16141],{"class":3605,"line":4742},[3603,16142,16143],{"class":5214},"            \u002F\u002F Метадані об'єкту\n",[3603,16145,16146,16149],{"class":3605,"line":4748},[3603,16147,16148],{"class":5576},"            Metadata",[3603,16150,16151],{"class":5541}," =\n",[3603,16153,16154],{"class":3605,"line":4753},[3603,16155,16156],{"class":5541},"            {\n",[3603,16158,16159,16162,16165,16168,16170,16172,16174,16176,16179,16181,16184],{"class":3605,"line":4758},[3603,16160,16161],{"class":5541},"                [",[3603,16163,16164],{"class":5224},"\"x-amz-meta-uploaded-at\"",[3603,16166,16167],{"class":5541},"] = ",[3603,16169,5621],{"class":5576},[3603,16171,3453],{"class":5541},[3603,16173,5626],{"class":5576},[3603,16175,3453],{"class":5541},[3603,16177,16178],{"class":5220},"ToString",[3603,16180,5707],{"class":5541},[3603,16182,16183],{"class":5224},"\"O\"",[3603,16185,12741],{"class":5541},[3603,16187,16188,16190,16193,16195],{"class":3605,"line":6363},[3603,16189,16161],{"class":5541},[3603,16191,16192],{"class":5224},"\"x-amz-meta-content-type\"",[3603,16194,16167],{"class":5541},[3603,16196,12920],{"class":5576},[3603,16198,16199],{"class":3605,"line":6368},[3603,16200,6045],{"class":5541},[3603,16202,16203],{"class":3605,"line":6830},[3603,16204,16205],{"class":5541},"        });\n",[3603,16207,16208],{"class":3605,"line":6835},[3603,16209,3628],{"emptyLinePlaceholder":3627},[3603,16211,16212,16214,16217,16219,16221,16223,16226,16228,16230,16232,16234],{"class":3605,"line":6841},[3603,16213,11308],{"class":5533},[3603,16215,16216],{"class":5224}," $\"https:\u002F\u002F",[3603,16218,5618],{"class":5617},[3603,16220,16084],{"class":5576},[3603,16222,5645],{"class":5617},[3603,16224,16225],{"class":5224},".s3.eu-central-1.amazonaws.com\u002F",[3603,16227,5618],{"class":5617},[3603,16229,11126],{"class":5576},[3603,16231,5645],{"class":5617},[3603,16233,6554],{"class":5224},[3603,16235,5547],{"class":5541},[3603,16237,16238],{"class":3605,"line":6858},[3603,16239,3664],{"class":5541},[3603,16241,16242],{"class":3605,"line":6871},[3603,16243,3628],{"emptyLinePlaceholder":3627},[3603,16245,16246],{"class":3605,"line":6881},[3603,16247,16248],{"class":5214},"    \u002F\u002F Завантаження файлу з S3\n",[3603,16250,16251,16253,16255,16257,16259,16261,16263,16266,16268,16270,16272],{"class":3605,"line":6894},[3603,16252,11012],{"class":5243},[3603,16254,11051],{"class":5243},[3603,16256,11054],{"class":5537},[3603,16258,11250],{"class":5541},[3603,16260,11076],{"class":5537},[3603,16262,11255],{"class":5541},[3603,16264,16265],{"class":5220},"DownloadAsync",[3603,16267,5707],{"class":5541},[3603,16269,7598],{"class":5243},[3603,16271,11071],{"class":5576},[3603,16273,6924],{"class":5541},[3603,16275,16276],{"class":3605,"line":6915},[3603,16277,11086],{"class":5541},[3603,16279,16280,16282,16284,16286,16288,16290,16292,16294,16296,16298],{"class":3605,"line":6927},[3603,16281,11091],{"class":5243},[3603,16283,11190],{"class":5576},[3603,16285,5580],{"class":5541},[3603,16287,5696],{"class":5243},[3603,16289,10977],{"class":5576},[3603,16291,3453],{"class":5541},[3603,16293,11293],{"class":5220},[3603,16295,5707],{"class":5541},[3603,16297,5583],{"class":5243},[3603,16299,16300],{"class":5537}," GetObjectRequest\n",[3603,16302,16303],{"class":3605,"line":6932},[3603,16304,5873],{"class":5541},[3603,16306,16307,16309,16311,16313],{"class":3605,"line":6946},[3603,16308,11109],{"class":5576},[3603,16310,5580],{"class":5541},[3603,16312,16084],{"class":5576},[3603,16314,5604],{"class":5541},[3603,16316,16317,16319,16321],{"class":3605,"line":6951},[3603,16318,11121],{"class":5576},[3603,16320,5580],{"class":5541},[3603,16322,16323],{"class":5576},"key\n",[3603,16325,16326],{"class":3605,"line":6963},[3603,16327,16205],{"class":5541},[3603,16329,16330,16332,16334,16336,16338],{"class":3605,"line":6976},[3603,16331,11308],{"class":5533},[3603,16333,11190],{"class":5576},[3603,16335,3453],{"class":5541},[3603,16337,11315],{"class":5576},[3603,16339,5547],{"class":5541},[3603,16341,16342],{"class":3605,"line":6985},[3603,16343,3664],{"class":5541},[3603,16345,16346],{"class":3605,"line":6998},[3603,16347,3628],{"emptyLinePlaceholder":3627},[3603,16349,16350],{"class":3605,"line":7009},[3603,16351,16352],{"class":5214},"    \u002F\u002F Перелік файлів за префіксом (аналог \"директорії\")\n",[3603,16354,16355,16357,16359,16361,16363,16366,16368,16370,16373,16376,16378,16380,16383],{"class":3605,"line":7014},[3603,16356,11012],{"class":5243},[3603,16358,11051],{"class":5243},[3603,16360,11054],{"class":5537},[3603,16362,11250],{"class":5541},[3603,16364,16365],{"class":5537},"List",[3603,16367,11250],{"class":5541},[3603,16369,7598],{"class":5243},[3603,16371,16372],{"class":5541},">> ",[3603,16374,16375],{"class":5220},"ListFilesAsync",[3603,16377,5707],{"class":5541},[3603,16379,7598],{"class":5243},[3603,16381,16382],{"class":5576}," prefix",[3603,16384,6924],{"class":5541},[3603,16386,16387],{"class":3605,"line":8491},[3603,16388,11086],{"class":5541},[3603,16390,16391,16393,16395,16397,16399,16401,16403,16406,16408,16410],{"class":3605,"line":8497},[3603,16392,11091],{"class":5243},[3603,16394,11190],{"class":5576},[3603,16396,5580],{"class":5541},[3603,16398,5696],{"class":5243},[3603,16400,10977],{"class":5576},[3603,16402,3453],{"class":5541},[3603,16404,16405],{"class":5220},"ListObjectsV2Async",[3603,16407,5707],{"class":5541},[3603,16409,5583],{"class":5243},[3603,16411,16412],{"class":5537}," ListObjectsV2Request\n",[3603,16414,16415],{"class":3605,"line":8503},[3603,16416,5873],{"class":5541},[3603,16418,16419,16421,16423,16425],{"class":3605,"line":8509},[3603,16420,11109],{"class":5576},[3603,16422,5580],{"class":5541},[3603,16424,16084],{"class":5576},[3603,16426,5604],{"class":5541},[3603,16428,16429,16432,16434,16437],{"class":3605,"line":8515},[3603,16430,16431],{"class":5576},"            Prefix",[3603,16433,5580],{"class":5541},[3603,16435,16436],{"class":5576},"prefix",[3603,16438,5604],{"class":5541},[3603,16440,16441,16444,16446],{"class":3605,"line":8521},[3603,16442,16443],{"class":5576},"            MaxKeys",[3603,16445,5580],{"class":5541},[3603,16447,16448],{"class":5943},"1000\n",[3603,16450,16451],{"class":3605,"line":8527},[3603,16452,16205],{"class":5541},[3603,16454,16455,16457,16459,16461,16464,16466,16469,16471,16474,16477,16479,16481,16483,16485,16488],{"class":3605,"line":8532},[3603,16456,11308],{"class":5533},[3603,16458,11190],{"class":5576},[3603,16460,3453],{"class":5541},[3603,16462,16463],{"class":5576},"S3Objects",[3603,16465,3453],{"class":5541},[3603,16467,16468],{"class":5220},"Select",[3603,16470,5707],{"class":5541},[3603,16472,16473],{"class":5576},"o",[3603,16475,16476],{"class":5541}," => ",[3603,16478,16473],{"class":5576},[3603,16480,3453],{"class":5541},[3603,16482,3558],{"class":5576},[3603,16484,9535],{"class":5541},[3603,16486,16487],{"class":5220},"ToList",[3603,16489,16490],{"class":5541},"();\n",[3603,16492,16494],{"class":3605,"line":16493},64,[3603,16495,3664],{"class":5541},[3603,16497,16499],{"class":3605,"line":16498},65,[3603,16500,3628],{"emptyLinePlaceholder":3627},[3603,16502,16504],{"class":3605,"line":16503},66,[3603,16505,16506],{"class":5214},"    \u002F\u002F Видалення об'єкту\n",[3603,16508,16510,16512,16514,16516,16519,16521,16523,16525],{"class":3605,"line":16509},67,[3603,16511,11012],{"class":5243},[3603,16513,11051],{"class":5243},[3603,16515,11054],{"class":5537},[3603,16517,16518],{"class":5220}," DeleteAsync",[3603,16520,5707],{"class":5541},[3603,16522,7598],{"class":5243},[3603,16524,11071],{"class":5576},[3603,16526,6924],{"class":5541},[3603,16528,16530],{"class":3605,"line":16529},68,[3603,16531,11086],{"class":5541},[3603,16533,16535,16537,16539,16541,16544,16546,16548,16550,16552],{"class":3605,"line":16534},69,[3603,16536,16058],{"class":5243},[3603,16538,10977],{"class":5576},[3603,16540,3453],{"class":5541},[3603,16542,16543],{"class":5220},"DeleteObjectAsync",[3603,16545,5707],{"class":5541},[3603,16547,16084],{"class":5576},[3603,16549,3470],{"class":5541},[3603,16551,11126],{"class":5576},[3603,16553,5713],{"class":5541},[3603,16555,16557],{"class":3605,"line":16556},70,[3603,16558,3664],{"class":5541},[3603,16560,16562],{"class":3605,"line":16561},71,[3603,16563,3628],{"emptyLinePlaceholder":3627},[3603,16565,16567],{"class":3605,"line":16566},72,[3603,16568,16569],{"class":5214},"    \u002F\u002F Presigned URL для завантаження (GET)\n",[3603,16571,16573,16575,16577,16580,16582,16584,16586,16588,16590,16592,16594,16597],{"class":3605,"line":16572},73,[3603,16574,11012],{"class":5243},[3603,16576,10994],{"class":5243},[3603,16578,16579],{"class":5220}," GetPresignedDownloadUrl",[3603,16581,5707],{"class":5541},[3603,16583,7598],{"class":5243},[3603,16585,11071],{"class":5576},[3603,16587,3470],{"class":5541},[3603,16589,12664],{"class":5243},[3603,16591,12667],{"class":5576},[3603,16593,5580],{"class":5541},[3603,16595,16596],{"class":5943},"60",[3603,16598,6924],{"class":5541},[3603,16600,16602],{"class":3605,"line":16601},74,[3603,16603,11086],{"class":5541},[3603,16605,16607,16609,16611,16613,16615,16617,16619],{"class":3605,"line":16606},75,[3603,16608,11308],{"class":5533},[3603,16610,10977],{"class":5576},[3603,16612,3453],{"class":5541},[3603,16614,12771],{"class":5220},[3603,16616,5707],{"class":5541},[3603,16618,5583],{"class":5243},[3603,16620,12691],{"class":5537},[3603,16622,16624],{"class":3605,"line":16623},76,[3603,16625,5873],{"class":5541},[3603,16627,16629,16631,16633,16635],{"class":3605,"line":16628},77,[3603,16630,11109],{"class":5576},[3603,16632,5580],{"class":5541},[3603,16634,16084],{"class":5576},[3603,16636,5604],{"class":5541},[3603,16638,16640,16642,16644,16646],{"class":3605,"line":16639},78,[3603,16641,11121],{"class":5576},[3603,16643,5580],{"class":5541},[3603,16645,11126],{"class":5576},[3603,16647,5604],{"class":5541},[3603,16649,16651,16653,16655,16657,16659,16661,16663,16665,16667,16669],{"class":3605,"line":16650},79,[3603,16652,12720],{"class":5576},[3603,16654,5580],{"class":5541},[3603,16656,5621],{"class":5576},[3603,16658,3453],{"class":5541},[3603,16660,5626],{"class":5576},[3603,16662,3453],{"class":5541},[3603,16664,12733],{"class":5220},[3603,16666,5707],{"class":5541},[3603,16668,12738],{"class":5576},[3603,16670,12741],{"class":5541},[3603,16672,16674,16676,16678,16680,16682],{"class":3605,"line":16673},80,[3603,16675,12746],{"class":5576},[3603,16677,5580],{"class":5541},[3603,16679,12751],{"class":5576},[3603,16681,3453],{"class":5541},[3603,16683,12756],{"class":5576},[3603,16685,16687],{"class":3605,"line":16686},81,[3603,16688,16205],{"class":5541},[3603,16690,16692],{"class":3605,"line":16691},82,[3603,16693,3664],{"class":5541},[3603,16695,16697],{"class":3605,"line":16696},83,[3603,16698,3628],{"emptyLinePlaceholder":3627},[3603,16700,16702],{"class":3605,"line":16701},84,[3603,16703,16704],{"class":5214},"    \u002F\u002F Presigned URL для прямого завантаження з браузера (PUT)\n",[3603,16706,16708,16710,16712,16715,16717,16719,16721,16723,16725,16727],{"class":3605,"line":16707},85,[3603,16709,11012],{"class":5243},[3603,16711,10994],{"class":5243},[3603,16713,16714],{"class":5220}," GetPresignedUploadUrl",[3603,16716,5707],{"class":5541},[3603,16718,7598],{"class":5243},[3603,16720,11071],{"class":5576},[3603,16722,3470],{"class":5541},[3603,16724,7598],{"class":5243},[3603,16726,12821],{"class":5576},[3603,16728,5604],{"class":5541},[3603,16730,16732,16735,16737,16739,16742],{"class":3605,"line":16731},86,[3603,16733,16734],{"class":5243},"        int",[3603,16736,12667],{"class":5576},[3603,16738,5580],{"class":5541},[3603,16740,16741],{"class":5943},"10",[3603,16743,6924],{"class":5541},[3603,16745,16747],{"class":3605,"line":16746},87,[3603,16748,11086],{"class":5541},[3603,16750,16752,16754,16756,16758,16760,16762,16764],{"class":3605,"line":16751},88,[3603,16753,11308],{"class":5533},[3603,16755,10977],{"class":5576},[3603,16757,3453],{"class":5541},[3603,16759,12771],{"class":5220},[3603,16761,5707],{"class":5541},[3603,16763,5583],{"class":5243},[3603,16765,12691],{"class":5537},[3603,16767,16769],{"class":3605,"line":16768},89,[3603,16770,5873],{"class":5541},[3603,16772,16774,16776,16778,16780],{"class":3605,"line":16773},90,[3603,16775,11109],{"class":5576},[3603,16777,5580],{"class":5541},[3603,16779,16084],{"class":5576},[3603,16781,5604],{"class":5541},[3603,16783,16785,16787,16789,16791],{"class":3605,"line":16784},91,[3603,16786,11121],{"class":5576},[3603,16788,5580],{"class":5541},[3603,16790,11126],{"class":5576},[3603,16792,5604],{"class":5541},[3603,16794,16796,16798,16800,16802,16804,16806,16808,16810,16812,16814],{"class":3605,"line":16795},92,[3603,16797,12720],{"class":5576},[3603,16799,5580],{"class":5541},[3603,16801,5621],{"class":5576},[3603,16803,3453],{"class":5541},[3603,16805,5626],{"class":5576},[3603,16807,3453],{"class":5541},[3603,16809,12733],{"class":5220},[3603,16811,5707],{"class":5541},[3603,16813,12738],{"class":5576},[3603,16815,12741],{"class":5541},[3603,16817,16819,16821,16823,16825,16827,16829],{"class":3605,"line":16818},93,[3603,16820,12746],{"class":5576},[3603,16822,5580],{"class":5541},[3603,16824,12751],{"class":5576},[3603,16826,3453],{"class":5541},[3603,16828,6134],{"class":5576},[3603,16830,5604],{"class":5541},[3603,16832,16834,16836,16838],{"class":3605,"line":16833},94,[3603,16835,12915],{"class":5576},[3603,16837,5580],{"class":5541},[3603,16839,12920],{"class":5576},[3603,16841,16843],{"class":3605,"line":16842},95,[3603,16844,16205],{"class":5541},[3603,16846,16848],{"class":3605,"line":16847},96,[3603,16849,3664],{"class":5541},[3603,16851,16853],{"class":3605,"line":16852},97,[3603,16854,3628],{"emptyLinePlaceholder":3627},[3603,16856,16858],{"class":3605,"line":16857},98,[3603,16859,16860],{"class":5214},"    \u002F\u002F Multipart upload для великих файлів (> 100 MB)\n",[3603,16862,16864],{"class":3605,"line":16863},99,[3603,16865,16866],{"class":5214},"    \u002F\u002F TransferUtility автоматично розбиває файл на PartSize-частини\n",[3603,16868,16870],{"class":3605,"line":16869},100,[3603,16871,16872],{"class":5214},"    \u002F\u002F і завантажує їх паралельно (ThreadsCount потоків).\n",[3603,16874,16876],{"class":3605,"line":16875},101,[3603,16877,16878],{"class":5214},"    \u002F\u002F Якщо завантаження перервалось — можна відновити з місця зупинки\n",[3603,16880,16882],{"class":3605,"line":16881},102,[3603,16883,16884],{"class":5214},"    \u002F\u002F через InitiateMultipartUploadRequest \u002F UploadPartRequest вручну.\n",[3603,16886,16888],{"class":3605,"line":16887},103,[3603,16889,16890],{"class":5214},"    \u002F\u002F 500 MB при ThreadsCount=5 завантажуються приблизно в 5x швидше ніж послідовно.\n",[3603,16892,16894,16896,16898,16900,16903,16905,16907,16910,16912,16914,16916],{"class":3605,"line":16893},104,[3603,16895,11012],{"class":5243},[3603,16897,11051],{"class":5243},[3603,16899,11054],{"class":5537},[3603,16901,16902],{"class":5220}," UploadLargeFileAsync",[3603,16904,5707],{"class":5541},[3603,16906,7598],{"class":5243},[3603,16908,16909],{"class":5576}," filePath",[3603,16911,3470],{"class":5541},[3603,16913,7598],{"class":5243},[3603,16915,11071],{"class":5576},[3603,16917,6924],{"class":5541},[3603,16919,16921],{"class":3605,"line":16920},105,[3603,16922,11086],{"class":5541},[3603,16924,16926,16928,16930,16932,16934,16936,16938,16940,16942],{"class":3605,"line":16925},106,[3603,16927,16030],{"class":5533},[3603,16929,16033],{"class":5243},[3603,16931,16036],{"class":5576},[3603,16933,5580],{"class":5541},[3603,16935,5583],{"class":5243},[3603,16937,16043],{"class":5537},[3603,16939,5707],{"class":5541},[3603,16941,11028],{"class":5576},[3603,16943,5713],{"class":5541},[3603,16945,16947,16949,16951,16953,16955,16957,16959],{"class":3605,"line":16946},107,[3603,16948,16058],{"class":5243},[3603,16950,16036],{"class":5576},[3603,16952,3453],{"class":5541},[3603,16954,16000],{"class":5220},[3603,16956,5707],{"class":5541},[3603,16958,5583],{"class":5243},[3603,16960,16071],{"class":5537},[3603,16962,16964],{"class":3605,"line":16963},108,[3603,16965,5873],{"class":5541},[3603,16967,16969,16971,16973,16975],{"class":3605,"line":16968},109,[3603,16970,11109],{"class":5576},[3603,16972,5580],{"class":5541},[3603,16974,16084],{"class":5576},[3603,16976,5604],{"class":5541},[3603,16978,16980,16982,16984,16986],{"class":3605,"line":16979},110,[3603,16981,11121],{"class":5576},[3603,16983,5580],{"class":5541},[3603,16985,11126],{"class":5576},[3603,16987,5604],{"class":5541},[3603,16989,16991,16994,16996,16999],{"class":3605,"line":16990},111,[3603,16992,16993],{"class":5576},"            FilePath",[3603,16995,5580],{"class":5541},[3603,16997,16998],{"class":5576},"filePath",[3603,17000,5604],{"class":5541},[3603,17002,17004],{"class":3605,"line":17003},112,[3603,17005,17006],{"class":5214},"            \u002F\u002F Multipart: частини по 50 MB, до 5 паралельних потоків\n",[3603,17008,17010,17013,17015,17018,17021,17024,17026,17028],{"class":3605,"line":17009},113,[3603,17011,17012],{"class":5576},"            PartSize",[3603,17014,5580],{"class":5541},[3603,17016,17017],{"class":5943},"50",[3603,17019,17020],{"class":5541}," * ",[3603,17022,17023],{"class":5943},"1024",[3603,17025,17020],{"class":5541},[3603,17027,17023],{"class":5943},[3603,17029,5604],{"class":5541},[3603,17031,17033,17036,17038],{"class":3605,"line":17032},114,[3603,17034,17035],{"class":5576},"            ThreadsCount",[3603,17037,5580],{"class":5541},[3603,17039,7548],{"class":5943},[3603,17041,17043],{"class":3605,"line":17042},115,[3603,17044,16205],{"class":5541},[3603,17046,17048],{"class":3605,"line":17047},116,[3603,17049,3664],{"class":5541},[3603,17051,17053],{"class":3605,"line":17052},117,[3603,17054,3628],{"emptyLinePlaceholder":3627},[3603,17056,17058],{"class":3605,"line":17057},118,[3603,17059,17060],{"class":5214},"    \u002F\u002F Копіювання об'єкту всередині S3 (без завантаження на клієнт)\n",[3603,17062,17064],{"class":3605,"line":17063},119,[3603,17065,17066],{"class":5214},"    \u002F\u002F CopyObjectRequest — S3 копіює файл на своєму боці, трафік не йде через ваш сервер.\n",[3603,17068,17070],{"class":3605,"line":17069},120,[3603,17071,17072],{"class":5214},"    \u002F\u002F Використовується для: перейменування (copy + delete), зміни storage class,\n",[3603,17074,17076],{"class":3605,"line":17075},121,[3603,17077,17078],{"class":5214},"    \u002F\u002F переміщення між bucket'ами, або дублювання файлу при збереженні версії.\n",[3603,17080,17082,17084,17086,17088,17091,17093,17095,17098,17100,17102,17105],{"class":3605,"line":17081},122,[3603,17083,11012],{"class":5243},[3603,17085,11051],{"class":5243},[3603,17087,11054],{"class":5537},[3603,17089,17090],{"class":5220}," CopyAsync",[3603,17092,5707],{"class":5541},[3603,17094,7598],{"class":5243},[3603,17096,17097],{"class":5576}," sourceKey",[3603,17099,3470],{"class":5541},[3603,17101,7598],{"class":5243},[3603,17103,17104],{"class":5576}," destinationKey",[3603,17106,6924],{"class":5541},[3603,17108,17110],{"class":3605,"line":17109},123,[3603,17111,11086],{"class":5541},[3603,17113,17115,17117,17119,17121,17123,17125,17127],{"class":3605,"line":17114},124,[3603,17116,16058],{"class":5243},[3603,17118,10977],{"class":5576},[3603,17120,3453],{"class":5541},[3603,17122,5838],{"class":5220},[3603,17124,5707],{"class":5541},[3603,17126,5583],{"class":5243},[3603,17128,5736],{"class":5537},[3603,17130,17132],{"class":3605,"line":17131},125,[3603,17133,5873],{"class":5541},[3603,17135,17137,17140,17142,17144],{"class":3605,"line":17136},126,[3603,17138,17139],{"class":5576},"            SourceBucket",[3603,17141,5580],{"class":5541},[3603,17143,16084],{"class":5576},[3603,17145,5604],{"class":5541},[3603,17147,17149,17152,17154,17157],{"class":3605,"line":17148},127,[3603,17150,17151],{"class":5576},"            SourceKey",[3603,17153,5580],{"class":5541},[3603,17155,17156],{"class":5576},"sourceKey",[3603,17158,5604],{"class":5541},[3603,17160,17162,17165,17167,17169],{"class":3605,"line":17161},128,[3603,17163,17164],{"class":5576},"            DestinationBucket",[3603,17166,5580],{"class":5541},[3603,17168,16084],{"class":5576},[3603,17170,5604],{"class":5541},[3603,17172,17174,17177,17179],{"class":3605,"line":17173},129,[3603,17175,17176],{"class":5576},"            DestinationKey",[3603,17178,5580],{"class":5541},[3603,17180,17181],{"class":5576},"destinationKey\n",[3603,17183,17185],{"class":3605,"line":17184},130,[3603,17186,16205],{"class":5541},[3603,17188,17190],{"class":3605,"line":17189},131,[3603,17191,3664],{"class":5541},[3603,17193,17195],{"class":3605,"line":17194},132,[3603,17196,3670],{"class":5541},[3353,17198,17199],{},[3356,17200,17201],{},"API Controller з прикладом використання:",[3353,17203,17204,17205,17207,17208,17211],{},"Контролер відповідає тільки за HTTP-шар: валідація вхідних даних, авторизація, формування відповіді. Всю роботу з S3 делегує ",[3432,17206,15734],{},". Зверніть увагу на підхід з ",[3356,17209,17210],{},"Presigned PUT URL"," — файл від користувача не проходить через ASP.NET: бекенд лише генерує дозвіл, а браузер завантажує файл напряму в S3. Це знімає навантаження з сервера і прибирає ліміт розміру файлу на рівні ASP.NET.",[3561,17213,17215],{"className":5519,"code":17214,"language":5521,"meta":3569,"style":3569},"[ApiController]\n[Route(\"api\u002F[controller]\")]\npublic class FilesController : ControllerBase\n{\n    private readonly S3StorageService _storage;\n\n    public FilesController(S3StorageService storage) => _storage = storage;\n\n    \u002F\u002F POST \u002Fapi\u002Ffiles\u002Fupload\n    [HttpPost(\"upload\")]\n    public async Task\u003CIActionResult> Upload(IFormFile file)\n    {\n        if (file.Length == 0) return BadRequest(\"File is empty\");\n\n        \u002F\u002F Генеруємо унікальний ключ щоб уникнути колізій\n        var key = $\"uploads\u002F{Guid.NewGuid()}\u002F{file.FileName}\";\n\n        using var stream = file.OpenReadStream();\n        var url = await _storage.UploadAsync(stream, key, file.ContentType);\n\n        return Ok(new { url, key });\n    }\n\n    \u002F\u002F GET \u002Fapi\u002Ffiles\u002Fdownload-url?key=uploads\u002Fabc123\u002Fphoto.jpg\n    [HttpGet(\"download-url\")]\n    public IActionResult GetDownloadUrl([FromQuery] string key)\n    {\n        var url = _storage.GetPresignedDownloadUrl(key, expiresInMinutes: 30);\n        return Ok(new { url, expiresIn = \"30 minutes\" });\n    }\n\n    \u002F\u002F GET \u002Fapi\u002Ffiles\u002Fupload-url?filename=photo.jpg&contentType=image\u002Fjpeg\n    [HttpGet(\"upload-url\")]\n    public IActionResult GetUploadUrl([FromQuery] string filename,\n        [FromQuery] string contentType)\n    {\n        \u002F\u002F Ключ з userId щоб ізолювати файли користувачів\n        var userId = User.FindFirst(\"sub\")?.Value ?? \"anonymous\";\n        var key = $\"users\u002F{userId}\u002F{Guid.NewGuid()}_{filename}\";\n\n        var url = _storage.GetPresignedUploadUrl(key, contentType);\n        return Ok(new { url, key });\n    }\n}\n",[3432,17216,17217,17226,17241,17256,17260,17273,17277,17302,17306,17311,17326,17353,17357,17392,17396,17401,17442,17446,17466,17503,17507,17530,17534,17538,17543,17557,17582,17586,17615,17641,17645,17649,17654,17667,17689,17704,17708,17713,17749,17792,17796,17821,17841,17845],{"__ignoreMap":3569},[3603,17218,17219,17221,17224],{"class":3605,"line":3606},[3603,17220,15943],{"class":5541},[3603,17222,17223],{"class":5537},"ApiController",[3603,17225,13913],{"class":5541},[3603,17227,17228,17230,17233,17235,17238],{"class":3605,"line":3612},[3603,17229,15943],{"class":5541},[3603,17231,17232],{"class":5537},"Route",[3603,17234,5707],{"class":5541},[3603,17236,17237],{"class":5224},"\"api\u002F[controller]\"",[3603,17239,17240],{"class":5541},")]\n",[3603,17242,17243,17245,17247,17250,17253],{"class":3605,"line":3618},[3603,17244,10953],{"class":5243},[3603,17246,10956],{"class":5243},[3603,17248,17249],{"class":5537}," FilesController",[3603,17251,17252],{"class":5541}," : ",[3603,17254,17255],{"class":5537},"ControllerBase\n",[3603,17257,17258],{"class":3605,"line":3624},[3603,17259,5591],{"class":5541},[3603,17261,17262,17264,17266,17268,17271],{"class":3605,"line":3631},[3603,17263,10968],{"class":5243},[3603,17265,10971],{"class":5243},[3603,17267,15896],{"class":5537},[3603,17269,17270],{"class":5576}," _storage",[3603,17272,5547],{"class":5541},[3603,17274,17275],{"class":3605,"line":3637},[3603,17276,3628],{"emptyLinePlaceholder":3627},[3603,17278,17279,17281,17283,17285,17287,17290,17292,17295,17297,17300],{"class":3605,"line":3643},[3603,17280,11012],{"class":5243},[3603,17282,17249],{"class":5220},[3603,17284,5707],{"class":5541},[3603,17286,15734],{"class":5537},[3603,17288,17289],{"class":5576}," storage",[3603,17291,11025],{"class":5541},[3603,17293,17294],{"class":5576},"_storage",[3603,17296,5580],{"class":5541},[3603,17298,17299],{"class":5576},"storage",[3603,17301,5547],{"class":5541},[3603,17303,17304],{"class":3605,"line":3649},[3603,17305,3628],{"emptyLinePlaceholder":3627},[3603,17307,17308],{"class":3605,"line":3655},[3603,17309,17310],{"class":5214},"    \u002F\u002F POST \u002Fapi\u002Ffiles\u002Fupload\n",[3603,17312,17313,17316,17319,17321,17324],{"class":3605,"line":3661},[3603,17314,17315],{"class":5541},"    [",[3603,17317,17318],{"class":5537},"HttpPost",[3603,17320,5707],{"class":5541},[3603,17322,17323],{"class":5224},"\"upload\"",[3603,17325,17240],{"class":5541},[3603,17327,17328,17330,17332,17334,17336,17339,17341,17344,17346,17349,17351],{"class":3605,"line":3667},[3603,17329,11012],{"class":5243},[3603,17331,11051],{"class":5243},[3603,17333,11054],{"class":5537},[3603,17335,11250],{"class":5541},[3603,17337,17338],{"class":5537},"IActionResult",[3603,17340,11255],{"class":5541},[3603,17342,17343],{"class":5220},"Upload",[3603,17345,5707],{"class":5541},[3603,17347,17348],{"class":5537},"IFormFile",[3603,17350,13032],{"class":5576},[3603,17352,6924],{"class":5541},[3603,17354,17355],{"class":3605,"line":3673},[3603,17356,11086],{"class":5541},[3603,17358,17359,17361,17363,17366,17368,17371,17374,17377,17379,17382,17385,17387,17390],{"class":3605,"line":3678},[3603,17360,15090],{"class":5533},[3603,17362,10425],{"class":5541},[3603,17364,17365],{"class":5576},"file",[3603,17367,3453],{"class":5541},[3603,17369,17370],{"class":5576},"Length",[3603,17372,17373],{"class":5541}," == ",[3603,17375,17376],{"class":5943},"0",[3603,17378,15099],{"class":5541},[3603,17380,17381],{"class":5533},"return",[3603,17383,17384],{"class":5220}," BadRequest",[3603,17386,5707],{"class":5541},[3603,17388,17389],{"class":5224},"\"File is empty\"",[3603,17391,5713],{"class":5541},[3603,17393,17394],{"class":3605,"line":3684},[3603,17395,3628],{"emptyLinePlaceholder":3627},[3603,17397,17398],{"class":3605,"line":3690},[3603,17399,17400],{"class":5214},"        \u002F\u002F Генеруємо унікальний ключ щоб уникнути колізій\n",[3603,17402,17403,17405,17407,17409,17412,17414,17417,17419,17422,17425,17427,17429,17431,17433,17436,17438,17440],{"class":3605,"line":3696},[3603,17404,11091],{"class":5243},[3603,17406,11071],{"class":5576},[3603,17408,5580],{"class":5541},[3603,17410,17411],{"class":5224},"$\"uploads\u002F",[3603,17413,5618],{"class":5617},[3603,17415,17416],{"class":5576},"Guid",[3603,17418,3453],{"class":5617},[3603,17420,17421],{"class":5220},"NewGuid",[3603,17423,17424],{"class":5617},"()}",[3603,17426,3575],{"class":5224},[3603,17428,5618],{"class":5617},[3603,17430,17365],{"class":5576},[3603,17432,3453],{"class":5617},[3603,17434,17435],{"class":5576},"FileName",[3603,17437,5645],{"class":5617},[3603,17439,6554],{"class":5224},[3603,17441,5547],{"class":5541},[3603,17443,17444],{"class":3605,"line":3702},[3603,17445,3628],{"emptyLinePlaceholder":3627},[3603,17447,17448,17450,17452,17455,17457,17459,17461,17464],{"class":3605,"line":3708},[3603,17449,16030],{"class":5533},[3603,17451,16033],{"class":5243},[3603,17453,17454],{"class":5576}," stream",[3603,17456,5580],{"class":5541},[3603,17458,17365],{"class":5576},[3603,17460,3453],{"class":5541},[3603,17462,17463],{"class":5220},"OpenReadStream",[3603,17465,16490],{"class":5541},[3603,17467,17468,17470,17473,17475,17477,17479,17481,17483,17485,17488,17490,17492,17494,17496,17498,17501],{"class":3605,"line":4667},[3603,17469,11091],{"class":5243},[3603,17471,17472],{"class":5576}," url",[3603,17474,5580],{"class":5541},[3603,17476,5696],{"class":5243},[3603,17478,17270],{"class":5576},[3603,17480,3453],{"class":5541},[3603,17482,16000],{"class":5220},[3603,17484,5707],{"class":5541},[3603,17486,17487],{"class":5576},"stream",[3603,17489,3470],{"class":5541},[3603,17491,11126],{"class":5576},[3603,17493,3470],{"class":5541},[3603,17495,17365],{"class":5576},[3603,17497,3453],{"class":5541},[3603,17499,17500],{"class":5576},"ContentType",[3603,17502,5713],{"class":5541},[3603,17504,17505],{"class":3605,"line":4673},[3603,17506,3628],{"emptyLinePlaceholder":3627},[3603,17508,17509,17511,17514,17516,17518,17520,17523,17525,17527],{"class":3605,"line":4679},[3603,17510,11308],{"class":5533},[3603,17512,17513],{"class":5220}," Ok",[3603,17515,5707],{"class":5541},[3603,17517,5583],{"class":5243},[3603,17519,12973],{"class":5541},[3603,17521,17522],{"class":5576},"url",[3603,17524,3470],{"class":5541},[3603,17526,11126],{"class":5576},[3603,17528,17529],{"class":5541}," });\n",[3603,17531,17532],{"class":3605,"line":4685},[3603,17533,3664],{"class":5541},[3603,17535,17536],{"class":3605,"line":4691},[3603,17537,3628],{"emptyLinePlaceholder":3627},[3603,17539,17540],{"class":3605,"line":4696},[3603,17541,17542],{"class":5214},"    \u002F\u002F GET \u002Fapi\u002Ffiles\u002Fdownload-url?key=uploads\u002Fabc123\u002Fphoto.jpg\n",[3603,17544,17545,17547,17550,17552,17555],{"class":3605,"line":4701},[3603,17546,17315],{"class":5541},[3603,17548,17549],{"class":5537},"HttpGet",[3603,17551,5707],{"class":5541},[3603,17553,17554],{"class":5224},"\"download-url\"",[3603,17556,17240],{"class":5541},[3603,17558,17559,17561,17564,17567,17570,17573,17576,17578,17580],{"class":3605,"line":4707},[3603,17560,11012],{"class":5243},[3603,17562,17563],{"class":5537}," IActionResult",[3603,17565,17566],{"class":5220}," GetDownloadUrl",[3603,17568,17569],{"class":5541},"([",[3603,17571,17572],{"class":5537},"FromQuery",[3603,17574,17575],{"class":5541},"] ",[3603,17577,7598],{"class":5243},[3603,17579,11071],{"class":5576},[3603,17581,6924],{"class":5541},[3603,17583,17584],{"class":3605,"line":4713},[3603,17585,11086],{"class":5541},[3603,17587,17588,17590,17592,17594,17596,17598,17601,17603,17605,17607,17609,17611,17613],{"class":3605,"line":4719},[3603,17589,11091],{"class":5243},[3603,17591,17472],{"class":5576},[3603,17593,5580],{"class":5541},[3603,17595,17294],{"class":5576},[3603,17597,3453],{"class":5541},[3603,17599,17600],{"class":5220},"GetPresignedDownloadUrl",[3603,17602,5707],{"class":5541},[3603,17604,11126],{"class":5576},[3603,17606,3470],{"class":5541},[3603,17608,12738],{"class":5576},[3603,17610,5881],{"class":5541},[3603,17612,5944],{"class":5943},[3603,17614,5713],{"class":5541},[3603,17616,17617,17619,17621,17623,17625,17627,17629,17631,17634,17636,17639],{"class":3605,"line":4724},[3603,17618,11308],{"class":5533},[3603,17620,17513],{"class":5220},[3603,17622,5707],{"class":5541},[3603,17624,5583],{"class":5243},[3603,17626,12973],{"class":5541},[3603,17628,17522],{"class":5576},[3603,17630,3470],{"class":5541},[3603,17632,17633],{"class":5576},"expiresIn",[3603,17635,5580],{"class":5541},[3603,17637,17638],{"class":5224},"\"30 minutes\"",[3603,17640,17529],{"class":5541},[3603,17642,17643],{"class":3605,"line":4730},[3603,17644,3664],{"class":5541},[3603,17646,17647],{"class":3605,"line":4736},[3603,17648,3628],{"emptyLinePlaceholder":3627},[3603,17650,17651],{"class":3605,"line":4742},[3603,17652,17653],{"class":5214},"    \u002F\u002F GET \u002Fapi\u002Ffiles\u002Fupload-url?filename=photo.jpg&contentType=image\u002Fjpeg\n",[3603,17655,17656,17658,17660,17662,17665],{"class":3605,"line":4748},[3603,17657,17315],{"class":5541},[3603,17659,17549],{"class":5537},[3603,17661,5707],{"class":5541},[3603,17663,17664],{"class":5224},"\"upload-url\"",[3603,17666,17240],{"class":5541},[3603,17668,17669,17671,17673,17676,17678,17680,17682,17684,17687],{"class":3605,"line":4753},[3603,17670,11012],{"class":5243},[3603,17672,17563],{"class":5537},[3603,17674,17675],{"class":5220}," GetUploadUrl",[3603,17677,17569],{"class":5541},[3603,17679,17572],{"class":5537},[3603,17681,17575],{"class":5541},[3603,17683,7598],{"class":5243},[3603,17685,17686],{"class":5576}," filename",[3603,17688,5604],{"class":5541},[3603,17690,17691,17694,17696,17698,17700,17702],{"class":3605,"line":4758},[3603,17692,17693],{"class":5541},"        [",[3603,17695,17572],{"class":5537},[3603,17697,17575],{"class":5541},[3603,17699,7598],{"class":5243},[3603,17701,12821],{"class":5576},[3603,17703,6924],{"class":5541},[3603,17705,17706],{"class":3605,"line":6363},[3603,17707,11086],{"class":5541},[3603,17709,17710],{"class":3605,"line":6368},[3603,17711,17712],{"class":5214},"        \u002F\u002F Ключ з userId щоб ізолювати файли користувачів\n",[3603,17714,17715,17717,17720,17722,17725,17727,17730,17732,17735,17738,17741,17744,17747],{"class":3605,"line":6830},[3603,17716,11091],{"class":5243},[3603,17718,17719],{"class":5576}," userId",[3603,17721,5580],{"class":5541},[3603,17723,17724],{"class":5576},"User",[3603,17726,3453],{"class":5541},[3603,17728,17729],{"class":5220},"FindFirst",[3603,17731,5707],{"class":5541},[3603,17733,17734],{"class":5224},"\"sub\"",[3603,17736,17737],{"class":5541},")?.",[3603,17739,17740],{"class":5576},"Value",[3603,17742,17743],{"class":5541}," ?? ",[3603,17745,17746],{"class":5224},"\"anonymous\"",[3603,17748,5547],{"class":5541},[3603,17750,17751,17753,17755,17757,17760,17762,17765,17767,17769,17771,17773,17775,17777,17779,17781,17783,17786,17788,17790],{"class":3605,"line":6835},[3603,17752,11091],{"class":5243},[3603,17754,11071],{"class":5576},[3603,17756,5580],{"class":5541},[3603,17758,17759],{"class":5224},"$\"users\u002F",[3603,17761,5618],{"class":5617},[3603,17763,17764],{"class":5576},"userId",[3603,17766,5645],{"class":5617},[3603,17768,3575],{"class":5224},[3603,17770,5618],{"class":5617},[3603,17772,17416],{"class":5576},[3603,17774,3453],{"class":5617},[3603,17776,17421],{"class":5220},[3603,17778,17424],{"class":5617},[3603,17780,3483],{"class":5224},[3603,17782,5618],{"class":5617},[3603,17784,17785],{"class":5576},"filename",[3603,17787,5645],{"class":5617},[3603,17789,6554],{"class":5224},[3603,17791,5547],{"class":5541},[3603,17793,17794],{"class":3605,"line":6841},[3603,17795,3628],{"emptyLinePlaceholder":3627},[3603,17797,17798,17800,17802,17804,17806,17808,17811,17813,17815,17817,17819],{"class":3605,"line":6858},[3603,17799,11091],{"class":5243},[3603,17801,17472],{"class":5576},[3603,17803,5580],{"class":5541},[3603,17805,17294],{"class":5576},[3603,17807,3453],{"class":5541},[3603,17809,17810],{"class":5220},"GetPresignedUploadUrl",[3603,17812,5707],{"class":5541},[3603,17814,11126],{"class":5576},[3603,17816,3470],{"class":5541},[3603,17818,16116],{"class":5576},[3603,17820,5713],{"class":5541},[3603,17822,17823,17825,17827,17829,17831,17833,17835,17837,17839],{"class":3605,"line":6871},[3603,17824,11308],{"class":5533},[3603,17826,17513],{"class":5220},[3603,17828,5707],{"class":5541},[3603,17830,5583],{"class":5243},[3603,17832,12973],{"class":5541},[3603,17834,17522],{"class":5576},[3603,17836,3470],{"class":5541},[3603,17838,11126],{"class":5576},[3603,17840,17529],{"class":5541},[3603,17842,17843],{"class":3605,"line":6881},[3603,17844,3664],{"class":5541},[3603,17846,17847],{"class":3605,"line":6894},[3603,17848,3670],{"class":5541},[3405,17850],{},[3348,17852,17854],{"id":17853},"практичний-приклад-react-spa-на-s3-від-а-до-я","Практичний приклад: React SPA на S3 від А до Я",[3353,17856,17857],{},"Побудуємо повноцінний React SPA з клієнтським роутингом та задеплоємо на S3 Static Website Hosting. Кроки йдуть у логічному порядку: спочатку створюємо застосунок, перевіряємо локально — потім налаштовуємо AWS-інфраструктуру та деплоємо.",[3405,17859],{},[3412,17861,17863],{"id":17862},"крок-1-створення-react-застосунку","Крок 1: Створення React застосунку",[3561,17865,17867],{"className":5205,"code":17866,"language":5207,"meta":3569,"style":3569},"# Vite — сучасний збирач, що замінив webpack\u002FCRA: холодний старт ~200мс, HMR миттєвий\n# --template react — JSX шаблон (є також react-ts для TypeScript)\nnpm create vite@latest my-react-app -- --template react\ncd my-react-app\nnpm install\n\n# react-router v7 — пакет тепер називається просто \"react-router\" (не react-router-dom)\nnpm install react-router\n",[3432,17868,17869,17874,17879,17901,17909,17916,17920,17925],{"__ignoreMap":3569},[3603,17870,17871],{"class":3605,"line":3606},[3603,17872,17873],{"class":5214},"# Vite — сучасний збирач, що замінив webpack\u002FCRA: холодний старт ~200мс, HMR миттєвий\n",[3603,17875,17876],{"class":3605,"line":3612},[3603,17877,17878],{"class":5214},"# --template react — JSX шаблон (є також react-ts для TypeScript)\n",[3603,17880,17881,17884,17887,17890,17892,17895,17898],{"class":3605,"line":3618},[3603,17882,17883],{"class":5220},"npm",[3603,17885,17886],{"class":5224}," create",[3603,17888,17889],{"class":5224}," vite@latest",[3603,17891,13394],{"class":5224},[3603,17893,17894],{"class":5243}," --",[3603,17896,17897],{"class":5243}," --template",[3603,17899,17900],{"class":5224}," react\n",[3603,17902,17903,17906],{"class":3605,"line":3624},[3603,17904,17905],{"class":5220},"cd",[3603,17907,17908],{"class":5224}," my-react-app\n",[3603,17910,17911,17913],{"class":3605,"line":3631},[3603,17912,17883],{"class":5220},[3603,17914,17915],{"class":5224}," install\n",[3603,17917,17918],{"class":3605,"line":3637},[3603,17919,3628],{"emptyLinePlaceholder":3627},[3603,17921,17922],{"class":3605,"line":3643},[3603,17923,17924],{"class":5214},"# react-router v7 — пакет тепер називається просто \"react-router\" (не react-router-dom)\n",[3603,17926,17927,17929,17932],{"class":3605,"line":3649},[3603,17928,17883],{"class":5220},[3603,17930,17931],{"class":5224}," install",[3603,17933,17934],{"class":5224}," react-router\n",[4801,17936,17938,17946,17949,17953,17956,17960,17967,17974],{"title":17937},"npm create vite@latest my-react-app -- --template react",[4805,17939,17941,4841,17944],{"className":17940},[3605],[3603,17942,7030],{"className":17943},[7029],[3356,17945,17937],{},[4805,17947],{"className":17948},[3605],[4805,17950,17952],{"className":17951},[3605],"Scaffolding project in \u002Fhome\u002Fuser\u002Fmy-react-app...",[4805,17954],{"className":17955},[3605],[4805,17957,17959],{"className":17958},[3605],"Done. Now run:",[4805,17961,7040,17963],{"className":17962},[3605],[3603,17964,17966],{"className":17965},[7085],"cd my-react-app",[4805,17968,7040,17970],{"className":17969},[3605],[3603,17971,17973],{"className":17972},[7085],"npm install",[4805,17975,7040,17977],{"className":17976},[3605],[3603,17978,17980],{"className":17979},[7085],"npm run dev",[3353,17982,17983],{},"Структура файлів після генерації та наших змін:",[3561,17985,17988],{"className":17986,"code":17987,"language":3566},[3564],"my-react-app\u002F\n├── index.html          ← точка входу Vite (у корені, не в public\u002F!)\n├── public\u002F             ← статичні ресурси (favicon тощо)\n├── src\u002F\n│   ├── main.jsx        ← монтує React у DOM\n│   ├── App.jsx         ← layout-компонент: навбар + \u003COutlet \u002F>\n│   ├── routes.jsx      ← конфіг маршрутів (JS-об'єкти, не JSX)\n│   ├── index.css       ← глобальні стилі\n│   └── pages\u002F\n│       ├── Home.jsx    ← головна сторінка \u002F\n│       └── About.jsx   ← сторінка \u002Fabout\n├── vite.config.js\n└── package.json\n",[3432,17989,17987],{"__ignoreMap":3569},[3353,17991,17992,17993,17998],{},"Створюємо ",[3356,17994,17995],{},[3432,17996,17997],{},"src\u002Froutes.jsx"," — маршрути як JS-об'єкти (React Router v7 data API):",[3561,18000,18002],{"className":14969,"code":18001,"language":14971,"meta":3569,"style":3569},"\u002F\u002F src\u002Froutes.jsx\nimport { createBrowserRouter } from 'react-router'\nimport App from '.\u002FApp'\nimport Home from '.\u002Fpages\u002FHome'\nimport About from '.\u002Fpages\u002FAbout'\n\n\u002F\u002F createBrowserRouter — React Router v7 data API.\n\u002F\u002F Маршрути описуються масивом об'єктів, а не JSX-деревом \u003CRoutes>\u003CRoute>.\n\u002F\u002F Component: (з великої) — статичний імпорт компонента.\nfunction NotFound() {\n    return (\n        \u003Cdiv className=\"not-found\">\n            \u003Ch1>404\u003C\u002Fh1>\n            \u003Cp>Сторінку не знайдено\u003C\u002Fp>\n        \u003C\u002Fdiv>\n    )\n}\n\nexport const router = createBrowserRouter([\n    {\n        path: '\u002F',\n        Component: App, \u002F\u002F layout: рендерить навбар + \u003COutlet \u002F>\n        children: [\n            { index: true, Component: Home }, \u002F\u002F рендериться на \u002F\n            { path: 'about', Component: About }, \u002F\u002F рендериться на \u002Fabout\n        ],\n    },\n    { path: '*', Component: NotFound },\n])\n",[3432,18003,18004,18009,18025,18037,18049,18061,18065,18070,18075,18080,18090,18097,18115,18134,18151,18160,18165,18169,18173,18190,18194,18204,18216,18224,18248,18269,18274,18278,18296],{"__ignoreMap":3569},[3603,18005,18006],{"class":3605,"line":3606},[3603,18007,18008],{"class":5214},"\u002F\u002F src\u002Froutes.jsx\n",[3603,18010,18011,18013,18015,18018,18020,18022],{"class":3605,"line":3612},[3603,18012,14978],{"class":5533},[3603,18014,12973],{"class":5541},[3603,18016,18017],{"class":5576},"createBrowserRouter",[3603,18019,14991],{"class":5541},[3603,18021,14994],{"class":5533},[3603,18023,18024],{"class":5224}," 'react-router'\n",[3603,18026,18027,18029,18032,18034],{"class":3605,"line":3618},[3603,18028,14978],{"class":5533},[3603,18030,18031],{"class":5576}," App",[3603,18033,15007],{"class":5533},[3603,18035,18036],{"class":5224}," '.\u002FApp'\n",[3603,18038,18039,18041,18044,18046],{"class":3605,"line":3624},[3603,18040,14978],{"class":5533},[3603,18042,18043],{"class":5576}," Home",[3603,18045,15007],{"class":5533},[3603,18047,18048],{"class":5224}," '.\u002Fpages\u002FHome'\n",[3603,18050,18051,18053,18056,18058],{"class":3605,"line":3631},[3603,18052,14978],{"class":5533},[3603,18054,18055],{"class":5576}," About",[3603,18057,15007],{"class":5533},[3603,18059,18060],{"class":5224}," '.\u002Fpages\u002FAbout'\n",[3603,18062,18063],{"class":3605,"line":3637},[3603,18064,3628],{"emptyLinePlaceholder":3627},[3603,18066,18067],{"class":3605,"line":3643},[3603,18068,18069],{"class":5214},"\u002F\u002F createBrowserRouter — React Router v7 data API.\n",[3603,18071,18072],{"class":3605,"line":3649},[3603,18073,18074],{"class":5214},"\u002F\u002F Маршрути описуються масивом об'єктів, а не JSX-деревом \u003CRoutes>\u003CRoute>.\n",[3603,18076,18077],{"class":3605,"line":3655},[3603,18078,18079],{"class":5214},"\u002F\u002F Component: (з великої) — статичний імпорт компонента.\n",[3603,18081,18082,18084,18087],{"class":3605,"line":3661},[3603,18083,15019],{"class":5243},[3603,18085,18086],{"class":5220}," NotFound",[3603,18088,18089],{"class":5541},"() {\n",[3603,18091,18092,18094],{"class":3605,"line":3667},[3603,18093,14931],{"class":5533},[3603,18095,18096],{"class":5541}," (\n",[3603,18098,18099,18102,18104,18107,18109,18112],{"class":3605,"line":3673},[3603,18100,18101],{"class":15364},"        \u003C",[3603,18103,4805],{"class":15368},[3603,18105,18106],{"class":15371}," className",[3603,18108,6507],{"class":5541},[3603,18110,18111],{"class":5224},"\"not-found\"",[3603,18113,18114],{"class":15364},">\n",[3603,18116,18117,18120,18122,18125,18127,18130,18132],{"class":3605,"line":3678},[3603,18118,18119],{"class":15364},"            \u003C",[3603,18121,3344],{"class":15368},[3603,18123,18124],{"class":15364},">",[3603,18126,13167],{"class":5541},[3603,18128,18129],{"class":15364},"\u003C\u002F",[3603,18131,3344],{"class":15368},[3603,18133,18114],{"class":15364},[3603,18135,18136,18138,18140,18142,18145,18147,18149],{"class":3605,"line":3684},[3603,18137,18119],{"class":15364},[3603,18139,3353],{"class":15368},[3603,18141,18124],{"class":15364},[3603,18143,18144],{"class":5541},"Сторінку не знайдено",[3603,18146,18129],{"class":15364},[3603,18148,3353],{"class":15368},[3603,18150,18114],{"class":15364},[3603,18152,18153,18156,18158],{"class":3605,"line":3690},[3603,18154,18155],{"class":15364},"        \u003C\u002F",[3603,18157,4805],{"class":15368},[3603,18159,18114],{"class":15364},[3603,18161,18162],{"class":3605,"line":3696},[3603,18163,18164],{"class":5541},"    )\n",[3603,18166,18167],{"class":3605,"line":3702},[3603,18168,3670],{"class":5541},[3603,18170,18171],{"class":3605,"line":3708},[3603,18172,3628],{"emptyLinePlaceholder":3627},[3603,18174,18175,18178,18180,18183,18185,18187],{"class":3605,"line":4667},[3603,18176,18177],{"class":5533},"export",[3603,18179,10991],{"class":5243},[3603,18181,18182],{"class":12976}," router",[3603,18184,5580],{"class":5541},[3603,18186,18017],{"class":5220},[3603,18188,18189],{"class":5541},"([\n",[3603,18191,18192],{"class":3605,"line":4673},[3603,18193,11086],{"class":5541},[3603,18195,18196,18199,18202],{"class":3605,"line":4679},[3603,18197,18198],{"class":5576},"        path:",[3603,18200,18201],{"class":5224}," '\u002F'",[3603,18203,5604],{"class":5541},[3603,18205,18206,18209,18211,18213],{"class":3605,"line":4685},[3603,18207,18208],{"class":5576},"        Component:",[3603,18210,18031],{"class":5576},[3603,18212,3470],{"class":5541},[3603,18214,18215],{"class":5214},"\u002F\u002F layout: рендерить навбар + \u003COutlet \u002F>\n",[3603,18217,18218,18221],{"class":3605,"line":4691},[3603,18219,18220],{"class":5576},"        children:",[3603,18222,18223],{"class":5541}," [\n",[3603,18225,18226,18229,18232,18235,18237,18240,18242,18245],{"class":3605,"line":4696},[3603,18227,18228],{"class":5541},"            { ",[3603,18230,18231],{"class":5576},"index:",[3603,18233,18234],{"class":5243}," true",[3603,18236,3470],{"class":5541},[3603,18238,18239],{"class":5576},"Component:",[3603,18241,18043],{"class":5576},[3603,18243,18244],{"class":5541}," }, ",[3603,18246,18247],{"class":5214},"\u002F\u002F рендериться на \u002F\n",[3603,18249,18250,18252,18255,18258,18260,18262,18264,18266],{"class":3605,"line":4701},[3603,18251,18228],{"class":5541},[3603,18253,18254],{"class":5576},"path:",[3603,18256,18257],{"class":5224}," 'about'",[3603,18259,3470],{"class":5541},[3603,18261,18239],{"class":5576},[3603,18263,18055],{"class":5576},[3603,18265,18244],{"class":5541},[3603,18267,18268],{"class":5214},"\u002F\u002F рендериться на \u002Fabout\n",[3603,18270,18271],{"class":3605,"line":4707},[3603,18272,18273],{"class":5541},"        ],\n",[3603,18275,18276],{"class":3605,"line":4713},[3603,18277,15697],{"class":5541},[3603,18279,18280,18283,18285,18288,18290,18292,18294],{"class":3605,"line":4719},[3603,18281,18282],{"class":5541},"    { ",[3603,18284,18254],{"class":5576},[3603,18286,18287],{"class":5224}," '*'",[3603,18289,3470],{"class":5541},[3603,18291,18239],{"class":5576},[3603,18293,18086],{"class":5576},[3603,18295,7450],{"class":5541},[3603,18297,18298],{"class":3605,"line":4724},[3603,18299,15353],{"class":5541},[3353,18301,18302,18303,13119,18308,18311,18312,5629],{},"Замінюємо ",[3356,18304,18305],{},[3432,18306,18307],{},"src\u002Fmain.jsx",[3432,18309,18310],{},"RouterProvider"," замість ",[3432,18313,18314],{},"BrowserRouter",[3561,18316,18318],{"className":14969,"code":18317,"language":14971,"meta":3569,"style":3569},"\u002F\u002F src\u002Fmain.jsx\nimport { StrictMode } from 'react'\nimport { createRoot } from 'react-dom\u002Fclient'\nimport { RouterProvider } from 'react-router'\nimport { router } from '.\u002Froutes'\nimport '.\u002Findex.css'\n\n\u002F\u002F RouterProvider — єдина точка монтування.\n\u002F\u002F При прямому переході на \u002Fabout S3 повертає 404 —\n\u002F\u002F якщо не налаштований Error Document: index.html.\ncreateRoot(document.getElementById('root')).render(\n    \u003CStrictMode>\n        \u003CRouterProvider router={router} \u002F>\n    \u003C\u002FStrictMode>,\n)\n",[3432,18319,18320,18325,18340,18356,18370,18386,18393,18397,18402,18407,18412,18439,18448,18466,18477],{"__ignoreMap":3569},[3603,18321,18322],{"class":3605,"line":3606},[3603,18323,18324],{"class":5214},"\u002F\u002F src\u002Fmain.jsx\n",[3603,18326,18327,18329,18331,18334,18336,18338],{"class":3605,"line":3612},[3603,18328,14978],{"class":5533},[3603,18330,12973],{"class":5541},[3603,18332,18333],{"class":5576},"StrictMode",[3603,18335,14991],{"class":5541},[3603,18337,14994],{"class":5533},[3603,18339,14997],{"class":5224},[3603,18341,18342,18344,18346,18349,18351,18353],{"class":3605,"line":3618},[3603,18343,14978],{"class":5533},[3603,18345,12973],{"class":5541},[3603,18347,18348],{"class":5576},"createRoot",[3603,18350,14991],{"class":5541},[3603,18352,14994],{"class":5533},[3603,18354,18355],{"class":5224}," 'react-dom\u002Fclient'\n",[3603,18357,18358,18360,18362,18364,18366,18368],{"class":3605,"line":3624},[3603,18359,14978],{"class":5533},[3603,18361,12973],{"class":5541},[3603,18363,18310],{"class":5576},[3603,18365,14991],{"class":5541},[3603,18367,14994],{"class":5533},[3603,18369,18024],{"class":5224},[3603,18371,18372,18374,18376,18379,18381,18383],{"class":3605,"line":3631},[3603,18373,14978],{"class":5533},[3603,18375,12973],{"class":5541},[3603,18377,18378],{"class":5576},"router",[3603,18380,14991],{"class":5541},[3603,18382,14994],{"class":5533},[3603,18384,18385],{"class":5224}," '.\u002Froutes'\n",[3603,18387,18388,18390],{"class":3605,"line":3637},[3603,18389,14978],{"class":5533},[3603,18391,18392],{"class":5224}," '.\u002Findex.css'\n",[3603,18394,18395],{"class":3605,"line":3643},[3603,18396,3628],{"emptyLinePlaceholder":3627},[3603,18398,18399],{"class":3605,"line":3649},[3603,18400,18401],{"class":5214},"\u002F\u002F RouterProvider — єдина точка монтування.\n",[3603,18403,18404],{"class":3605,"line":3655},[3603,18405,18406],{"class":5214},"\u002F\u002F При прямому переході на \u002Fabout S3 повертає 404 —\n",[3603,18408,18409],{"class":3605,"line":3661},[3603,18410,18411],{"class":5214},"\u002F\u002F якщо не налаштований Error Document: index.html.\n",[3603,18413,18414,18416,18418,18421,18423,18426,18428,18431,18434,18437],{"class":3605,"line":3667},[3603,18415,18348],{"class":5220},[3603,18417,5707],{"class":5541},[3603,18419,18420],{"class":5576},"document",[3603,18422,3453],{"class":5541},[3603,18424,18425],{"class":5220},"getElementById",[3603,18427,5707],{"class":5541},[3603,18429,18430],{"class":5224},"'root'",[3603,18432,18433],{"class":5541},")).",[3603,18435,18436],{"class":5220},"render",[3603,18438,14825],{"class":5541},[3603,18440,18441,18444,18446],{"class":3605,"line":3673},[3603,18442,18443],{"class":15364},"    \u003C",[3603,18445,18333],{"class":5537},[3603,18447,18114],{"class":15364},[3603,18449,18450,18452,18454,18456,18458,18460,18462,18464],{"class":3605,"line":3678},[3603,18451,18101],{"class":15364},[3603,18453,18310],{"class":5537},[3603,18455,18182],{"class":15371},[3603,18457,6507],{"class":5541},[3603,18459,5618],{"class":5243},[3603,18461,18378],{"class":5576},[3603,18463,5645],{"class":5243},[3603,18465,15414],{"class":15364},[3603,18467,18468,18471,18473,18475],{"class":3605,"line":3684},[3603,18469,18470],{"class":15364},"    \u003C\u002F",[3603,18472,18333],{"class":5537},[3603,18474,18124],{"class":15364},[3603,18476,5604],{"class":5541},[3603,18478,18479],{"class":3605,"line":3690},[3603,18480,6924],{"class":5541},[3353,18482,18302,18483,18488,18489,5629],{},[3356,18484,18485],{},[3432,18486,18487],{},"src\u002FApp.jsx"," — layout-компонент з ",[3432,18490,18491],{},"\u003COutlet \u002F>",[3561,18493,18495],{"className":14969,"code":18494,"language":14971,"meta":3569,"style":3569},"\u002F\u002F src\u002FApp.jsx\nimport { Outlet, NavLink } from 'react-router'\n\n\u002F\u002F App — layout: рендерить навбар один раз,\n\u002F\u002F а \u003COutlet \u002F> підставляє потрібну сторінку залежно від URL\nexport default function App() {\n    return (\n        \u003Cdiv className=\"app\">\n            \u003Cnav className=\"navbar\">\n                \u003Cspan className=\"brand\">☁️ S3 Demo App\u003C\u002Fspan>\n                \u003Cdiv className=\"nav-links\">\n                    {\u002F* NavLink автоматично додає клас active до поточного посилання *\u002F}\n                    \u003CNavLink to=\"\u002F\" end>\n                        Home\n                    \u003C\u002FNavLink>\n                    \u003CNavLink to=\"\u002Fabout\">About\u003C\u002FNavLink>\n                \u003C\u002Fdiv>\n            \u003C\u002Fnav>\n            \u003Cmain className=\"content\">\n                \u003COutlet \u002F> {\u002F* тут рендериться Home або About залежно від URL *\u002F}\n            \u003C\u002Fmain>\n        \u003C\u002Fdiv>\n    )\n}\n",[3432,18496,18497,18502,18522,18526,18531,18536,18550,18556,18571,18587,18612,18627,18636,18656,18661,18670,18694,18703,18712,18728,18745,18753,18761,18765],{"__ignoreMap":3569},[3603,18498,18499],{"class":3605,"line":3606},[3603,18500,18501],{"class":5214},"\u002F\u002F src\u002FApp.jsx\n",[3603,18503,18504,18506,18508,18511,18513,18516,18518,18520],{"class":3605,"line":3612},[3603,18505,14978],{"class":5533},[3603,18507,12973],{"class":5541},[3603,18509,18510],{"class":5576},"Outlet",[3603,18512,3470],{"class":5541},[3603,18514,18515],{"class":5576},"NavLink",[3603,18517,14991],{"class":5541},[3603,18519,14994],{"class":5533},[3603,18521,18024],{"class":5224},[3603,18523,18524],{"class":3605,"line":3618},[3603,18525,3628],{"emptyLinePlaceholder":3627},[3603,18527,18528],{"class":3605,"line":3624},[3603,18529,18530],{"class":5214},"\u002F\u002F App — layout: рендерить навбар один раз,\n",[3603,18532,18533],{"class":3605,"line":3631},[3603,18534,18535],{"class":5214},"\u002F\u002F а \u003COutlet \u002F> підставляє потрібну сторінку залежно від URL\n",[3603,18537,18538,18540,18543,18546,18548],{"class":3605,"line":3637},[3603,18539,18177],{"class":5533},[3603,18541,18542],{"class":5533}," default",[3603,18544,18545],{"class":5243}," function",[3603,18547,18031],{"class":5220},[3603,18549,18089],{"class":5541},[3603,18551,18552,18554],{"class":3605,"line":3643},[3603,18553,14931],{"class":5533},[3603,18555,18096],{"class":5541},[3603,18557,18558,18560,18562,18564,18566,18569],{"class":3605,"line":3649},[3603,18559,18101],{"class":15364},[3603,18561,4805],{"class":15368},[3603,18563,18106],{"class":15371},[3603,18565,6507],{"class":5541},[3603,18567,18568],{"class":5224},"\"app\"",[3603,18570,18114],{"class":15364},[3603,18572,18573,18575,18578,18580,18582,18585],{"class":3605,"line":3655},[3603,18574,18119],{"class":15364},[3603,18576,18577],{"class":15368},"nav",[3603,18579,18106],{"class":15371},[3603,18581,6507],{"class":5541},[3603,18583,18584],{"class":5224},"\"navbar\"",[3603,18586,18114],{"class":15364},[3603,18588,18589,18592,18594,18596,18598,18601,18603,18606,18608,18610],{"class":3605,"line":3661},[3603,18590,18591],{"class":15364},"                \u003C",[3603,18593,3603],{"class":15368},[3603,18595,18106],{"class":15371},[3603,18597,6507],{"class":5541},[3603,18599,18600],{"class":5224},"\"brand\"",[3603,18602,18124],{"class":15364},[3603,18604,18605],{"class":5541},"☁️ S3 Demo App",[3603,18607,18129],{"class":15364},[3603,18609,3603],{"class":15368},[3603,18611,18114],{"class":15364},[3603,18613,18614,18616,18618,18620,18622,18625],{"class":3605,"line":3667},[3603,18615,18591],{"class":15364},[3603,18617,4805],{"class":15368},[3603,18619,18106],{"class":15371},[3603,18621,6507],{"class":5541},[3603,18623,18624],{"class":5224},"\"nav-links\"",[3603,18626,18114],{"class":15364},[3603,18628,18629,18631,18634],{"class":3605,"line":3673},[3603,18630,12121],{"class":5243},[3603,18632,18633],{"class":5214},"\u002F* NavLink автоматично додає клас active до поточного посилання *\u002F",[3603,18635,3670],{"class":5243},[3603,18637,18638,18641,18643,18646,18648,18651,18654],{"class":3605,"line":3678},[3603,18639,18640],{"class":15364},"                    \u003C",[3603,18642,18515],{"class":5537},[3603,18644,18645],{"class":15371}," to",[3603,18647,6507],{"class":5541},[3603,18649,18650],{"class":5224},"\"\u002F\"",[3603,18652,18653],{"class":15371}," end",[3603,18655,18114],{"class":15364},[3603,18657,18658],{"class":3605,"line":3684},[3603,18659,18660],{"class":5541},"                        Home\n",[3603,18662,18663,18666,18668],{"class":3605,"line":3690},[3603,18664,18665],{"class":15364},"                    \u003C\u002F",[3603,18667,18515],{"class":5537},[3603,18669,18114],{"class":15364},[3603,18671,18672,18674,18676,18678,18680,18683,18685,18688,18690,18692],{"class":3605,"line":3696},[3603,18673,18640],{"class":15364},[3603,18675,18515],{"class":5537},[3603,18677,18645],{"class":15371},[3603,18679,6507],{"class":5541},[3603,18681,18682],{"class":5224},"\"\u002Fabout\"",[3603,18684,18124],{"class":15364},[3603,18686,18687],{"class":5541},"About",[3603,18689,18129],{"class":15364},[3603,18691,18515],{"class":5537},[3603,18693,18114],{"class":15364},[3603,18695,18696,18699,18701],{"class":3605,"line":3702},[3603,18697,18698],{"class":15364},"                \u003C\u002F",[3603,18700,4805],{"class":15368},[3603,18702,18114],{"class":15364},[3603,18704,18705,18708,18710],{"class":3605,"line":3708},[3603,18706,18707],{"class":15364},"            \u003C\u002F",[3603,18709,18577],{"class":15368},[3603,18711,18114],{"class":15364},[3603,18713,18714,18716,18719,18721,18723,18726],{"class":3605,"line":4667},[3603,18715,18119],{"class":15364},[3603,18717,18718],{"class":15368},"main",[3603,18720,18106],{"class":15371},[3603,18722,6507],{"class":5541},[3603,18724,18725],{"class":5224},"\"content\"",[3603,18727,18114],{"class":15364},[3603,18729,18730,18732,18734,18737,18740,18743],{"class":3605,"line":4673},[3603,18731,18591],{"class":15364},[3603,18733,18510],{"class":5537},[3603,18735,18736],{"class":15364}," \u002F>",[3603,18738,18739],{"class":5243}," {",[3603,18741,18742],{"class":5214},"\u002F* тут рендериться Home або About залежно від URL *\u002F",[3603,18744,3670],{"class":5243},[3603,18746,18747,18749,18751],{"class":3605,"line":4679},[3603,18748,18707],{"class":15364},[3603,18750,18718],{"class":15368},[3603,18752,18114],{"class":15364},[3603,18754,18755,18757,18759],{"class":3605,"line":4685},[3603,18756,18155],{"class":15364},[3603,18758,4805],{"class":15368},[3603,18760,18114],{"class":15364},[3603,18762,18763],{"class":3605,"line":4691},[3603,18764,18164],{"class":5541},[3603,18766,18767],{"class":3605,"line":4696},[3603,18768,3670],{"class":5541},[3353,18770,18302,18771,18776],{},[3356,18772,18773],{},[3432,18774,18775],{},"src\u002Findex.css"," (Vite генерує цей файл як глобальний):",[3561,18778,18782],{"className":18779,"code":18780,"language":18781,"meta":3569,"style":3569},"language-css shiki shiki-themes light-plus dark-plus dark-plus","\u002F* src\u002Findex.css *\u002F\n*,\n*::before,\n*::after {\n    box-sizing: border-box;\n    margin: 0;\n    padding: 0;\n}\n\nbody {\n    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n    background: #f0f2f5;\n    color: #1a1a2e;\n}\n\n.app {\n    min-height: 100vh;\n    display: flex;\n    flex-direction: column;\n}\n\n.navbar {\n    background: #232f3e; \u002F* AWS dark navy *\u002F\n    color: white;\n    padding: 1rem 2rem;\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n}\n\n.brand {\n    font-size: 1.2rem;\n    font-weight: 700;\n}\n\n.nav-links a {\n    color: #adb5bd;\n    text-decoration: none;\n    margin-left: 1.5rem;\n    font-weight: 500;\n    transition: color 0.2s;\n}\n\n.nav-links a.active,\n.nav-links a:hover {\n    color: #ff9900;\n} \u002F* AWS orange *\u002F\n\n.content {\n    flex: 1;\n    max-width: 900px;\n    margin: 2rem auto;\n    padding: 0 1.5rem;\n    width: 100%;\n}\n\n\u002F* Сторінки *\u002F\n.page h1 {\n    font-size: 2rem;\n    margin-bottom: 1rem;\n    color: #232f3e;\n}\n.page h2 {\n    font-size: 1.4rem;\n    margin: 1.5rem 0 0.75rem;\n    color: #232f3e;\n}\n.page p {\n    color: #555;\n    line-height: 1.6;\n    margin-bottom: 1rem;\n}\n.page ol {\n    padding-left: 1.5rem;\n    color: #555;\n    line-height: 2;\n}\n\n\u002F* Картки *\u002F\n.cards {\n    display: grid;\n    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));\n    gap: 1rem;\n    margin-top: 1.5rem;\n}\n\n.card {\n    background: white;\n    border-radius: 8px;\n    padding: 1.5rem;\n    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);\n}\n\n.card h3 {\n    margin-bottom: 0.5rem;\n    color: #232f3e;\n}\n.card p {\n    font-size: 0.9rem;\n    color: #666;\n    margin: 0;\n}\n\n\u002F* 404 *\u002F\n.not-found {\n    text-align: center;\n    padding: 4rem 0;\n}\n.not-found h1 {\n    font-size: 5rem;\n    color: #dee2e6;\n}\n.not-found p {\n    color: #aaa;\n    margin-top: 0.5rem;\n}\n\n\u002F* Inline elements *\u002F\nkbd {\n    background: #eee;\n    border: 1px solid #ccc;\n    border-radius: 3px;\n    padding: 1px 6px;\n    font-size: 0.85em;\n}\ncode {\n    background: #f4f4f4;\n    padding: 1px 5px;\n    border-radius: 3px;\n    font-size: 0.88em;\n    font-family: monospace;\n}\n","css",[3432,18783,18784,18789,18795,18805,18814,18827,18838,18849,18853,18857,18864,18883,18895,18907,18911,18915,18922,18934,18946,18958,18962,18966,18973,18987,18998,19012,19022,19034,19046,19083,19087,19091,19098,19110,19122,19126,19130,19140,19151,19163,19175,19186,19201,19205,19209,19218,19227,19238,19246,19250,19257,19268,19280,19294,19307,19319,19323,19327,19332,19342,19352,19363,19373,19377,19386,19397,19413,19423,19427,19436,19447,19459,19469,19473,19482,19493,19503,19514,19518,19522,19527,19534,19545,19578,19589,19600,19604,19608,19615,19625,19637,19647,19682,19686,19690,19699,19710,19720,19724,19732,19743,19754,19764,19768,19772,19777,19784,19795,19808,19812,19820,19831,19842,19846,19854,19865,19875,19879,19883,19888,19895,19906,19924,19935,19948,19959,19963,19969,19980,19993,20003,20014,20025],{"__ignoreMap":3569},[3603,18785,18786],{"class":3605,"line":3606},[3603,18787,18788],{"class":5214},"\u002F* src\u002Findex.css *\u002F\n",[3603,18790,18791,18793],{"class":3605,"line":3612},[3603,18792,13981],{"class":15368},[3603,18794,5604],{"class":5541},[3603,18796,18797,18799,18803],{"class":3605,"line":3618},[3603,18798,13981],{"class":15368},[3603,18800,18802],{"class":18801},"sqdDX","::before",[3603,18804,5604],{"class":5541},[3603,18806,18807,18809,18812],{"class":3605,"line":3624},[3603,18808,13981],{"class":15368},[3603,18810,18811],{"class":18801},"::after",[3603,18813,15067],{"class":5541},[3603,18815,18816,18819,18821,18825],{"class":3605,"line":3631},[3603,18817,18818],{"class":15371},"    box-sizing",[3603,18820,5881],{"class":5541},[3603,18822,18824],{"class":18823},"sDUd3","border-box",[3603,18826,5547],{"class":5541},[3603,18828,18829,18832,18834,18836],{"class":3605,"line":3637},[3603,18830,18831],{"class":15371},"    margin",[3603,18833,5881],{"class":5541},[3603,18835,17376],{"class":5943},[3603,18837,5547],{"class":5541},[3603,18839,18840,18843,18845,18847],{"class":3605,"line":3643},[3603,18841,18842],{"class":15371},"    padding",[3603,18844,5881],{"class":5541},[3603,18846,17376],{"class":5943},[3603,18848,5547],{"class":5541},[3603,18850,18851],{"class":3605,"line":3649},[3603,18852,3670],{"class":5541},[3603,18854,18855],{"class":3605,"line":3655},[3603,18856,3628],{"emptyLinePlaceholder":3627},[3603,18858,18859,18862],{"class":3605,"line":3661},[3603,18860,18861],{"class":18801},"body",[3603,18863,15067],{"class":5541},[3603,18865,18866,18869,18872,18875,18878,18881],{"class":3605,"line":3667},[3603,18867,18868],{"class":15371},"    font-family",[3603,18870,18871],{"class":5541},": -apple-system, BlinkMacSystemFont, ",[3603,18873,18874],{"class":5224},"'Segoe UI'",[3603,18876,18877],{"class":5541},", Roboto, ",[3603,18879,18880],{"class":18823},"sans-serif",[3603,18882,5547],{"class":5541},[3603,18884,18885,18888,18890,18893],{"class":3605,"line":3673},[3603,18886,18887],{"class":15371},"    background",[3603,18889,5881],{"class":5541},[3603,18891,18892],{"class":18823},"#f0f2f5",[3603,18894,5547],{"class":5541},[3603,18896,18897,18900,18902,18905],{"class":3605,"line":3678},[3603,18898,18899],{"class":15371},"    color",[3603,18901,5881],{"class":5541},[3603,18903,18904],{"class":18823},"#1a1a2e",[3603,18906,5547],{"class":5541},[3603,18908,18909],{"class":3605,"line":3684},[3603,18910,3670],{"class":5541},[3603,18912,18913],{"class":3605,"line":3690},[3603,18914,3628],{"emptyLinePlaceholder":3627},[3603,18916,18917,18920],{"class":3605,"line":3696},[3603,18918,18919],{"class":18801},".app",[3603,18921,15067],{"class":5541},[3603,18923,18924,18927,18929,18932],{"class":3605,"line":3702},[3603,18925,18926],{"class":15371},"    min-height",[3603,18928,5881],{"class":5541},[3603,18930,18931],{"class":5943},"100vh",[3603,18933,5547],{"class":5541},[3603,18935,18936,18939,18941,18944],{"class":3605,"line":3708},[3603,18937,18938],{"class":15371},"    display",[3603,18940,5881],{"class":5541},[3603,18942,18943],{"class":18823},"flex",[3603,18945,5547],{"class":5541},[3603,18947,18948,18951,18953,18956],{"class":3605,"line":4667},[3603,18949,18950],{"class":15371},"    flex-direction",[3603,18952,5881],{"class":5541},[3603,18954,18955],{"class":18823},"column",[3603,18957,5547],{"class":5541},[3603,18959,18960],{"class":3605,"line":4673},[3603,18961,3670],{"class":5541},[3603,18963,18964],{"class":3605,"line":4679},[3603,18965,3628],{"emptyLinePlaceholder":3627},[3603,18967,18968,18971],{"class":3605,"line":4685},[3603,18969,18970],{"class":18801},".navbar",[3603,18972,15067],{"class":5541},[3603,18974,18975,18977,18979,18982,18984],{"class":3605,"line":4691},[3603,18976,18887],{"class":15371},[3603,18978,5881],{"class":5541},[3603,18980,18981],{"class":18823},"#232f3e",[3603,18983,11318],{"class":5541},[3603,18985,18986],{"class":5214},"\u002F* AWS dark navy *\u002F\n",[3603,18988,18989,18991,18993,18996],{"class":3605,"line":4696},[3603,18990,18899],{"class":15371},[3603,18992,5881],{"class":5541},[3603,18994,18995],{"class":18823},"white",[3603,18997,5547],{"class":5541},[3603,18999,19000,19002,19004,19007,19010],{"class":3605,"line":4701},[3603,19001,18842],{"class":15371},[3603,19003,5881],{"class":5541},[3603,19005,19006],{"class":5943},"1rem",[3603,19008,19009],{"class":5943}," 2rem",[3603,19011,5547],{"class":5541},[3603,19013,19014,19016,19018,19020],{"class":3605,"line":4707},[3603,19015,18938],{"class":15371},[3603,19017,5881],{"class":5541},[3603,19019,18943],{"class":18823},[3603,19021,5547],{"class":5541},[3603,19023,19024,19027,19029,19032],{"class":3605,"line":4713},[3603,19025,19026],{"class":15371},"    justify-content",[3603,19028,5881],{"class":5541},[3603,19030,19031],{"class":18823},"space-between",[3603,19033,5547],{"class":5541},[3603,19035,19036,19039,19041,19044],{"class":3605,"line":4719},[3603,19037,19038],{"class":15371},"    align-items",[3603,19040,5881],{"class":5541},[3603,19042,19043],{"class":18823},"center",[3603,19045,5547],{"class":5541},[3603,19047,19048,19051,19053,19055,19058,19061,19064,19066,19068,19070,19072,19074,19076,19078,19081],{"class":3605,"line":4724},[3603,19049,19050],{"class":15371},"    box-shadow",[3603,19052,5881],{"class":5541},[3603,19054,17376],{"class":5943},[3603,19056,19057],{"class":5943}," 2px",[3603,19059,19060],{"class":5943}," 8px",[3603,19062,19063],{"class":5220}," rgba",[3603,19065,5707],{"class":5541},[3603,19067,17376],{"class":5943},[3603,19069,3470],{"class":5541},[3603,19071,17376],{"class":5943},[3603,19073,3470],{"class":5541},[3603,19075,17376],{"class":5943},[3603,19077,3470],{"class":5541},[3603,19079,19080],{"class":5943},"0.3",[3603,19082,5713],{"class":5541},[3603,19084,19085],{"class":3605,"line":4730},[3603,19086,3670],{"class":5541},[3603,19088,19089],{"class":3605,"line":4736},[3603,19090,3628],{"emptyLinePlaceholder":3627},[3603,19092,19093,19096],{"class":3605,"line":4742},[3603,19094,19095],{"class":18801},".brand",[3603,19097,15067],{"class":5541},[3603,19099,19100,19103,19105,19108],{"class":3605,"line":4748},[3603,19101,19102],{"class":15371},"    font-size",[3603,19104,5881],{"class":5541},[3603,19106,19107],{"class":5943},"1.2rem",[3603,19109,5547],{"class":5541},[3603,19111,19112,19115,19117,19120],{"class":3605,"line":4753},[3603,19113,19114],{"class":15371},"    font-weight",[3603,19116,5881],{"class":5541},[3603,19118,19119],{"class":5943},"700",[3603,19121,5547],{"class":5541},[3603,19123,19124],{"class":3605,"line":4758},[3603,19125,3670],{"class":5541},[3603,19127,19128],{"class":3605,"line":6363},[3603,19129,3628],{"emptyLinePlaceholder":3627},[3603,19131,19132,19135,19138],{"class":3605,"line":6368},[3603,19133,19134],{"class":18801},".nav-links",[3603,19136,19137],{"class":18801}," a",[3603,19139,15067],{"class":5541},[3603,19141,19142,19144,19146,19149],{"class":3605,"line":6830},[3603,19143,18899],{"class":15371},[3603,19145,5881],{"class":5541},[3603,19147,19148],{"class":18823},"#adb5bd",[3603,19150,5547],{"class":5541},[3603,19152,19153,19156,19158,19161],{"class":3605,"line":6835},[3603,19154,19155],{"class":15371},"    text-decoration",[3603,19157,5881],{"class":5541},[3603,19159,19160],{"class":18823},"none",[3603,19162,5547],{"class":5541},[3603,19164,19165,19168,19170,19173],{"class":3605,"line":6841},[3603,19166,19167],{"class":15371},"    margin-left",[3603,19169,5881],{"class":5541},[3603,19171,19172],{"class":5943},"1.5rem",[3603,19174,5547],{"class":5541},[3603,19176,19177,19179,19181,19184],{"class":3605,"line":6858},[3603,19178,19114],{"class":15371},[3603,19180,5881],{"class":5541},[3603,19182,19183],{"class":5943},"500",[3603,19185,5547],{"class":5541},[3603,19187,19188,19191,19193,19196,19199],{"class":3605,"line":6871},[3603,19189,19190],{"class":15371},"    transition",[3603,19192,5881],{"class":5541},[3603,19194,19195],{"class":18823},"color",[3603,19197,19198],{"class":5943}," 0.2s",[3603,19200,5547],{"class":5541},[3603,19202,19203],{"class":3605,"line":6881},[3603,19204,3670],{"class":5541},[3603,19206,19207],{"class":3605,"line":6894},[3603,19208,3628],{"emptyLinePlaceholder":3627},[3603,19210,19211,19213,19216],{"class":3605,"line":6915},[3603,19212,19134],{"class":18801},[3603,19214,19215],{"class":18801}," a.active",[3603,19217,5604],{"class":5541},[3603,19219,19220,19222,19225],{"class":3605,"line":6927},[3603,19221,19134],{"class":18801},[3603,19223,19224],{"class":18801}," a:hover",[3603,19226,15067],{"class":5541},[3603,19228,19229,19231,19233,19236],{"class":3605,"line":6932},[3603,19230,18899],{"class":15371},[3603,19232,5881],{"class":5541},[3603,19234,19235],{"class":18823},"#ff9900",[3603,19237,5547],{"class":5541},[3603,19239,19240,19243],{"class":3605,"line":6946},[3603,19241,19242],{"class":5541},"} ",[3603,19244,19245],{"class":5214},"\u002F* AWS orange *\u002F\n",[3603,19247,19248],{"class":3605,"line":6951},[3603,19249,3628],{"emptyLinePlaceholder":3627},[3603,19251,19252,19255],{"class":3605,"line":6963},[3603,19253,19254],{"class":18801},".content",[3603,19256,15067],{"class":5541},[3603,19258,19259,19262,19264,19266],{"class":3605,"line":6976},[3603,19260,19261],{"class":15371},"    flex",[3603,19263,5881],{"class":5541},[3603,19265,15161],{"class":5943},[3603,19267,5547],{"class":5541},[3603,19269,19270,19273,19275,19278],{"class":3605,"line":6985},[3603,19271,19272],{"class":15371},"    max-width",[3603,19274,5881],{"class":5541},[3603,19276,19277],{"class":5943},"900px",[3603,19279,5547],{"class":5541},[3603,19281,19282,19284,19286,19289,19292],{"class":3605,"line":6998},[3603,19283,18831],{"class":15371},[3603,19285,5881],{"class":5541},[3603,19287,19288],{"class":5943},"2rem",[3603,19290,19291],{"class":18823}," auto",[3603,19293,5547],{"class":5541},[3603,19295,19296,19298,19300,19302,19305],{"class":3605,"line":7009},[3603,19297,18842],{"class":15371},[3603,19299,5881],{"class":5541},[3603,19301,17376],{"class":5943},[3603,19303,19304],{"class":5943}," 1.5rem",[3603,19306,5547],{"class":5541},[3603,19308,19309,19312,19314,19317],{"class":3605,"line":7014},[3603,19310,19311],{"class":15371},"    width",[3603,19313,5881],{"class":5541},[3603,19315,19316],{"class":5943},"100%",[3603,19318,5547],{"class":5541},[3603,19320,19321],{"class":3605,"line":8491},[3603,19322,3670],{"class":5541},[3603,19324,19325],{"class":3605,"line":8497},[3603,19326,3628],{"emptyLinePlaceholder":3627},[3603,19328,19329],{"class":3605,"line":8503},[3603,19330,19331],{"class":5214},"\u002F* Сторінки *\u002F\n",[3603,19333,19334,19337,19340],{"class":3605,"line":8509},[3603,19335,19336],{"class":18801},".page",[3603,19338,19339],{"class":18801}," h1",[3603,19341,15067],{"class":5541},[3603,19343,19344,19346,19348,19350],{"class":3605,"line":8515},[3603,19345,19102],{"class":15371},[3603,19347,5881],{"class":5541},[3603,19349,19288],{"class":5943},[3603,19351,5547],{"class":5541},[3603,19353,19354,19357,19359,19361],{"class":3605,"line":8521},[3603,19355,19356],{"class":15371},"    margin-bottom",[3603,19358,5881],{"class":5541},[3603,19360,19006],{"class":5943},[3603,19362,5547],{"class":5541},[3603,19364,19365,19367,19369,19371],{"class":3605,"line":8527},[3603,19366,18899],{"class":15371},[3603,19368,5881],{"class":5541},[3603,19370,18981],{"class":18823},[3603,19372,5547],{"class":5541},[3603,19374,19375],{"class":3605,"line":8532},[3603,19376,3670],{"class":5541},[3603,19378,19379,19381,19384],{"class":3605,"line":16493},[3603,19380,19336],{"class":18801},[3603,19382,19383],{"class":18801}," h2",[3603,19385,15067],{"class":5541},[3603,19387,19388,19390,19392,19395],{"class":3605,"line":16498},[3603,19389,19102],{"class":15371},[3603,19391,5881],{"class":5541},[3603,19393,19394],{"class":5943},"1.4rem",[3603,19396,5547],{"class":5541},[3603,19398,19399,19401,19403,19405,19408,19411],{"class":3605,"line":16503},[3603,19400,18831],{"class":15371},[3603,19402,5881],{"class":5541},[3603,19404,19172],{"class":5943},[3603,19406,19407],{"class":5943}," 0",[3603,19409,19410],{"class":5943}," 0.75rem",[3603,19412,5547],{"class":5541},[3603,19414,19415,19417,19419,19421],{"class":3605,"line":16509},[3603,19416,18899],{"class":15371},[3603,19418,5881],{"class":5541},[3603,19420,18981],{"class":18823},[3603,19422,5547],{"class":5541},[3603,19424,19425],{"class":3605,"line":16529},[3603,19426,3670],{"class":5541},[3603,19428,19429,19431,19434],{"class":3605,"line":16534},[3603,19430,19336],{"class":18801},[3603,19432,19433],{"class":18801}," p",[3603,19435,15067],{"class":5541},[3603,19437,19438,19440,19442,19445],{"class":3605,"line":16556},[3603,19439,18899],{"class":15371},[3603,19441,5881],{"class":5541},[3603,19443,19444],{"class":18823},"#555",[3603,19446,5547],{"class":5541},[3603,19448,19449,19452,19454,19457],{"class":3605,"line":16561},[3603,19450,19451],{"class":15371},"    line-height",[3603,19453,5881],{"class":5541},[3603,19455,19456],{"class":5943},"1.6",[3603,19458,5547],{"class":5541},[3603,19460,19461,19463,19465,19467],{"class":3605,"line":16566},[3603,19462,19356],{"class":15371},[3603,19464,5881],{"class":5541},[3603,19466,19006],{"class":5943},[3603,19468,5547],{"class":5541},[3603,19470,19471],{"class":3605,"line":16572},[3603,19472,3670],{"class":5541},[3603,19474,19475,19477,19480],{"class":3605,"line":16601},[3603,19476,19336],{"class":18801},[3603,19478,19479],{"class":18801}," ol",[3603,19481,15067],{"class":5541},[3603,19483,19484,19487,19489,19491],{"class":3605,"line":16606},[3603,19485,19486],{"class":15371},"    padding-left",[3603,19488,5881],{"class":5541},[3603,19490,19172],{"class":5943},[3603,19492,5547],{"class":5541},[3603,19494,19495,19497,19499,19501],{"class":3605,"line":16623},[3603,19496,18899],{"class":15371},[3603,19498,5881],{"class":5541},[3603,19500,19444],{"class":18823},[3603,19502,5547],{"class":5541},[3603,19504,19505,19507,19509,19512],{"class":3605,"line":16628},[3603,19506,19451],{"class":15371},[3603,19508,5881],{"class":5541},[3603,19510,19511],{"class":5943},"2",[3603,19513,5547],{"class":5541},[3603,19515,19516],{"class":3605,"line":16639},[3603,19517,3670],{"class":5541},[3603,19519,19520],{"class":3605,"line":16650},[3603,19521,3628],{"emptyLinePlaceholder":3627},[3603,19523,19524],{"class":3605,"line":16673},[3603,19525,19526],{"class":5214},"\u002F* Картки *\u002F\n",[3603,19528,19529,19532],{"class":3605,"line":16686},[3603,19530,19531],{"class":18801},".cards",[3603,19533,15067],{"class":5541},[3603,19535,19536,19538,19540,19543],{"class":3605,"line":16691},[3603,19537,18938],{"class":15371},[3603,19539,5881],{"class":5541},[3603,19541,19542],{"class":18823},"grid",[3603,19544,5547],{"class":5541},[3603,19546,19547,19550,19552,19555,19557,19560,19562,19565,19567,19570,19572,19575],{"class":3605,"line":16696},[3603,19548,19549],{"class":15371},"    grid-template-columns",[3603,19551,5881],{"class":5541},[3603,19553,19554],{"class":5220},"repeat",[3603,19556,5707],{"class":5541},[3603,19558,19559],{"class":18823},"auto-fit",[3603,19561,3470],{"class":5541},[3603,19563,19564],{"class":5220},"minmax",[3603,19566,5707],{"class":5541},[3603,19568,19569],{"class":5943},"220px",[3603,19571,3470],{"class":5541},[3603,19573,19574],{"class":5943},"1fr",[3603,19576,19577],{"class":5541},"));\n",[3603,19579,19580,19583,19585,19587],{"class":3605,"line":16701},[3603,19581,19582],{"class":15371},"    gap",[3603,19584,5881],{"class":5541},[3603,19586,19006],{"class":5943},[3603,19588,5547],{"class":5541},[3603,19590,19591,19594,19596,19598],{"class":3605,"line":16707},[3603,19592,19593],{"class":15371},"    margin-top",[3603,19595,5881],{"class":5541},[3603,19597,19172],{"class":5943},[3603,19599,5547],{"class":5541},[3603,19601,19602],{"class":3605,"line":16731},[3603,19603,3670],{"class":5541},[3603,19605,19606],{"class":3605,"line":16746},[3603,19607,3628],{"emptyLinePlaceholder":3627},[3603,19609,19610,19613],{"class":3605,"line":16751},[3603,19611,19612],{"class":18801},".card",[3603,19614,15067],{"class":5541},[3603,19616,19617,19619,19621,19623],{"class":3605,"line":16768},[3603,19618,18887],{"class":15371},[3603,19620,5881],{"class":5541},[3603,19622,18995],{"class":18823},[3603,19624,5547],{"class":5541},[3603,19626,19627,19630,19632,19635],{"class":3605,"line":16773},[3603,19628,19629],{"class":15371},"    border-radius",[3603,19631,5881],{"class":5541},[3603,19633,19634],{"class":5943},"8px",[3603,19636,5547],{"class":5541},[3603,19638,19639,19641,19643,19645],{"class":3605,"line":16784},[3603,19640,18842],{"class":15371},[3603,19642,5881],{"class":5541},[3603,19644,19172],{"class":5943},[3603,19646,5547],{"class":5541},[3603,19648,19649,19651,19653,19655,19658,19661,19663,19665,19667,19669,19671,19673,19675,19677,19680],{"class":3605,"line":16795},[3603,19650,19050],{"class":15371},[3603,19652,5881],{"class":5541},[3603,19654,17376],{"class":5943},[3603,19656,19657],{"class":5943}," 1px",[3603,19659,19660],{"class":5943}," 4px",[3603,19662,19063],{"class":5220},[3603,19664,5707],{"class":5541},[3603,19666,17376],{"class":5943},[3603,19668,3470],{"class":5541},[3603,19670,17376],{"class":5943},[3603,19672,3470],{"class":5541},[3603,19674,17376],{"class":5943},[3603,19676,3470],{"class":5541},[3603,19678,19679],{"class":5943},"0.1",[3603,19681,5713],{"class":5541},[3603,19683,19684],{"class":3605,"line":16818},[3603,19685,3670],{"class":5541},[3603,19687,19688],{"class":3605,"line":16833},[3603,19689,3628],{"emptyLinePlaceholder":3627},[3603,19691,19692,19694,19697],{"class":3605,"line":16842},[3603,19693,19612],{"class":18801},[3603,19695,19696],{"class":18801}," h3",[3603,19698,15067],{"class":5541},[3603,19700,19701,19703,19705,19708],{"class":3605,"line":16847},[3603,19702,19356],{"class":15371},[3603,19704,5881],{"class":5541},[3603,19706,19707],{"class":5943},"0.5rem",[3603,19709,5547],{"class":5541},[3603,19711,19712,19714,19716,19718],{"class":3605,"line":16852},[3603,19713,18899],{"class":15371},[3603,19715,5881],{"class":5541},[3603,19717,18981],{"class":18823},[3603,19719,5547],{"class":5541},[3603,19721,19722],{"class":3605,"line":16857},[3603,19723,3670],{"class":5541},[3603,19725,19726,19728,19730],{"class":3605,"line":16863},[3603,19727,19612],{"class":18801},[3603,19729,19433],{"class":18801},[3603,19731,15067],{"class":5541},[3603,19733,19734,19736,19738,19741],{"class":3605,"line":16869},[3603,19735,19102],{"class":15371},[3603,19737,5881],{"class":5541},[3603,19739,19740],{"class":5943},"0.9rem",[3603,19742,5547],{"class":5541},[3603,19744,19745,19747,19749,19752],{"class":3605,"line":16875},[3603,19746,18899],{"class":15371},[3603,19748,5881],{"class":5541},[3603,19750,19751],{"class":18823},"#666",[3603,19753,5547],{"class":5541},[3603,19755,19756,19758,19760,19762],{"class":3605,"line":16881},[3603,19757,18831],{"class":15371},[3603,19759,5881],{"class":5541},[3603,19761,17376],{"class":5943},[3603,19763,5547],{"class":5541},[3603,19765,19766],{"class":3605,"line":16887},[3603,19767,3670],{"class":5541},[3603,19769,19770],{"class":3605,"line":16893},[3603,19771,3628],{"emptyLinePlaceholder":3627},[3603,19773,19774],{"class":3605,"line":16920},[3603,19775,19776],{"class":5214},"\u002F* 404 *\u002F\n",[3603,19778,19779,19782],{"class":3605,"line":16925},[3603,19780,19781],{"class":18801},".not-found",[3603,19783,15067],{"class":5541},[3603,19785,19786,19789,19791,19793],{"class":3605,"line":16946},[3603,19787,19788],{"class":15371},"    text-align",[3603,19790,5881],{"class":5541},[3603,19792,19043],{"class":18823},[3603,19794,5547],{"class":5541},[3603,19796,19797,19799,19801,19804,19806],{"class":3605,"line":16963},[3603,19798,18842],{"class":15371},[3603,19800,5881],{"class":5541},[3603,19802,19803],{"class":5943},"4rem",[3603,19805,19407],{"class":5943},[3603,19807,5547],{"class":5541},[3603,19809,19810],{"class":3605,"line":16968},[3603,19811,3670],{"class":5541},[3603,19813,19814,19816,19818],{"class":3605,"line":16979},[3603,19815,19781],{"class":18801},[3603,19817,19339],{"class":18801},[3603,19819,15067],{"class":5541},[3603,19821,19822,19824,19826,19829],{"class":3605,"line":16990},[3603,19823,19102],{"class":15371},[3603,19825,5881],{"class":5541},[3603,19827,19828],{"class":5943},"5rem",[3603,19830,5547],{"class":5541},[3603,19832,19833,19835,19837,19840],{"class":3605,"line":17003},[3603,19834,18899],{"class":15371},[3603,19836,5881],{"class":5541},[3603,19838,19839],{"class":18823},"#dee2e6",[3603,19841,5547],{"class":5541},[3603,19843,19844],{"class":3605,"line":17009},[3603,19845,3670],{"class":5541},[3603,19847,19848,19850,19852],{"class":3605,"line":17032},[3603,19849,19781],{"class":18801},[3603,19851,19433],{"class":18801},[3603,19853,15067],{"class":5541},[3603,19855,19856,19858,19860,19863],{"class":3605,"line":17042},[3603,19857,18899],{"class":15371},[3603,19859,5881],{"class":5541},[3603,19861,19862],{"class":18823},"#aaa",[3603,19864,5547],{"class":5541},[3603,19866,19867,19869,19871,19873],{"class":3605,"line":17047},[3603,19868,19593],{"class":15371},[3603,19870,5881],{"class":5541},[3603,19872,19707],{"class":5943},[3603,19874,5547],{"class":5541},[3603,19876,19877],{"class":3605,"line":17052},[3603,19878,3670],{"class":5541},[3603,19880,19881],{"class":3605,"line":17057},[3603,19882,3628],{"emptyLinePlaceholder":3627},[3603,19884,19885],{"class":3605,"line":17063},[3603,19886,19887],{"class":5214},"\u002F* Inline elements *\u002F\n",[3603,19889,19890,19893],{"class":3605,"line":17069},[3603,19891,19892],{"class":18801},"kbd",[3603,19894,15067],{"class":5541},[3603,19896,19897,19899,19901,19904],{"class":3605,"line":17075},[3603,19898,18887],{"class":15371},[3603,19900,5881],{"class":5541},[3603,19902,19903],{"class":18823},"#eee",[3603,19905,5547],{"class":5541},[3603,19907,19908,19911,19913,19916,19919,19922],{"class":3605,"line":17081},[3603,19909,19910],{"class":15371},"    border",[3603,19912,5881],{"class":5541},[3603,19914,19915],{"class":5943},"1px",[3603,19917,19918],{"class":18823}," solid",[3603,19920,19921],{"class":18823}," #ccc",[3603,19923,5547],{"class":5541},[3603,19925,19926,19928,19930,19933],{"class":3605,"line":17109},[3603,19927,19629],{"class":15371},[3603,19929,5881],{"class":5541},[3603,19931,19932],{"class":5943},"3px",[3603,19934,5547],{"class":5541},[3603,19936,19937,19939,19941,19943,19946],{"class":3605,"line":17114},[3603,19938,18842],{"class":15371},[3603,19940,5881],{"class":5541},[3603,19942,19915],{"class":5943},[3603,19944,19945],{"class":5943}," 6px",[3603,19947,5547],{"class":5541},[3603,19949,19950,19952,19954,19957],{"class":3605,"line":17131},[3603,19951,19102],{"class":15371},[3603,19953,5881],{"class":5541},[3603,19955,19956],{"class":5943},"0.85em",[3603,19958,5547],{"class":5541},[3603,19960,19961],{"class":3605,"line":17136},[3603,19962,3670],{"class":5541},[3603,19964,19965,19967],{"class":3605,"line":17148},[3603,19966,3432],{"class":18801},[3603,19968,15067],{"class":5541},[3603,19970,19971,19973,19975,19978],{"class":3605,"line":17161},[3603,19972,18887],{"class":15371},[3603,19974,5881],{"class":5541},[3603,19976,19977],{"class":18823},"#f4f4f4",[3603,19979,5547],{"class":5541},[3603,19981,19982,19984,19986,19988,19991],{"class":3605,"line":17173},[3603,19983,18842],{"class":15371},[3603,19985,5881],{"class":5541},[3603,19987,19915],{"class":5943},[3603,19989,19990],{"class":5943}," 5px",[3603,19992,5547],{"class":5541},[3603,19994,19995,19997,19999,20001],{"class":3605,"line":17184},[3603,19996,19629],{"class":15371},[3603,19998,5881],{"class":5541},[3603,20000,19932],{"class":5943},[3603,20002,5547],{"class":5541},[3603,20004,20005,20007,20009,20012],{"class":3605,"line":17189},[3603,20006,19102],{"class":15371},[3603,20008,5881],{"class":5541},[3603,20010,20011],{"class":5943},"0.88em",[3603,20013,5547],{"class":5541},[3603,20015,20016,20018,20020,20023],{"class":3605,"line":17194},[3603,20017,18868],{"class":15371},[3603,20019,5881],{"class":5541},[3603,20021,20022],{"class":18823},"monospace",[3603,20024,5547],{"class":5541},[3603,20026,20028],{"class":3605,"line":20027},133,[3603,20029,3670],{"class":5541},[3353,20031,17992,20032,5629],{},[3356,20033,20034],{},[3432,20035,20036],{},"src\u002Fpages\u002FHome.jsx",[3561,20038,20040],{"className":14969,"code":20039,"language":14971,"meta":3569,"style":3569},"\u002F\u002F src\u002Fpages\u002FHome.jsx\nexport default function Home() {\n    return (\n        \u003Cdiv className=\"page\">\n            \u003Ch1>🚀 Ласкаво просимо!\u003C\u002Fh1>\n            \u003Cp>\n                Цей React SPA задеплоєно на \u003Cstrong>Amazon S3 Static Website Hosting\u003C\u002Fstrong>.\n            \u003C\u002Fp>\n            \u003Cdiv className=\"cards\">\n                \u003Cdiv className=\"card\">\n                    \u003Ch3>⚡ Швидкість\u003C\u002Fh3>\n                    \u003Cp>Статичні файли роздаються напряму з S3 або через CloudFront CDN.\u003C\u002Fp>\n                \u003C\u002Fdiv>\n                \u003Cdiv className=\"card\">\n                    \u003Ch3>💰 Вартість\u003C\u002Fh3>\n                    \u003Cp>Зберігання від $0.023\u002FGB. Для типового SPA — менше $1 на місяць.\u003C\u002Fp>\n                \u003C\u002Fdiv>\n                \u003Cdiv className=\"card\">\n                    \u003Ch3>🔒 Надійність\u003C\u002Fh3>\n                    \u003Cp>S3 гарантує 99.99% доступність та 11 дев'яток довговічності даних.\u003C\u002Fp>\n                \u003C\u002Fdiv>\n            \u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n    )\n}\n",[3432,20041,20042,20047,20059,20065,20080,20097,20105,20128,20136,20151,20166,20183,20200,20208,20222,20239,20256,20264,20278,20295,20312,20320,20328,20336,20340],{"__ignoreMap":3569},[3603,20043,20044],{"class":3605,"line":3606},[3603,20045,20046],{"class":5214},"\u002F\u002F src\u002Fpages\u002FHome.jsx\n",[3603,20048,20049,20051,20053,20055,20057],{"class":3605,"line":3612},[3603,20050,18177],{"class":5533},[3603,20052,18542],{"class":5533},[3603,20054,18545],{"class":5243},[3603,20056,18043],{"class":5220},[3603,20058,18089],{"class":5541},[3603,20060,20061,20063],{"class":3605,"line":3618},[3603,20062,14931],{"class":5533},[3603,20064,18096],{"class":5541},[3603,20066,20067,20069,20071,20073,20075,20078],{"class":3605,"line":3624},[3603,20068,18101],{"class":15364},[3603,20070,4805],{"class":15368},[3603,20072,18106],{"class":15371},[3603,20074,6507],{"class":5541},[3603,20076,20077],{"class":5224},"\"page\"",[3603,20079,18114],{"class":15364},[3603,20081,20082,20084,20086,20088,20091,20093,20095],{"class":3605,"line":3631},[3603,20083,18119],{"class":15364},[3603,20085,3344],{"class":15368},[3603,20087,18124],{"class":15364},[3603,20089,20090],{"class":5541},"🚀 Ласкаво просимо!",[3603,20092,18129],{"class":15364},[3603,20094,3344],{"class":15368},[3603,20096,18114],{"class":15364},[3603,20098,20099,20101,20103],{"class":3605,"line":3637},[3603,20100,18119],{"class":15364},[3603,20102,3353],{"class":15368},[3603,20104,18114],{"class":15364},[3603,20106,20107,20110,20112,20114,20116,20119,20121,20123,20125],{"class":3605,"line":3643},[3603,20108,20109],{"class":5541},"                Цей React SPA задеплоєно на ",[3603,20111,11250],{"class":15364},[3603,20113,3356],{"class":15368},[3603,20115,18124],{"class":15364},[3603,20117,20118],{"class":5541},"Amazon S3 Static Website Hosting",[3603,20120,18129],{"class":15364},[3603,20122,3356],{"class":15368},[3603,20124,18124],{"class":15364},[3603,20126,20127],{"class":5541},".\n",[3603,20129,20130,20132,20134],{"class":3605,"line":3649},[3603,20131,18707],{"class":15364},[3603,20133,3353],{"class":15368},[3603,20135,18114],{"class":15364},[3603,20137,20138,20140,20142,20144,20146,20149],{"class":3605,"line":3655},[3603,20139,18119],{"class":15364},[3603,20141,4805],{"class":15368},[3603,20143,18106],{"class":15371},[3603,20145,6507],{"class":5541},[3603,20147,20148],{"class":5224},"\"cards\"",[3603,20150,18114],{"class":15364},[3603,20152,20153,20155,20157,20159,20161,20164],{"class":3605,"line":3661},[3603,20154,18591],{"class":15364},[3603,20156,4805],{"class":15368},[3603,20158,18106],{"class":15371},[3603,20160,6507],{"class":5541},[3603,20162,20163],{"class":5224},"\"card\"",[3603,20165,18114],{"class":15364},[3603,20167,20168,20170,20172,20174,20177,20179,20181],{"class":3605,"line":3667},[3603,20169,18640],{"class":15364},[3603,20171,3412],{"class":15368},[3603,20173,18124],{"class":15364},[3603,20175,20176],{"class":5541},"⚡ Швидкість",[3603,20178,18129],{"class":15364},[3603,20180,3412],{"class":15368},[3603,20182,18114],{"class":15364},[3603,20184,20185,20187,20189,20191,20194,20196,20198],{"class":3605,"line":3673},[3603,20186,18640],{"class":15364},[3603,20188,3353],{"class":15368},[3603,20190,18124],{"class":15364},[3603,20192,20193],{"class":5541},"Статичні файли роздаються напряму з S3 або через CloudFront CDN.",[3603,20195,18129],{"class":15364},[3603,20197,3353],{"class":15368},[3603,20199,18114],{"class":15364},[3603,20201,20202,20204,20206],{"class":3605,"line":3678},[3603,20203,18698],{"class":15364},[3603,20205,4805],{"class":15368},[3603,20207,18114],{"class":15364},[3603,20209,20210,20212,20214,20216,20218,20220],{"class":3605,"line":3684},[3603,20211,18591],{"class":15364},[3603,20213,4805],{"class":15368},[3603,20215,18106],{"class":15371},[3603,20217,6507],{"class":5541},[3603,20219,20163],{"class":5224},[3603,20221,18114],{"class":15364},[3603,20223,20224,20226,20228,20230,20233,20235,20237],{"class":3605,"line":3690},[3603,20225,18640],{"class":15364},[3603,20227,3412],{"class":15368},[3603,20229,18124],{"class":15364},[3603,20231,20232],{"class":5541},"💰 Вартість",[3603,20234,18129],{"class":15364},[3603,20236,3412],{"class":15368},[3603,20238,18114],{"class":15364},[3603,20240,20241,20243,20245,20247,20250,20252,20254],{"class":3605,"line":3696},[3603,20242,18640],{"class":15364},[3603,20244,3353],{"class":15368},[3603,20246,18124],{"class":15364},[3603,20248,20249],{"class":5541},"Зберігання від $0.023\u002FGB. Для типового SPA — менше $1 на місяць.",[3603,20251,18129],{"class":15364},[3603,20253,3353],{"class":15368},[3603,20255,18114],{"class":15364},[3603,20257,20258,20260,20262],{"class":3605,"line":3702},[3603,20259,18698],{"class":15364},[3603,20261,4805],{"class":15368},[3603,20263,18114],{"class":15364},[3603,20265,20266,20268,20270,20272,20274,20276],{"class":3605,"line":3708},[3603,20267,18591],{"class":15364},[3603,20269,4805],{"class":15368},[3603,20271,18106],{"class":15371},[3603,20273,6507],{"class":5541},[3603,20275,20163],{"class":5224},[3603,20277,18114],{"class":15364},[3603,20279,20280,20282,20284,20286,20289,20291,20293],{"class":3605,"line":4667},[3603,20281,18640],{"class":15364},[3603,20283,3412],{"class":15368},[3603,20285,18124],{"class":15364},[3603,20287,20288],{"class":5541},"🔒 Надійність",[3603,20290,18129],{"class":15364},[3603,20292,3412],{"class":15368},[3603,20294,18114],{"class":15364},[3603,20296,20297,20299,20301,20303,20306,20308,20310],{"class":3605,"line":4673},[3603,20298,18640],{"class":15364},[3603,20300,3353],{"class":15368},[3603,20302,18124],{"class":15364},[3603,20304,20305],{"class":5541},"S3 гарантує 99.99% доступність та 11 дев'яток довговічності даних.",[3603,20307,18129],{"class":15364},[3603,20309,3353],{"class":15368},[3603,20311,18114],{"class":15364},[3603,20313,20314,20316,20318],{"class":3605,"line":4679},[3603,20315,18698],{"class":15364},[3603,20317,4805],{"class":15368},[3603,20319,18114],{"class":15364},[3603,20321,20322,20324,20326],{"class":3605,"line":4685},[3603,20323,18707],{"class":15364},[3603,20325,4805],{"class":15368},[3603,20327,18114],{"class":15364},[3603,20329,20330,20332,20334],{"class":3605,"line":4691},[3603,20331,18155],{"class":15364},[3603,20333,4805],{"class":15368},[3603,20335,18114],{"class":15364},[3603,20337,20338],{"class":3605,"line":4696},[3603,20339,18164],{"class":5541},[3603,20341,20342],{"class":3605,"line":4701},[3603,20343,3670],{"class":5541},[3353,20345,17992,20346,5629],{},[3356,20347,20348],{},[3432,20349,20350],{},"src\u002Fpages\u002FAbout.jsx",[3561,20352,20354],{"className":14969,"code":20353,"language":14971,"meta":3569,"style":3569},"\u002F\u002F src\u002Fpages\u002FAbout.jsx\nexport default function About() {\n    return (\n        \u003Cdiv className=\"page\">\n            \u003Ch1>ℹ️ Про застосунок\u003C\u002Fh1>\n            \u003Cp>\n                Навчальний React SPA розгорнутий на \u003Cstrong>AWS S3 Static Website Hosting\u003C\u002Fstrong>.\n            \u003C\u002Fp>\n            \u003Cp>\n                Спробуйте натиснути \u003Ckbd>F5\u003C\u002Fkbd> — завдяки \u003Ccode>Error Document: index.html\u003C\u002Fcode> роутинг працює\n                навіть при прямому переході на цей URL.\n            \u003C\u002Fp>\n            \u003Ch2>Як це працює?\u003C\u002Fh2>\n            \u003Col>\n                \u003Cli>\n                    Браузер запитує \u003Ccode>\u002Fabout\u003C\u002Fcode> безпосередньо у S3\n                \u003C\u002Fli>\n                \u003Cli>\n                    S3 не знаходить файл \u003Ccode>about\u003C\u002Fcode> і мав би повернути 403\u002F404\n                \u003C\u002Fli>\n                \u003Cli>\n                    Але замість помилки S3 повертає \u003Ccode>index.html\u003C\u002Fcode> (Error Document)\n                \u003C\u002Fli>\n                \u003Cli>\n                    React Router зчитує URL і рендерить компонент \u003Ccode>&lt;About \u002F&gt;\u003C\u002Fcode>\n                \u003C\u002Fli>\n            \u003C\u002Fol>\n        \u003C\u002Fdiv>\n    )\n}\n",[3432,20355,20356,20361,20373,20379,20393,20410,20418,20440,20448,20456,20496,20501,20509,20526,20534,20542,20564,20572,20580,20603,20611,20619,20641,20649,20657,20683,20691,20699,20707,20711],{"__ignoreMap":3569},[3603,20357,20358],{"class":3605,"line":3606},[3603,20359,20360],{"class":5214},"\u002F\u002F src\u002Fpages\u002FAbout.jsx\n",[3603,20362,20363,20365,20367,20369,20371],{"class":3605,"line":3612},[3603,20364,18177],{"class":5533},[3603,20366,18542],{"class":5533},[3603,20368,18545],{"class":5243},[3603,20370,18055],{"class":5220},[3603,20372,18089],{"class":5541},[3603,20374,20375,20377],{"class":3605,"line":3618},[3603,20376,14931],{"class":5533},[3603,20378,18096],{"class":5541},[3603,20380,20381,20383,20385,20387,20389,20391],{"class":3605,"line":3624},[3603,20382,18101],{"class":15364},[3603,20384,4805],{"class":15368},[3603,20386,18106],{"class":15371},[3603,20388,6507],{"class":5541},[3603,20390,20077],{"class":5224},[3603,20392,18114],{"class":15364},[3603,20394,20395,20397,20399,20401,20404,20406,20408],{"class":3605,"line":3631},[3603,20396,18119],{"class":15364},[3603,20398,3344],{"class":15368},[3603,20400,18124],{"class":15364},[3603,20402,20403],{"class":5541},"ℹ️ Про застосунок",[3603,20405,18129],{"class":15364},[3603,20407,3344],{"class":15368},[3603,20409,18114],{"class":15364},[3603,20411,20412,20414,20416],{"class":3605,"line":3637},[3603,20413,18119],{"class":15364},[3603,20415,3353],{"class":15368},[3603,20417,18114],{"class":15364},[3603,20419,20420,20423,20425,20427,20429,20432,20434,20436,20438],{"class":3605,"line":3643},[3603,20421,20422],{"class":5541},"                Навчальний React SPA розгорнутий на ",[3603,20424,11250],{"class":15364},[3603,20426,3356],{"class":15368},[3603,20428,18124],{"class":15364},[3603,20430,20431],{"class":5541},"AWS S3 Static Website Hosting",[3603,20433,18129],{"class":15364},[3603,20435,3356],{"class":15368},[3603,20437,18124],{"class":15364},[3603,20439,20127],{"class":5541},[3603,20441,20442,20444,20446],{"class":3605,"line":3649},[3603,20443,18707],{"class":15364},[3603,20445,3353],{"class":15368},[3603,20447,18114],{"class":15364},[3603,20449,20450,20452,20454],{"class":3605,"line":3655},[3603,20451,18119],{"class":15364},[3603,20453,3353],{"class":15368},[3603,20455,18114],{"class":15364},[3603,20457,20458,20461,20463,20465,20467,20469,20471,20473,20475,20478,20480,20482,20484,20487,20489,20491,20493],{"class":3605,"line":3661},[3603,20459,20460],{"class":5541},"                Спробуйте натиснути ",[3603,20462,11250],{"class":15364},[3603,20464,19892],{"class":15368},[3603,20466,18124],{"class":15364},[3603,20468,13155],{"class":5541},[3603,20470,18129],{"class":15364},[3603,20472,19892],{"class":15368},[3603,20474,18124],{"class":15364},[3603,20476,20477],{"class":5541}," — завдяки ",[3603,20479,11250],{"class":15364},[3603,20481,3432],{"class":15368},[3603,20483,18124],{"class":15364},[3603,20485,20486],{"class":5541},"Error Document: index.html",[3603,20488,18129],{"class":15364},[3603,20490,3432],{"class":15368},[3603,20492,18124],{"class":15364},[3603,20494,20495],{"class":5541}," роутинг працює\n",[3603,20497,20498],{"class":3605,"line":3667},[3603,20499,20500],{"class":5541},"                навіть при прямому переході на цей URL.\n",[3603,20502,20503,20505,20507],{"class":3605,"line":3673},[3603,20504,18707],{"class":15364},[3603,20506,3353],{"class":15368},[3603,20508,18114],{"class":15364},[3603,20510,20511,20513,20515,20517,20520,20522,20524],{"class":3605,"line":3678},[3603,20512,18119],{"class":15364},[3603,20514,3348],{"class":15368},[3603,20516,18124],{"class":15364},[3603,20518,20519],{"class":5541},"Як це працює?",[3603,20521,18129],{"class":15364},[3603,20523,3348],{"class":15368},[3603,20525,18114],{"class":15364},[3603,20527,20528,20530,20532],{"class":3605,"line":3684},[3603,20529,18119],{"class":15364},[3603,20531,6382],{"class":15368},[3603,20533,18114],{"class":15364},[3603,20535,20536,20538,20540],{"class":3605,"line":3690},[3603,20537,18591],{"class":15364},[3603,20539,3372],{"class":15368},[3603,20541,18114],{"class":15364},[3603,20543,20544,20547,20549,20551,20553,20555,20557,20559,20561],{"class":3605,"line":3696},[3603,20545,20546],{"class":5541},"                    Браузер запитує ",[3603,20548,11250],{"class":15364},[3603,20550,3432],{"class":15368},[3603,20552,18124],{"class":15364},[3603,20554,13102],{"class":5541},[3603,20556,18129],{"class":15364},[3603,20558,3432],{"class":15368},[3603,20560,18124],{"class":15364},[3603,20562,20563],{"class":5541}," безпосередньо у S3\n",[3603,20565,20566,20568,20570],{"class":3605,"line":3702},[3603,20567,18698],{"class":15364},[3603,20569,3372],{"class":15368},[3603,20571,18114],{"class":15364},[3603,20573,20574,20576,20578],{"class":3605,"line":3708},[3603,20575,18591],{"class":15364},[3603,20577,3372],{"class":15368},[3603,20579,18114],{"class":15364},[3603,20581,20582,20585,20587,20589,20591,20594,20596,20598,20600],{"class":3605,"line":4667},[3603,20583,20584],{"class":5541},"                    S3 не знаходить файл ",[3603,20586,11250],{"class":15364},[3603,20588,3432],{"class":15368},[3603,20590,18124],{"class":15364},[3603,20592,20593],{"class":5541},"about",[3603,20595,18129],{"class":15364},[3603,20597,3432],{"class":15368},[3603,20599,18124],{"class":15364},[3603,20601,20602],{"class":5541}," і мав би повернути 403\u002F404\n",[3603,20604,20605,20607,20609],{"class":3605,"line":4673},[3603,20606,18698],{"class":15364},[3603,20608,3372],{"class":15368},[3603,20610,18114],{"class":15364},[3603,20612,20613,20615,20617],{"class":3605,"line":4679},[3603,20614,18591],{"class":15364},[3603,20616,3372],{"class":15368},[3603,20618,18114],{"class":15364},[3603,20620,20621,20624,20626,20628,20630,20632,20634,20636,20638],{"class":3605,"line":4685},[3603,20622,20623],{"class":5541},"                    Але замість помилки S3 повертає ",[3603,20625,11250],{"class":15364},[3603,20627,3432],{"class":15368},[3603,20629,18124],{"class":15364},[3603,20631,13122],{"class":5541},[3603,20633,18129],{"class":15364},[3603,20635,3432],{"class":15368},[3603,20637,18124],{"class":15364},[3603,20639,20640],{"class":5541}," (Error Document)\n",[3603,20642,20643,20645,20647],{"class":3605,"line":4691},[3603,20644,18698],{"class":15364},[3603,20646,3372],{"class":15368},[3603,20648,18114],{"class":15364},[3603,20650,20651,20653,20655],{"class":3605,"line":4696},[3603,20652,18591],{"class":15364},[3603,20654,3372],{"class":15368},[3603,20656,18114],{"class":15364},[3603,20658,20659,20662,20664,20666,20668,20671,20674,20677,20679,20681],{"class":3605,"line":4701},[3603,20660,20661],{"class":5541},"                    React Router зчитує URL і рендерить компонент ",[3603,20663,11250],{"class":15364},[3603,20665,3432],{"class":15368},[3603,20667,18124],{"class":15364},[3603,20669,20670],{"class":5243},"&lt;",[3603,20672,20673],{"class":5541},"About \u002F",[3603,20675,20676],{"class":5243},"&gt;",[3603,20678,18129],{"class":15364},[3603,20680,3432],{"class":15368},[3603,20682,18114],{"class":15364},[3603,20684,20685,20687,20689],{"class":3605,"line":4707},[3603,20686,18698],{"class":15364},[3603,20688,3372],{"class":15368},[3603,20690,18114],{"class":15364},[3603,20692,20693,20695,20697],{"class":3605,"line":4713},[3603,20694,18707],{"class":15364},[3603,20696,6382],{"class":15368},[3603,20698,18114],{"class":15364},[3603,20700,20701,20703,20705],{"class":3605,"line":4719},[3603,20702,18155],{"class":15364},[3603,20704,4805],{"class":15368},[3603,20706,18114],{"class":15364},[3603,20708,20709],{"class":3605,"line":4724},[3603,20710,18164],{"class":5541},[3603,20712,20713],{"class":3605,"line":4730},[3603,20714,3670],{"class":5541},[3405,20716],{},[3412,20718,20720],{"id":20719},"крок-2-локальний-запуск-та-перевірка","Крок 2: Локальний запуск та перевірка",[3561,20722,20724],{"className":5205,"code":20723,"language":5207,"meta":3569,"style":3569},"# Vite dev server — значно швидший за webpack (HMR ~50мс замість ~2с у CRA)\nnpm run dev\n# Відкрийте http:\u002F\u002Flocalhost:5173 у браузері\n",[3432,20725,20726,20731,20741],{"__ignoreMap":3569},[3603,20727,20728],{"class":3605,"line":3606},[3603,20729,20730],{"class":5214},"# Vite dev server — значно швидший за webpack (HMR ~50мс замість ~2с у CRA)\n",[3603,20732,20733,20735,20738],{"class":3605,"line":3612},[3603,20734,17883],{"class":5220},[3603,20736,20737],{"class":5224}," run",[3603,20739,20740],{"class":5224}," dev\n",[3603,20742,20743],{"class":3605,"line":3618},[3603,20744,20745],{"class":5214},"# Відкрийте http:\u002F\u002Flocalhost:5173 у браузері\n",[4801,20747,20748,20756,20759,20771,20774,20782,20786],{"title":17980},[4805,20749,20751,4841,20754],{"className":20750},[3605],[3603,20752,7030],{"className":20753},[7029],[3356,20755,17980],{},[4805,20757],{"className":20758},[3605],[4805,20760,7040,20762,20766,20767],{"className":20761},[3605],[3603,20763,20765],{"className":20764},[4811],"VITE v5.4.0","  ready in ",[3603,20768,20770],{"className":20769},[7085],"213 ms",[4805,20772],{"className":20773},[3605],[4805,20775,20777,20778],{"className":20776},[3605],"  ➜  Local:   ",[3603,20779,20781],{"className":20780},[4811],"http:\u002F\u002Flocalhost:5173\u002F",[4805,20783,20785],{"className":20784},[3605],"  ➜  Network: http:\u002F\u002F192.168.1.10:5173\u002F",[4805,20787,20789,20790,20794],{"className":20788},[3605],"  ➜  press ",[3603,20791,20793],{"className":20792},[7085],"h + enter"," to show help",[3353,20796,20797],{},"Що перевірити перед деплоєм:",[6382,20799,20800,20806,20815,20821],{},[3372,20801,20802,20805],{},[3432,20803,20804],{},"http:\u002F\u002Flocalhost:5173"," — відображається Home з картками",[3372,20807,20808,20809,20811,20812,20814],{},"Клік ",[3356,20810,18687],{}," у навбарі — URL змінюється на ",[3432,20813,13102],{}," без перезавантаження",[3372,20816,20817,20818,20820],{},"Оновлення на ",[3432,20819,13102],{}," (F5) — dev-сервер коректно обробляє маршрут",[3372,20822,20823,20824,20827],{},"Перехід на ",[3432,20825,20826],{},"\u002Fxyz"," — відображається компонент 404",[3397,20829,20830,20831,20833,20834,3453],{},"Локально React Router працює завдяки Vite dev server, який перенаправляє всі 404 на ",[3432,20832,13122],{},". На S3 цю роль виконуватиме ",[3432,20835,20486],{},[3405,20837],{},[3412,20839,20841],{"id":20840},"крок-3-створення-s3-bucket","Крок 3: Створення S3 Bucket",[5197,20843,20844,20904],{},[5200,20845,20846],{"label":6375},[6382,20847,20848,20856,20868,20876,20884,20894,20900],{},[3372,20849,6386,20850,20852,20853],{},[3356,20851,5544],{}," у AWS Console → ",[3356,20854,20855],{},"Create bucket",[3372,20857,20858,4841,20861,4841,20864],{},[3356,20859,20860],{},"Bucket name:",[3432,20862,20863],{},"my-react-app-2024",[20865,20866,20867],"em",{},"(назва глобально унікальна, тому додайте рік або ваше ім'я)",[3372,20869,20870,4841,20873],{},[3356,20871,20872],{},"AWS Region:",[3432,20874,20875],{},"eu-central-1 (Europe Frankfurt)",[3372,20877,20878,4841,20881],{},[3356,20879,20880],{},"Object Ownership:",[3432,20882,20883],{},"ACLs disabled (recommended)",[3372,20885,20886,20889,20890,20893],{},[3356,20887,20888],{},"Block Public Access:"," для публічного сайту знімаємо ",[3356,20891,20892],{},"Block all public access"," → підтвердіть попередження",[3372,20895,20896,20899],{},[3356,20897,20898],{},"Versioning:"," Enable (рекомендовано — щоб відкатити деплой)",[3372,20901,20902],{},[3356,20903,20855],{},[5200,20905,20906],{"label":6494},[3561,20907,20909],{"className":5205,"code":20908,"language":5207,"meta":3569,"style":3569},"BUCKET=\"my-react-app-2024\"\nREGION=\"eu-central-1\"\n\n# Створити bucket (eu-central-1 потребує LocationConstraint)\naws s3api create-bucket \\\n    --bucket $BUCKET \\\n    --region $REGION \\\n    --create-bucket-configuration LocationConstraint=$REGION\n\n# Увімкнути версіонування\naws s3api put-bucket-versioning \\\n    --bucket $BUCKET \\\n    --versioning-configuration Status=Enabled\n\n# Дозволити публічний доступ (необхідно для static website)\naws s3api delete-public-access-block --bucket $BUCKET\n",[3432,20910,20911,20920,20928,20932,20937,20948,20957,20966,20977,20981,20985,20995,21003,21010,21014,21019],{"__ignoreMap":3569},[3603,20912,20913,20915,20917],{"class":3605,"line":3606},[3603,20914,6504],{"class":5576},[3603,20916,6507],{"class":5541},[3603,20918,20919],{"class":5224},"\"my-react-app-2024\"\n",[3603,20921,20922,20924,20926],{"class":3605,"line":3612},[3603,20923,6515],{"class":5576},[3603,20925,6507],{"class":5541},[3603,20927,6520],{"class":5224},[3603,20929,20930],{"class":3605,"line":3618},[3603,20931,3628],{"emptyLinePlaceholder":3627},[3603,20933,20934],{"class":3605,"line":3624},[3603,20935,20936],{"class":5214},"# Створити bucket (eu-central-1 потребує LocationConstraint)\n",[3603,20938,20939,20941,20943,20946],{"class":3605,"line":3631},[3603,20940,5221],{"class":5220},[3603,20942,5365],{"class":5224},[3603,20944,20945],{"class":5224}," create-bucket",[3603,20947,5238],{"class":5237},[3603,20949,20950,20952,20955],{"class":3605,"line":3637},[3603,20951,6545],{"class":5243},[3603,20953,20954],{"class":5576}," $BUCKET",[3603,20956,5238],{"class":5237},[3603,20958,20959,20961,20964],{"class":3605,"line":3643},[3603,20960,6571],{"class":5243},[3603,20962,20963],{"class":5576}," $REGION",[3603,20965,5238],{"class":5237},[3603,20967,20968,20971,20974],{"class":3605,"line":3649},[3603,20969,20970],{"class":5243},"    --create-bucket-configuration",[3603,20972,20973],{"class":5224}," LocationConstraint=",[3603,20975,20976],{"class":5576},"$REGION\n",[3603,20978,20979],{"class":3605,"line":3655},[3603,20980,3628],{"emptyLinePlaceholder":3627},[3603,20982,20983],{"class":3605,"line":3661},[3603,20984,6529],{"class":5214},[3603,20986,20987,20989,20991,20993],{"class":3605,"line":3667},[3603,20988,5221],{"class":5220},[3603,20990,5365],{"class":5224},[3603,20992,6538],{"class":5224},[3603,20994,5238],{"class":5237},[3603,20996,20997,20999,21001],{"class":3605,"line":3673},[3603,20998,6545],{"class":5243},[3603,21000,20954],{"class":5576},[3603,21002,5238],{"class":5237},[3603,21004,21005,21007],{"class":3605,"line":3678},[3603,21006,6561],{"class":5243},[3603,21008,21009],{"class":5224}," Status=Enabled\n",[3603,21011,21012],{"class":3605,"line":3684},[3603,21013,3628],{"emptyLinePlaceholder":3627},[3603,21015,21016],{"class":3605,"line":3690},[3603,21017,21018],{"class":5214},"# Дозволити публічний доступ (необхідно для static website)\n",[3603,21020,21021,21023,21025,21028,21031],{"class":3605,"line":3696},[3603,21022,5221],{"class":5220},[3603,21024,5365],{"class":5224},[3603,21026,21027],{"class":5224}," delete-public-access-block",[3603,21029,21030],{"class":5243}," --bucket",[3603,21032,21033],{"class":5576}," $BUCKET\n",[3405,21035],{},[3412,21037,21039],{"id":21038},"крок-4-bucket-policy-для-публічного-читання","Крок 4: Bucket Policy для публічного читання",[3561,21041,21043],{"className":5205,"code":21042,"language":5207,"meta":3569,"style":3569},"# Зберегти у файл policy.json\ncat > \u002Ftmp\u002Freact-bucket-policy.json \u003C\u003C EOF\n{\n    \"Version\": \"2012-10-17\",\n    \"Statement\": [\n        {\n            \"Sid\": \"PublicReadGetObject\",\n            \"Effect\": \"Allow\",\n            \"Principal\": \"*\",\n            \"Action\": \"s3:GetObject\",\n            \"Resource\": \"arn:aws:s3:::my-react-app-2024\u002F*\"\n        }\n    ]\n}\nEOF\n\n# Застосувати Policy\n# ЗАМІНІТЬ my-react-app-2024 на ваше реальне ім'я bucket\naws s3api put-bucket-policy \\\n    --bucket my-react-app-2024 \\\n    --policy file:\u002F\u002F\u002Ftmp\u002Freact-bucket-policy.json \\\n    --region eu-central-1\n",[3432,21044,21045,21050,21063,21067,21071,21075,21079,21084,21088,21093,21098,21103,21107,21111,21115,21119,21123,21128,21133,21143,21152,21161],{"__ignoreMap":3569},[3603,21046,21047],{"class":3605,"line":3606},[3603,21048,21049],{"class":5214},"# Зберегти у файл policy.json\n",[3603,21051,21052,21054,21056,21059,21061],{"class":3605,"line":3612},[3603,21053,7831],{"class":5220},[3603,21055,7834],{"class":5541},[3603,21057,21058],{"class":5224},"\u002Ftmp\u002Freact-bucket-policy.json",[3603,21060,7840],{"class":5541},[3603,21062,7945],{"class":5541},[3603,21064,21065],{"class":3605,"line":3618},[3603,21066,5591],{"class":5224},[3603,21068,21069],{"class":3605,"line":3624},[3603,21070,9702],{"class":5224},[3603,21072,21073],{"class":3605,"line":3631},[3603,21074,9707],{"class":5224},[3603,21076,21077],{"class":3605,"line":3637},[3603,21078,5873],{"class":5224},[3603,21080,21081],{"class":3605,"line":3643},[3603,21082,21083],{"class":5224},"            \"Sid\": \"PublicReadGetObject\",\n",[3603,21085,21086],{"class":3605,"line":3649},[3603,21087,9721],{"class":5224},[3603,21089,21090],{"class":3605,"line":3655},[3603,21091,21092],{"class":5224},"            \"Principal\": \"*\",\n",[3603,21094,21095],{"class":3605,"line":3661},[3603,21096,21097],{"class":5224},"            \"Action\": \"s3:GetObject\",\n",[3603,21099,21100],{"class":3605,"line":3667},[3603,21101,21102],{"class":5224},"            \"Resource\": \"arn:aws:s3:::my-react-app-2024\u002F*\"\n",[3603,21104,21105],{"class":3605,"line":3673},[3603,21106,6050],{"class":5224},[3603,21108,21109],{"class":3605,"line":3678},[3603,21110,6055],{"class":5224},[3603,21112,21113],{"class":3605,"line":3684},[3603,21114,3670],{"class":5224},[3603,21116,21117],{"class":3605,"line":3690},[3603,21118,7945],{"class":5541},[3603,21120,21121],{"class":3605,"line":3696},[3603,21122,3628],{"emptyLinePlaceholder":3627},[3603,21124,21125],{"class":3605,"line":3702},[3603,21126,21127],{"class":5214},"# Застосувати Policy\n",[3603,21129,21130],{"class":3605,"line":3708},[3603,21131,21132],{"class":5214},"# ЗАМІНІТЬ my-react-app-2024 на ваше реальне ім'я bucket\n",[3603,21134,21135,21137,21139,21141],{"class":3605,"line":4667},[3603,21136,5221],{"class":5220},[3603,21138,5365],{"class":5224},[3603,21140,9789],{"class":5224},[3603,21142,5238],{"class":5237},[3603,21144,21145,21147,21150],{"class":3605,"line":4673},[3603,21146,6545],{"class":5243},[3603,21148,21149],{"class":5224}," my-react-app-2024",[3603,21151,5238],{"class":5237},[3603,21153,21154,21156,21159],{"class":3605,"line":4679},[3603,21155,9808],{"class":5243},[3603,21157,21158],{"class":5224}," file:\u002F\u002F\u002Ftmp\u002Freact-bucket-policy.json",[3603,21160,5238],{"class":5237},[3603,21162,21163,21165],{"class":3605,"line":4685},[3603,21164,6571],{"class":5243},[3603,21166,13426],{"class":5224},[3405,21168],{},[3412,21170,21172],{"id":21171},"крок-5-налаштування-static-website-hosting","Крок 5: Налаштування Static Website Hosting",[5197,21174,21175,21223],{},[5200,21176,21177],{"label":6375},[6382,21178,21179,21184,21191,21195,21202,21212,21216],{},[3372,21180,21181,21182],{},"S3 → ваш bucket → вкладка ",[3356,21183,6392],{},[3372,21185,9593,21186,3976,21189],{},[3356,21187,21188],{},"Static website hosting",[3356,21190,6401],{},[3372,21192,21193],{},[3356,21194,6407],{},[3372,21196,21197,4841,21200],{},[3356,21198,21199],{},"Index document:",[3432,21201,13122],{},[3372,21203,21204,4841,21207,4841,21209],{},[3356,21205,21206],{},"Error document:",[3432,21208,13122],{},[20865,21210,21211],{},"(критично для React Router!)",[3372,21213,21214],{},[3356,21215,6410],{},[3372,21217,21218,21219,21222],{},"Скопіюйте ",[3356,21220,21221],{},"Bucket website endpoint"," — знадобиться для тестування",[5200,21224,21225],{"label":6494},[3561,21226,21228],{"className":5205,"code":21227,"language":5207,"meta":3569,"style":3569},"aws s3api put-bucket-website \\\n    --bucket my-react-app-2024 \\\n    --website-configuration '{\n        \"IndexDocument\": {\"Suffix\": \"index.html\"},\n        \"ErrorDocument\": {\"Key\": \"index.html\"}\n    }' \\\n    --region eu-central-1\n\n# URL сайту:\necho \"Website URL: http:\u002F\u002Fmy-react-app-2024.s3-website.eu-central-1.amazonaws.com\"\n",[3432,21229,21230,21240,21248,21254,21258,21262,21268,21274,21278,21283],{"__ignoreMap":3569},[3603,21231,21232,21234,21236,21238],{"class":3605,"line":3606},[3603,21233,5221],{"class":5220},[3603,21235,5365],{"class":5224},[3603,21237,13385],{"class":5224},[3603,21239,5238],{"class":5237},[3603,21241,21242,21244,21246],{"class":3605,"line":3612},[3603,21243,6545],{"class":5243},[3603,21245,21149],{"class":5224},[3603,21247,5238],{"class":5237},[3603,21249,21250,21252],{"class":3605,"line":3618},[3603,21251,13401],{"class":5243},[3603,21253,5442],{"class":5224},[3603,21255,21256],{"class":3605,"line":3624},[3603,21257,13408],{"class":5224},[3603,21259,21260],{"class":3605,"line":3631},[3603,21261,13413],{"class":5224},[3603,21263,21264,21266],{"class":3605,"line":3637},[3603,21265,11638],{"class":5224},[3603,21267,5238],{"class":5237},[3603,21269,21270,21272],{"class":3605,"line":3643},[3603,21271,6571],{"class":5243},[3603,21273,13426],{"class":5224},[3603,21275,21276],{"class":3605,"line":3649},[3603,21277,3628],{"emptyLinePlaceholder":3627},[3603,21279,21280],{"class":3605,"line":3655},[3603,21281,21282],{"class":5214},"# URL сайту:\n",[3603,21284,21285,21287],{"class":3605,"line":3661},[3603,21286,6935],{"class":5220},[3603,21288,21289],{"class":5224}," \"Website URL: http:\u002F\u002Fmy-react-app-2024.s3-website.eu-central-1.amazonaws.com\"\n",[3405,21291],{},[3412,21293,21295],{"id":21294},"крок-6-cors-конфігурація","Крок 6: CORS конфігурація",[3561,21297,21299],{"className":5205,"code":21298,"language":5207,"meta":3569,"style":3569},"cat > \u002Ftmp\u002Fcors.json \u003C\u003C 'EOF'\n[\n    {\n        \"AllowedHeaders\": [\"*\"],\n        \"AllowedMethods\": [\"GET\", \"PUT\", \"POST\", \"DELETE\", \"HEAD\"],\n        \"AllowedOrigins\": [\n            \"http:\u002F\u002Fmy-react-app-2024.s3-website.eu-central-1.amazonaws.com\",\n            \"http:\u002F\u002Flocalhost:3000\",\n            \"https:\u002F\u002Fyourdomain.com\"\n        ],\n        \"ExposeHeaders\": [\"ETag\"],\n        \"MaxAgeSeconds\": 3000\n    }\n]\nEOF\n\n# ЗАМІНІТЬ my-react-app-2024 на ваш bucket\naws s3api put-bucket-cors \\\n    --bucket my-react-app-2024 \\\n    --cors-configuration file:\u002F\u002F\u002Ftmp\u002Fcors.json \\\n    --region eu-central-1\n",[3432,21300,21301,21314,21318,21322,21327,21332,21337,21342,21347,21352,21356,21361,21366,21370,21374,21378,21382,21387,21397,21405,21414],{"__ignoreMap":3569},[3603,21302,21303,21305,21307,21310,21312],{"class":3605,"line":3606},[3603,21304,7831],{"class":5220},[3603,21306,7834],{"class":5541},[3603,21308,21309],{"class":5224},"\u002Ftmp\u002Fcors.json",[3603,21311,7840],{"class":5541},[3603,21313,7843],{"class":5541},[3603,21315,21316],{"class":3605,"line":3612},[3603,21317,13813],{"class":5224},[3603,21319,21320],{"class":3605,"line":3618},[3603,21321,11086],{"class":5224},[3603,21323,21324],{"class":3605,"line":3624},[3603,21325,21326],{"class":5224},"        \"AllowedHeaders\": [\"*\"],\n",[3603,21328,21329],{"class":3605,"line":3631},[3603,21330,21331],{"class":5224},"        \"AllowedMethods\": [\"GET\", \"PUT\", \"POST\", \"DELETE\", \"HEAD\"],\n",[3603,21333,21334],{"class":3605,"line":3637},[3603,21335,21336],{"class":5224},"        \"AllowedOrigins\": [\n",[3603,21338,21339],{"class":3605,"line":3643},[3603,21340,21341],{"class":5224},"            \"http:\u002F\u002Fmy-react-app-2024.s3-website.eu-central-1.amazonaws.com\",\n",[3603,21343,21344],{"class":3605,"line":3649},[3603,21345,21346],{"class":5224},"            \"http:\u002F\u002Flocalhost:3000\",\n",[3603,21348,21349],{"class":3605,"line":3655},[3603,21350,21351],{"class":5224},"            \"https:\u002F\u002Fyourdomain.com\"\n",[3603,21353,21354],{"class":3605,"line":3661},[3603,21355,18273],{"class":5224},[3603,21357,21358],{"class":3605,"line":3667},[3603,21359,21360],{"class":5224},"        \"ExposeHeaders\": [\"ETag\"],\n",[3603,21362,21363],{"class":3605,"line":3673},[3603,21364,21365],{"class":5224},"        \"MaxAgeSeconds\": 3000\n",[3603,21367,21368],{"class":3605,"line":3678},[3603,21369,3664],{"class":5224},[3603,21371,21372],{"class":3605,"line":3684},[3603,21373,13913],{"class":5224},[3603,21375,21376],{"class":3605,"line":3690},[3603,21377,7945],{"class":5541},[3603,21379,21380],{"class":3605,"line":3696},[3603,21381,3628],{"emptyLinePlaceholder":3627},[3603,21383,21384],{"class":3605,"line":3702},[3603,21385,21386],{"class":5214},"# ЗАМІНІТЬ my-react-app-2024 на ваш bucket\n",[3603,21388,21389,21391,21393,21395],{"class":3605,"line":3708},[3603,21390,5221],{"class":5220},[3603,21392,5365],{"class":5224},[3603,21394,13932],{"class":5224},[3603,21396,5238],{"class":5237},[3603,21398,21399,21401,21403],{"class":3605,"line":4667},[3603,21400,6545],{"class":5243},[3603,21402,21149],{"class":5224},[3603,21404,5238],{"class":5237},[3603,21406,21407,21409,21412],{"class":3605,"line":4673},[3603,21408,13948],{"class":5243},[3603,21410,21411],{"class":5224}," file:\u002F\u002F\u002Ftmp\u002Fcors.json",[3603,21413,5238],{"class":5237},[3603,21415,21416,21418],{"class":3605,"line":4679},[3603,21417,6571],{"class":5243},[3603,21419,13426],{"class":5224},[3405,21421],{},[3412,21423,21425],{"id":21424},"крок-7-збірка-та-деплой-react-застосунку","Крок 7: Збірка та деплой React застосунку",[3353,21427,21428],{},[6142,21429],{"alt":21430,"className":21431,"src":21432},"Amazon S3 console objects deployment screenshot",[6146],"\u002Fimages\u002Faws\u002Fs3\u002F07.gif",[3353,21434,21435],{},"Перейдіть у директорію проєкту (створили у Кроці 1):",[3561,21437,21439],{"className":5205,"code":21438,"language":5207,"meta":3569,"style":3569},"# Перейдіть у директорію проєкту\ncd my-react-app\n\n# Зберіть production build\n# Vite (на відміну від CRA) кладе результат у dist\u002F, а не build\u002F\nnpm run build\n",[3432,21440,21441,21446,21452,21456,21461,21466],{"__ignoreMap":3569},[3603,21442,21443],{"class":3605,"line":3606},[3603,21444,21445],{"class":5214},"# Перейдіть у директорію проєкту\n",[3603,21447,21448,21450],{"class":3605,"line":3612},[3603,21449,17905],{"class":5220},[3603,21451,17908],{"class":5224},[3603,21453,21454],{"class":3605,"line":3618},[3603,21455,3628],{"emptyLinePlaceholder":3627},[3603,21457,21458],{"class":3605,"line":3624},[3603,21459,21460],{"class":5214},"# Зберіть production build\n",[3603,21462,21463],{"class":3605,"line":3631},[3603,21464,21465],{"class":5214},"# Vite (на відміну від CRA) кладе результат у dist\u002F, а не build\u002F\n",[3603,21467,21468,21470,21472],{"class":3605,"line":3637},[3603,21469,17883],{"class":5220},[3603,21471,20737],{"class":5224},[3603,21473,21474],{"class":5224}," build\n",[4801,21476,21478,21486,21489,21493,21497,21500,21504,21508,21515,21518],{"title":21477},"npm run build",[4805,21479,21481,4841,21484],{"className":21480},[3605],[3603,21482,7030],{"className":21483},[7029],[3356,21485,21477],{},[4805,21487],{"className":21488},[3605],[4805,21490,21492],{"className":21491},[3605],"vite v5.4.0 building for production...",[4805,21494,21496],{"className":21495},[3605],"✓ 34 modules transformed.",[4805,21498],{"className":21499},[3605],[4805,21501,21503],{"className":21502},[3605],"dist\u002Findex.html                        0.46 kB │ gzip:  0.30 kB",[4805,21505,21507],{"className":21506},[3605],"dist\u002Fassets\u002Findex-C2PyeNYV.css         1.50 kB │ gzip:  0.75 kB",[4805,21509,7040,21511],{"className":21510},[3605],[3603,21512,21514],{"className":21513},[4811],"dist\u002Fassets\u002Findex-BNMJvBVp.js        142.62 kB │ gzip: 45.73 kB",[4805,21516],{"className":21517},[3605],[4805,21519,21521,21522],{"className":21520},[3605],"✓ built in ",[3603,21523,21525],{"className":21524},[7085],"891ms",[3561,21527,21529],{"className":5205,"code":21528,"language":5207,"meta":3569,"style":3569},"# Завантажте dist\u002F папку у S3 (Vite кладе build у dist\u002F, а не build\u002F)\n# aws s3 sync — синхронізація директорії з S3 (завантажує лише змінені файли)\n# --delete — видаляє файли з S3, яких немає локально (прибирає старий deploy)\n# ЗАМІНІТЬ my-react-app-2024 на ваш bucket\naws s3 sync .\u002Fdist s3:\u002F\u002Fmy-react-app-2024\u002F \\\n    --delete \\\n    --region eu-central-1\n\n# Встановити правильний Content-Type для HTML файлів (важливо!)\naws s3 cp s3:\u002F\u002Fmy-react-app-2024\u002Findex.html s3:\u002F\u002Fmy-react-app-2024\u002Findex.html \\\n    --metadata-directive REPLACE \\\n    --content-type \"text\u002Fhtml\" \\\n    --cache-control \"no-cache, no-store, must-revalidate\" \\\n    --region eu-central-1\n",[3432,21530,21531,21536,21541,21546,21550,21566,21573,21579,21583,21588,21603,21613,21623,21633],{"__ignoreMap":3569},[3603,21532,21533],{"class":3605,"line":3606},[3603,21534,21535],{"class":5214},"# Завантажте dist\u002F папку у S3 (Vite кладе build у dist\u002F, а не build\u002F)\n",[3603,21537,21538],{"class":3605,"line":3612},[3603,21539,21540],{"class":5214},"# aws s3 sync — синхронізація директорії з S3 (завантажує лише змінені файли)\n",[3603,21542,21543],{"class":3605,"line":3618},[3603,21544,21545],{"class":5214},"# --delete — видаляє файли з S3, яких немає локально (прибирає старий deploy)\n",[3603,21547,21548],{"class":3605,"line":3624},[3603,21549,21386],{"class":5214},[3603,21551,21552,21554,21556,21558,21561,21564],{"class":3605,"line":3631},[3603,21553,5221],{"class":5220},[3603,21555,5225],{"class":5224},[3603,21557,5284],{"class":5224},[3603,21559,21560],{"class":5224}," .\u002Fdist",[3603,21562,21563],{"class":5224}," s3:\u002F\u002Fmy-react-app-2024\u002F",[3603,21565,5238],{"class":5237},[3603,21567,21568,21571],{"class":3605,"line":3637},[3603,21569,21570],{"class":5243},"    --delete",[3603,21572,5238],{"class":5237},[3603,21574,21575,21577],{"class":3605,"line":3643},[3603,21576,6571],{"class":5243},[3603,21578,13426],{"class":5224},[3603,21580,21581],{"class":3605,"line":3649},[3603,21582,3628],{"emptyLinePlaceholder":3627},[3603,21584,21585],{"class":3605,"line":3655},[3603,21586,21587],{"class":5214},"# Встановити правильний Content-Type для HTML файлів (важливо!)\n",[3603,21589,21590,21592,21594,21596,21599,21601],{"class":3605,"line":3661},[3603,21591,5221],{"class":5220},[3603,21593,5225],{"class":5224},[3603,21595,5228],{"class":5224},[3603,21597,21598],{"class":5224}," s3:\u002F\u002Fmy-react-app-2024\u002Findex.html",[3603,21600,21598],{"class":5224},[3603,21602,5238],{"class":5237},[3603,21604,21605,21608,21611],{"class":3605,"line":3667},[3603,21606,21607],{"class":5243},"    --metadata-directive",[3603,21609,21610],{"class":5224}," REPLACE",[3603,21612,5238],{"class":5237},[3603,21614,21615,21618,21621],{"class":3605,"line":3673},[3603,21616,21617],{"class":5243},"    --content-type",[3603,21619,21620],{"class":5224}," \"text\u002Fhtml\"",[3603,21622,5238],{"class":5237},[3603,21624,21625,21628,21631],{"class":3605,"line":3678},[3603,21626,21627],{"class":5243},"    --cache-control",[3603,21629,21630],{"class":5224}," \"no-cache, no-store, must-revalidate\"",[3603,21632,5238],{"class":5237},[3603,21634,21635,21637],{"class":3605,"line":3684},[3603,21636,6571],{"class":5243},[3603,21638,13426],{"class":5224},[4801,21640,21642,21651,21655,21659,21663],{"title":21641},"aws s3 sync результат",[4805,21643,21645,4841,21648],{"className":21644},[3605],[3603,21646,7030],{"className":21647},[7029],[3356,21649,21650],{},"aws s3 sync .\u002Fdist s3:\u002F\u002Fmy-react-app-2024\u002F --delete --region eu-central-1",[4805,21652,21654],{"className":21653},[3605],"upload: build\u002Findex.html to s3:\u002F\u002Fmy-react-app-2024\u002Findex.html",[4805,21656,21658],{"className":21657},[3605],"upload: build\u002Fstatic\u002Fjs\u002Fmain.abc123.js to s3:\u002F\u002Fmy-react-app-2024\u002Fstatic\u002Fjs\u002Fmain.abc123.js",[4805,21660,21662],{"className":21661},[3605],"upload: build\u002Fstatic\u002Fcss\u002Fmain.def456.css to s3:\u002F\u002Fmy-react-app-2024\u002Fstatic\u002Fcss\u002Fmain.def456.css",[4805,21664,21666],{"className":21665},[3605],[3603,21667,21669],{"className":21668},[4811],"upload: build\u002Ffavicon.ico to s3:\u002F\u002Fmy-react-app-2024\u002Ffavicon.ico",[3353,21671,21672,21673],{},"Відкрийте ваш сайт у браузері: ",[3432,21674,21675],{},"http:\u002F\u002Fmy-react-app-2024.s3-website.eu-central-1.amazonaws.com",[3405,21677],{},[3412,21679,21681],{"id":21680},"крок-8-перевірка-у-браузері","Крок 8: Перевірка у браузері",[3353,21683,21684],{},[3356,21685,21686],{},"URL вашого сайту:",[3561,21688,21691],{"className":21689,"code":21690,"language":3566},[3564],"http:\u002F\u002Fmy-react-app-2024.s3-website.eu-central-1.amazonaws.com\n",[3432,21692,21690],{"__ignoreMap":3569},[3353,21694,21695],{},"Сценарії для тестування:",[6382,21697,21698,21704,21716,21730],{},[3372,21699,21700,21703],{},[3356,21701,21702],{},"Home сторінка"," — відкрийте URL → відображаються картки з інформацією про S3",[3372,21705,21706,21709,21710,21712,21713,21715],{},[3356,21707,21708],{},"Навігація через UI"," — клікніть ",[3356,21711,18687],{}," у навбарі → URL змінюється на ",[3432,21714,13102],{}," без перезавантаження сторінки",[3372,21717,21718,21721,21722,21725,21726,21729],{},[3356,21719,21720],{},"Прямий перехід (критичний тест)"," — вставте в адресний рядок ",[3432,21723,21724],{},"...\u002Fabout"," та натисніть Enter → має відобразитись ",[3356,21727,21728],{},"About сторінка",", а не XML-помилка S3",[3372,21731,21732,21735,21736,21738,21739,21741],{},[3356,21733,21734],{},"Оновлення сторінки"," — натисніть F5 на ",[3432,21737,13102],{}," → завдяки ",[3432,21740,20486],{}," роутинг зберігається",[3879,21743,21744,21745,21747,21748,21751,21752,21755,21756,21759,21760,21762,21763,3453],{},"Якщо при переході на ",[3432,21746,13102],{}," ви бачите XML на кшталт ",[3432,21749,21750],{},"\u003CError>\u003CCode>NoSuchKey\u003C\u002FCode>..."," — значить ",[3356,21753,21754],{},"Error Document"," не налаштований або вказаний неправильно. Поверніться до Кроку 5 і переконайтесь, що і ",[3356,21757,21758],{},"Index document",", і ",[3356,21761,13262],{}," вказують на ",[3432,21764,13122],{},[3353,21766,21767],{},"Для швидкої перевірки через термінал:",[3561,21769,21771],{"className":5205,"code":21770,"language":5207,"meta":3569,"style":3569},"# Перевіряємо що обидва маршрути повертають 200 OK з index.html\ncurl -sI http:\u002F\u002Fmy-react-app-2024.s3-website.eu-central-1.amazonaws.com | head -1\ncurl -sI http:\u002F\u002Fmy-react-app-2024.s3-website.eu-central-1.amazonaws.com\u002Fabout | head -1\n",[3432,21772,21773,21778,21797],{"__ignoreMap":3569},[3603,21774,21775],{"class":3605,"line":3606},[3603,21776,21777],{"class":5214},"# Перевіряємо що обидва маршрути повертають 200 OK з index.html\n",[3603,21779,21780,21783,21786,21789,21791,21794],{"class":3605,"line":3612},[3603,21781,21782],{"class":5220},"curl",[3603,21784,21785],{"class":5243}," -sI",[3603,21787,21788],{"class":5224}," http:\u002F\u002Fmy-react-app-2024.s3-website.eu-central-1.amazonaws.com",[3603,21790,9881],{"class":5541},[3603,21792,21793],{"class":5220},"head",[3603,21795,21796],{"class":5243}," -1\n",[3603,21798,21799,21801,21803,21806,21808,21810],{"class":3605,"line":3618},[3603,21800,21782],{"class":5220},[3603,21802,21785],{"class":5243},[3603,21804,21805],{"class":5224}," http:\u002F\u002Fmy-react-app-2024.s3-website.eu-central-1.amazonaws.com\u002Fabout",[3603,21807,9881],{"class":5541},[3603,21809,21793],{"class":5220},[3603,21811,21796],{"class":5243},[4801,21813,21815,21824,21831,21839],{"title":21814},"curl перевірка роутингу",[4805,21816,21818,4841,21821],{"className":21817},[3605],[3603,21819,7030],{"className":21820},[7029],[3356,21822,21823],{},"curl -sI http:\u002F\u002Fmy-react-app-2024.s3-website.eu-central-1.amazonaws.com\u002Fabout | head -1",[4805,21825,21827],{"className":21826},[3605],[3603,21828,21830],{"className":21829},[4811],"HTTP\u002F1.1 200 OK",[4805,21832,21834],{"className":21833},[3605],[3603,21835,21838],{"className":21836},[21837],"opacity-60","# 200 (не 404\u002F403) — Error Document спрацював, index.html повернуто",[4805,21840,21842],{"className":21841},[3605],[3603,21843,21845],{"className":21844},[21837],"# React Router на клієнті далі рендерить \u002Fabout компонент",[3405,21847],{},[3412,21849,21851],{"id":21850},"крок-9-lifecycle-policy-для-старих-версій","Крок 9: Lifecycle Policy для старих версій",[3353,21853,21854],{},"Оскільки увімкнено Versioning, при кожному деплої накопичуються старі версії. Налаштуємо автоматичне очищення:",[3561,21856,21858],{"className":5205,"code":21857,"language":5207,"meta":3569,"style":3569},"cat > \u002Ftmp\u002Flifecycle.json \u003C\u003C 'EOF'\n{\n    \"Rules\": [\n        {\n            \"ID\": \"CleanOldVersions\",\n            \"Status\": \"Enabled\",\n            \"Filter\": {},\n            \"NoncurrentVersionExpiration\": {\n                \"NoncurrentDays\": 30,\n                \"NewerNoncurrentVersions\": 5\n            },\n            \"AbortIncompleteMultipartUpload\": {\n                \"DaysAfterInitiation\": 7\n            }\n        }\n    ]\n}\nEOF\n\naws s3api put-bucket-lifecycle-configuration \\\n    --bucket my-react-app-2024 \\\n    --lifecycle-configuration file:\u002F\u002F\u002Ftmp\u002Flifecycle.json \\\n    --region eu-central-1\n",[3432,21859,21860,21872,21876,21880,21884,21889,21893,21898,21902,21906,21910,21914,21918,21922,21926,21930,21934,21938,21942,21946,21956,21964,21972],{"__ignoreMap":3569},[3603,21861,21862,21864,21866,21868,21870],{"class":3605,"line":3606},[3603,21863,7831],{"class":5220},[3603,21865,7834],{"class":5541},[3603,21867,7837],{"class":5224},[3603,21869,7840],{"class":5541},[3603,21871,7843],{"class":5541},[3603,21873,21874],{"class":3605,"line":3612},[3603,21875,5591],{"class":5224},[3603,21877,21878],{"class":3605,"line":3618},[3603,21879,7852],{"class":5224},[3603,21881,21882],{"class":3605,"line":3624},[3603,21883,5873],{"class":5224},[3603,21885,21886],{"class":3605,"line":3631},[3603,21887,21888],{"class":5224},"            \"ID\": \"CleanOldVersions\",\n",[3603,21890,21891],{"class":3605,"line":3637},[3603,21892,7866],{"class":5224},[3603,21894,21895],{"class":3605,"line":3643},[3603,21896,21897],{"class":5224},"            \"Filter\": {},\n",[3603,21899,21900],{"class":3605,"line":3649},[3603,21901,7900],{"class":5224},[3603,21903,21904],{"class":3605,"line":3655},[3603,21905,7905],{"class":5224},[3603,21907,21908],{"class":3605,"line":3661},[3603,21909,7910],{"class":5224},[3603,21911,21912],{"class":3605,"line":3667},[3603,21913,5921],{"class":5224},[3603,21915,21916],{"class":3605,"line":3673},[3603,21917,7919],{"class":5224},[3603,21919,21920],{"class":3605,"line":3678},[3603,21921,7924],{"class":5224},[3603,21923,21924],{"class":3605,"line":3684},[3603,21925,6045],{"class":5224},[3603,21927,21928],{"class":3605,"line":3690},[3603,21929,6050],{"class":5224},[3603,21931,21932],{"class":3605,"line":3696},[3603,21933,6055],{"class":5224},[3603,21935,21936],{"class":3605,"line":3702},[3603,21937,3670],{"class":5224},[3603,21939,21940],{"class":3605,"line":3708},[3603,21941,7945],{"class":5541},[3603,21943,21944],{"class":3605,"line":4667},[3603,21945,3628],{"emptyLinePlaceholder":3627},[3603,21947,21948,21950,21952,21954],{"class":3605,"line":4673},[3603,21949,5221],{"class":5220},[3603,21951,5365],{"class":5224},[3603,21953,7963],{"class":5224},[3603,21955,5238],{"class":5237},[3603,21957,21958,21960,21962],{"class":3605,"line":4679},[3603,21959,6545],{"class":5243},[3603,21961,21149],{"class":5224},[3603,21963,5238],{"class":5237},[3603,21965,21966,21968,21970],{"class":3605,"line":4685},[3603,21967,7982],{"class":5243},[3603,21969,7985],{"class":5224},[3603,21971,5238],{"class":5237},[3603,21973,21974,21976],{"class":3605,"line":4691},[3603,21975,6571],{"class":5243},[3603,21977,13426],{"class":5224},[3353,21979,21980],{},"Це правило: зберігати не більше 5 попередніх версій кожного файлу, і видаляти версії старші 30 днів. Також прибираємо незавершені multipart uploads старші 7 днів.",[3405,21982],{},[3412,21984,21986],{"id":21985},"крок-10-обовязково-очищення","Крок 10: ОБОВ'ЯЗКОВО — Очищення",[3879,21988,21989],{},"S3 сам по собі дуже дешевий, але Elastic IP та інші ресурси можуть тарифікуватись. Для навчального bucket витрати мінімальні, але після завершення роботи видаліть.",[3561,21991,21993],{"className":5205,"code":21992,"language":5207,"meta":3569,"style":3569},"BUCKET=\"my-react-app-2024\"\nREGION=\"eu-central-1\"\n\n# Видалити всі об'єкти (включаючи всі версії)\naws s3api delete-objects \\\n    --bucket $BUCKET \\\n    --delete \"$(aws s3api list-object-versions \\\n        --bucket $BUCKET \\\n        --query '{Objects: Versions[].{Key:Key,VersionId:VersionId}}' \\\n        --output json)\" \\\n    --region $REGION\n\n# Видалити Delete Markers (якщо є)\n# (аналогічна команда з DeleteMarkers замість Versions)\n\n# Видалити сам bucket (лише якщо порожній)\naws s3api delete-bucket --bucket $BUCKET --region $REGION\n",[3432,21994,21995,22003,22011,22015,22020,22031,22039,22054,22063,22073,22083,22090,22094,22099,22104,22108,22113],{"__ignoreMap":3569},[3603,21996,21997,21999,22001],{"class":3605,"line":3606},[3603,21998,6504],{"class":5576},[3603,22000,6507],{"class":5541},[3603,22002,20919],{"class":5224},[3603,22004,22005,22007,22009],{"class":3605,"line":3612},[3603,22006,6515],{"class":5576},[3603,22008,6507],{"class":5541},[3603,22010,6520],{"class":5224},[3603,22012,22013],{"class":3605,"line":3618},[3603,22014,3628],{"emptyLinePlaceholder":3627},[3603,22016,22017],{"class":3605,"line":3624},[3603,22018,22019],{"class":5214},"# Видалити всі об'єкти (включаючи всі версії)\n",[3603,22021,22022,22024,22026,22029],{"class":3605,"line":3631},[3603,22023,5221],{"class":5220},[3603,22025,5365],{"class":5224},[3603,22027,22028],{"class":5224}," delete-objects",[3603,22030,5238],{"class":5237},[3603,22032,22033,22035,22037],{"class":3605,"line":3637},[3603,22034,6545],{"class":5243},[3603,22036,20954],{"class":5576},[3603,22038,5238],{"class":5237},[3603,22040,22041,22043,22046,22048,22051],{"class":3605,"line":3643},[3603,22042,21570],{"class":5243},[3603,22044,22045],{"class":5224}," \"$(",[3603,22047,5221],{"class":5220},[3603,22049,22050],{"class":5224}," s3api list-object-versions ",[3603,22052,22053],{"class":5237},"\\\n",[3603,22055,22056,22059,22061],{"class":3605,"line":3649},[3603,22057,22058],{"class":5243},"        --bucket",[3603,22060,20954],{"class":5576},[3603,22062,5238],{"class":5237},[3603,22064,22065,22068,22071],{"class":3605,"line":3655},[3603,22066,22067],{"class":5243},"        --query",[3603,22069,22070],{"class":5224}," '{Objects: Versions[].{Key:Key,VersionId:VersionId}}' ",[3603,22072,22053],{"class":5237},[3603,22074,22075,22078,22081],{"class":3605,"line":3661},[3603,22076,22077],{"class":5243},"        --output",[3603,22079,22080],{"class":5224}," json)\"",[3603,22082,5238],{"class":5237},[3603,22084,22085,22087],{"class":3605,"line":3667},[3603,22086,6571],{"class":5243},[3603,22088,22089],{"class":5576}," $REGION\n",[3603,22091,22092],{"class":3605,"line":3673},[3603,22093,3628],{"emptyLinePlaceholder":3627},[3603,22095,22096],{"class":3605,"line":3678},[3603,22097,22098],{"class":5214},"# Видалити Delete Markers (якщо є)\n",[3603,22100,22101],{"class":3605,"line":3684},[3603,22102,22103],{"class":5214},"# (аналогічна команда з DeleteMarkers замість Versions)\n",[3603,22105,22106],{"class":3605,"line":3690},[3603,22107,3628],{"emptyLinePlaceholder":3627},[3603,22109,22110],{"class":3605,"line":3696},[3603,22111,22112],{"class":5214},"# Видалити сам bucket (лише якщо порожній)\n",[3603,22114,22115,22117,22119,22122,22124,22126,22128],{"class":3605,"line":3702},[3603,22116,5221],{"class":5220},[3603,22118,5365],{"class":5224},[3603,22120,22121],{"class":5224}," delete-bucket",[3603,22123,21030],{"class":5243},[3603,22125,20954],{"class":5576},[3603,22127,6612],{"class":5243},[3603,22129,22089],{"class":5576},[3405,22131],{},[3348,22133,22135],{"id":22134},"практичний-приклад-product-image-manager-aspnet-s3","Практичний приклад: Product Image Manager (ASP.NET + S3)",[3353,22137,22138,22139,22142,22143,22146],{},"Реалістичний сценарій: REST API для управління продуктами, де зображення зберігаються в ",[3356,22140,22141],{},"приватному"," S3 bucket. Ключовий патерн — ",[3356,22144,22145],{},"Presigned PUT Upload",": фронтенд завантажує файл напряму в S3, не гоняючи бінарні дані через ASP.NET сервер.",[3561,22148,22151],{"className":22149,"code":22150,"language":3566},[3564],"┌─────────┐  1. GET \u002Fproducts\u002F1\u002Fupload-url   ┌─────────────────┐\n│         ├─────────────────────────────────►│   ASP.NET API   │\n│         │◄─────────────────────────────────┤ (генерує signed │\n│         │  { uploadUrl, key }              │  PUT URL + key) │\n│         │                                  └─────────────────┘\n│ Browser │\n│         │  2. PUT image.jpg (напряму!)     ┌─────────────────┐\n│         ├─────────────────────────────────►│      S3         │\n│         │◄─────────────────────────────────┤  (приватний     │\n│         │  200 OK                          │   bucket)       │\n│         │                                  └─────────────────┘\n│         │  3. PUT \u002Fproducts\u002F1\u002Fimage        ┌─────────────────┐\n│         │     { key: \"products\u002F1\u002F...\" }    │   ASP.NET API   │\n│         ├─────────────────────────────────►│ (зберігає key   │\n│         │◄─────────────────────────────────┤  у БД, повертає │\n└─────────┘  { imageUrl: presigned GET URL } │  GET URL)       │\n                                             └─────────────────┘\n",[3432,22152,22150],{"__ignoreMap":3569},[3353,22154,22155,22156,22159],{},"Сервер ",[3356,22157,22158],{},"ніколи не отримує файл"," — він лише підписує URL та зберігає ключ у БД.",[3405,22161],{},[3412,22163,22165],{"id":22164},"крок-1-підготовка-s3-bucket","Крок 1: Підготовка S3 Bucket",[3353,22167,22168,22169,22172],{},"Bucket для зображень — ",[3356,22170,22171],{},"приватний"," (Block Public Access ON). Доступ лише через Presigned URLs.",[5197,22174,22175,22229],{},[5200,22176,22177],{"label":6375},[6382,22178,22179,22185,22195,22201,22210,22214,22224],{},[3372,22180,22181,3976,22183],{},[3356,22182,5544],{},[3356,22184,20855],{},[3372,22186,22187,4841,22189,4841,22192],{},[3356,22188,20860],{},[3432,22190,22191],{},"product-images-2024",[20865,22193,22194],{},"(додайте власний суфікс)",[3372,22196,22197,4841,22199],{},[3356,22198,20872],{},[3432,22200,14192],{},[3372,22202,22203,22205,22206,22209],{},[3356,22204,20888],{}," залишаємо ",[3356,22207,22208],{},"увімкненим"," — bucket приватний",[3372,22211,22212],{},[3356,22213,20855],{},[3372,22215,22216,22217,3976,22219,3976,22222],{},"Відкрийте bucket → ",[3356,22218,8593],{},[3356,22220,22221],{},"Cross-origin resource sharing (CORS)",[3356,22223,6401],{},[3372,22225,22226,22227],{},"Вставте CORS конфіг з секції нижче → ",[3356,22228,6410],{},[5200,22230,22231],{"label":6494},[3561,22232,22234],{"className":5205,"code":22233,"language":5207,"meta":3569,"style":3569},"BUCKET=\"product-images-2024\"\nREGION=\"eu-central-1\"\n\n# Bucket без публічного доступу (за замовчуванням — private)\naws s3api create-bucket \\\n    --bucket $BUCKET \\\n    --region $REGION \\\n    --create-bucket-configuration LocationConstraint=$REGION\n",[3432,22235,22236,22245,22253,22257,22262,22272,22280,22288],{"__ignoreMap":3569},[3603,22237,22238,22240,22242],{"class":3605,"line":3606},[3603,22239,6504],{"class":5576},[3603,22241,6507],{"class":5541},[3603,22243,22244],{"class":5224},"\"product-images-2024\"\n",[3603,22246,22247,22249,22251],{"class":3605,"line":3612},[3603,22248,6515],{"class":5576},[3603,22250,6507],{"class":5541},[3603,22252,6520],{"class":5224},[3603,22254,22255],{"class":3605,"line":3618},[3603,22256,3628],{"emptyLinePlaceholder":3627},[3603,22258,22259],{"class":3605,"line":3624},[3603,22260,22261],{"class":5214},"# Bucket без публічного доступу (за замовчуванням — private)\n",[3603,22263,22264,22266,22268,22270],{"class":3605,"line":3631},[3603,22265,5221],{"class":5220},[3603,22267,5365],{"class":5224},[3603,22269,20945],{"class":5224},[3603,22271,5238],{"class":5237},[3603,22273,22274,22276,22278],{"class":3605,"line":3637},[3603,22275,6545],{"class":5243},[3603,22277,20954],{"class":5576},[3603,22279,5238],{"class":5237},[3603,22281,22282,22284,22286],{"class":3605,"line":3643},[3603,22283,6571],{"class":5243},[3603,22285,20963],{"class":5576},[3603,22287,5238],{"class":5237},[3603,22289,22290,22292,22294],{"class":3605,"line":3649},[3603,22291,20970],{"class":5243},[3603,22293,20973],{"class":5224},[3603,22295,20976],{"class":5576},[3353,22297,22298,22299,22301],{},"CORS — потрібен щоб браузер міг робити ",[3432,22300,6134],{}," напряму в S3:",[3561,22303,22305],{"className":5205,"code":22304,"language":5207,"meta":3569,"style":3569},"cat > \u002Ftmp\u002Fproducts-cors.json \u003C\u003C 'EOF'\n[\n    {\n        \"AllowedHeaders\": [\"Content-Type\"],\n        \"AllowedMethods\": [\"PUT\", \"GET\"],\n        \"AllowedOrigins\": [\n            \"http:\u002F\u002Flocalhost:5173\",\n            \"https:\u002F\u002Fyourdomain.com\"\n        ],\n        \"ExposeHeaders\": [\"ETag\"],\n        \"MaxAgeSeconds\": 3000\n    }\n]\nEOF\n\naws s3api put-bucket-cors \\\n    --bucket product-images-2024 \\\n    --cors-configuration file:\u002F\u002F\u002Ftmp\u002Fproducts-cors.json \\\n    --region eu-central-1\n",[3432,22306,22307,22320,22324,22328,22333,22338,22342,22347,22351,22355,22359,22363,22367,22371,22375,22379,22389,22398,22407],{"__ignoreMap":3569},[3603,22308,22309,22311,22313,22316,22318],{"class":3605,"line":3606},[3603,22310,7831],{"class":5220},[3603,22312,7834],{"class":5541},[3603,22314,22315],{"class":5224},"\u002Ftmp\u002Fproducts-cors.json",[3603,22317,7840],{"class":5541},[3603,22319,7843],{"class":5541},[3603,22321,22322],{"class":3605,"line":3612},[3603,22323,13813],{"class":5224},[3603,22325,22326],{"class":3605,"line":3618},[3603,22327,11086],{"class":5224},[3603,22329,22330],{"class":3605,"line":3624},[3603,22331,22332],{"class":5224},"        \"AllowedHeaders\": [\"Content-Type\"],\n",[3603,22334,22335],{"class":3605,"line":3631},[3603,22336,22337],{"class":5224},"        \"AllowedMethods\": [\"PUT\", \"GET\"],\n",[3603,22339,22340],{"class":3605,"line":3637},[3603,22341,21336],{"class":5224},[3603,22343,22344],{"class":3605,"line":3643},[3603,22345,22346],{"class":5224},"            \"http:\u002F\u002Flocalhost:5173\",\n",[3603,22348,22349],{"class":3605,"line":3649},[3603,22350,21351],{"class":5224},[3603,22352,22353],{"class":3605,"line":3655},[3603,22354,18273],{"class":5224},[3603,22356,22357],{"class":3605,"line":3661},[3603,22358,21360],{"class":5224},[3603,22360,22361],{"class":3605,"line":3667},[3603,22362,21365],{"class":5224},[3603,22364,22365],{"class":3605,"line":3673},[3603,22366,3664],{"class":5224},[3603,22368,22369],{"class":3605,"line":3678},[3603,22370,13913],{"class":5224},[3603,22372,22373],{"class":3605,"line":3684},[3603,22374,7945],{"class":5541},[3603,22376,22377],{"class":3605,"line":3690},[3603,22378,3628],{"emptyLinePlaceholder":3627},[3603,22380,22381,22383,22385,22387],{"class":3605,"line":3696},[3603,22382,5221],{"class":5220},[3603,22384,5365],{"class":5224},[3603,22386,13932],{"class":5224},[3603,22388,5238],{"class":5237},[3603,22390,22391,22393,22396],{"class":3605,"line":3702},[3603,22392,6545],{"class":5243},[3603,22394,22395],{"class":5224}," product-images-2024",[3603,22397,5238],{"class":5237},[3603,22399,22400,22402,22405],{"class":3605,"line":3708},[3603,22401,13948],{"class":5243},[3603,22403,22404],{"class":5224}," file:\u002F\u002F\u002Ftmp\u002Fproducts-cors.json",[3603,22406,5238],{"class":5237},[3603,22408,22409,22411],{"class":3605,"line":4667},[3603,22410,6571],{"class":5243},[3603,22412,13426],{"class":5224},[3397,22414,22415,22418,22419,22421],{},[3432,22416,22417],{},"AllowedHeaders: [\"Content-Type\"]"," — мінімальний необхідний набір. Presigned PUT підписується разом із ",[3432,22420,3522],{},", тому браузер зобов'язаний надіслати цей заголовок — він має бути у дозволених.",[3405,22423],{},[3412,22425,22427],{"id":22426},"крок-2-ініціалізація-aspnet-проєкту","Крок 2: Ініціалізація ASP.NET проєкту",[3561,22429,22431],{"className":5205,"code":22430,"language":5207,"meta":3569,"style":3569},"# Minimal API проєкт без HTTPS (для локальної розробки)\ndotnet new webapi -n ProductImageManager --no-https\ncd ProductImageManager\n\n# EF Core + SQLite — продукти зберігаємо локально (не потрібен окремий сервер БД)\ndotnet add package Microsoft.EntityFrameworkCore.Sqlite\ndotnet add package Microsoft.EntityFrameworkCore.Design\n\n# AWS SDK\ndotnet add package AWSSDK.S3\ndotnet add package AWSSDK.Extensions.NETCore.Setup\n",[3432,22432,22433,22438,22456,22463,22467,22472,22483,22494,22498,22503,22513],{"__ignoreMap":3569},[3603,22434,22435],{"class":3605,"line":3606},[3603,22436,22437],{"class":5214},"# Minimal API проєкт без HTTPS (для локальної розробки)\n",[3603,22439,22440,22442,22444,22447,22450,22453],{"class":3605,"line":3612},[3603,22441,15564],{"class":5220},[3603,22443,15955],{"class":5224},[3603,22445,22446],{"class":5224}," webapi",[3603,22448,22449],{"class":5243}," -n",[3603,22451,22452],{"class":5224}," ProductImageManager",[3603,22454,22455],{"class":5243}," --no-https\n",[3603,22457,22458,22460],{"class":3605,"line":3618},[3603,22459,17905],{"class":5220},[3603,22461,22462],{"class":5224}," ProductImageManager\n",[3603,22464,22465],{"class":3605,"line":3624},[3603,22466,3628],{"emptyLinePlaceholder":3627},[3603,22468,22469],{"class":3605,"line":3631},[3603,22470,22471],{"class":5214},"# EF Core + SQLite — продукти зберігаємо локально (не потрібен окремий сервер БД)\n",[3603,22473,22474,22476,22478,22480],{"class":3605,"line":3637},[3603,22475,15564],{"class":5220},[3603,22477,15567],{"class":5224},[3603,22479,15570],{"class":5224},[3603,22481,22482],{"class":5224}," Microsoft.EntityFrameworkCore.Sqlite\n",[3603,22484,22485,22487,22489,22491],{"class":3605,"line":3643},[3603,22486,15564],{"class":5220},[3603,22488,15567],{"class":5224},[3603,22490,15570],{"class":5224},[3603,22492,22493],{"class":5224}," Microsoft.EntityFrameworkCore.Design\n",[3603,22495,22496],{"class":3605,"line":3649},[3603,22497,3628],{"emptyLinePlaceholder":3627},[3603,22499,22500],{"class":3605,"line":3655},[3603,22501,22502],{"class":5214},"# AWS SDK\n",[3603,22504,22505,22507,22509,22511],{"class":3605,"line":3661},[3603,22506,15564],{"class":5220},[3603,22508,15567],{"class":5224},[3603,22510,15570],{"class":5224},[3603,22512,15573],{"class":5224},[3603,22514,22515,22517,22519,22521],{"class":3605,"line":3667},[3603,22516,15564],{"class":5220},[3603,22518,15567],{"class":5224},[3603,22520,15570],{"class":5224},[3603,22522,15584],{"class":5224},[3353,22524,22525],{},"Структура проєкту після наших змін:",[3561,22527,22530],{"className":22528,"code":22529,"language":3566},[3564],"ProductImageManager\u002F\n├── Data\u002F\n│   └── AppDbContext.cs     ← EF Core контекст\n├── Models\u002F\n│   └── Product.cs          ← сутність продукту\n├── Services\u002F\n│   └── ProductImageService.cs  ← S3 операції\n├── Program.cs              ← Minimal API endpoints\n└── appsettings.json\n",[3432,22531,22529],{"__ignoreMap":3569},[3405,22533],{},[3412,22535,22537],{"id":22536},"крок-3-модель-та-dbcontext","Крок 3: Модель та DbContext",[3353,22539,22540,5629],{},[3356,22541,22542],{},[3432,22543,22544],{},"Models\u002FProduct.cs",[3561,22546,22548],{"className":5519,"code":22547,"language":5521,"meta":3569,"style":3569},"\u002F\u002F Models\u002FProduct.cs\nnamespace ProductImageManager.Models;\n\npublic class Product\n{\n    public int     Id          { get; set; }\n    public string  Name        { get; set; } = string.Empty;\n    public string  Description { get; set; } = string.Empty;\n    public decimal Price       { get; set; }\n\n    \u002F\u002F S3 ключ зображення: \"products\u002F{id}\u002F{guid}.jpg\"\n    \u002F\u002F null — зображення ще не завантажено\n    public string? ImageKey    { get; set; }\n\n    public DateTime CreatedAt  { get; set; } = DateTime.UtcNow;\n}\n",[3432,22549,22550,22555,22569,22573,22582,22586,22610,22639,22666,22687,22691,22696,22701,22723,22727,22756],{"__ignoreMap":3569},[3603,22551,22552],{"class":3605,"line":3606},[3603,22553,22554],{"class":5214},"\u002F\u002F Models\u002FProduct.cs\n",[3603,22556,22557,22560,22562,22564,22567],{"class":3605,"line":3612},[3603,22558,22559],{"class":5243},"namespace",[3603,22561,22452],{"class":5537},[3603,22563,3453],{"class":5541},[3603,22565,22566],{"class":5537},"Models",[3603,22568,5547],{"class":5541},[3603,22570,22571],{"class":3605,"line":3618},[3603,22572,3628],{"emptyLinePlaceholder":3627},[3603,22574,22575,22577,22579],{"class":3605,"line":3624},[3603,22576,10953],{"class":5243},[3603,22578,10956],{"class":5243},[3603,22580,22581],{"class":5537}," Product\n",[3603,22583,22584],{"class":3605,"line":3631},[3603,22585,5591],{"class":5541},[3603,22587,22588,22590,22593,22596,22599,22602,22604,22607],{"class":3605,"line":3637},[3603,22589,11012],{"class":5243},[3603,22591,22592],{"class":5243}," int",[3603,22594,22595],{"class":5576},"     Id",[3603,22597,22598],{"class":5541},"          { ",[3603,22600,22601],{"class":5243},"get",[3603,22603,11318],{"class":5541},[3603,22605,22606],{"class":5243},"set",[3603,22608,22609],{"class":5541},"; }\n",[3603,22611,22612,22614,22616,22619,22621,22623,22625,22627,22630,22632,22634,22637],{"class":3605,"line":3643},[3603,22613,11012],{"class":5243},[3603,22615,10994],{"class":5243},[3603,22617,22618],{"class":5576},"  Name",[3603,22620,8151],{"class":5541},[3603,22622,22601],{"class":5243},[3603,22624,11318],{"class":5541},[3603,22626,22606],{"class":5243},[3603,22628,22629],{"class":5541},"; } = ",[3603,22631,7598],{"class":5243},[3603,22633,3453],{"class":5541},[3603,22635,22636],{"class":5576},"Empty",[3603,22638,5547],{"class":5541},[3603,22640,22641,22643,22645,22648,22650,22652,22654,22656,22658,22660,22662,22664],{"class":3605,"line":3649},[3603,22642,11012],{"class":5243},[3603,22644,10994],{"class":5243},[3603,22646,22647],{"class":5576},"  Description",[3603,22649,12973],{"class":5541},[3603,22651,22601],{"class":5243},[3603,22653,11318],{"class":5541},[3603,22655,22606],{"class":5243},[3603,22657,22629],{"class":5541},[3603,22659,7598],{"class":5243},[3603,22661,3453],{"class":5541},[3603,22663,22636],{"class":5576},[3603,22665,5547],{"class":5541},[3603,22667,22668,22670,22673,22676,22679,22681,22683,22685],{"class":3605,"line":3655},[3603,22669,11012],{"class":5243},[3603,22671,22672],{"class":5243}," decimal",[3603,22674,22675],{"class":5576}," Price",[3603,22677,22678],{"class":5541},"       { ",[3603,22680,22601],{"class":5243},[3603,22682,11318],{"class":5541},[3603,22684,22606],{"class":5243},[3603,22686,22609],{"class":5541},[3603,22688,22689],{"class":3605,"line":3661},[3603,22690,3628],{"emptyLinePlaceholder":3627},[3603,22692,22693],{"class":3605,"line":3667},[3603,22694,22695],{"class":5214},"    \u002F\u002F S3 ключ зображення: \"products\u002F{id}\u002F{guid}.jpg\"\n",[3603,22697,22698],{"class":3605,"line":3673},[3603,22699,22700],{"class":5214},"    \u002F\u002F null — зображення ще не завантажено\n",[3603,22702,22703,22705,22707,22710,22713,22715,22717,22719,22721],{"class":3605,"line":3678},[3603,22704,11012],{"class":5243},[3603,22706,10994],{"class":5243},[3603,22708,22709],{"class":5541},"? ",[3603,22711,22712],{"class":5576},"ImageKey",[3603,22714,18282],{"class":5541},[3603,22716,22601],{"class":5243},[3603,22718,11318],{"class":5541},[3603,22720,22606],{"class":5243},[3603,22722,22609],{"class":5541},[3603,22724,22725],{"class":3605,"line":3684},[3603,22726,3628],{"emptyLinePlaceholder":3627},[3603,22728,22729,22731,22734,22737,22740,22742,22744,22746,22748,22750,22752,22754],{"class":3605,"line":3690},[3603,22730,11012],{"class":5243},[3603,22732,22733],{"class":5537}," DateTime",[3603,22735,22736],{"class":5576}," CreatedAt",[3603,22738,22739],{"class":5541},"  { ",[3603,22741,22601],{"class":5243},[3603,22743,11318],{"class":5541},[3603,22745,22606],{"class":5243},[3603,22747,22629],{"class":5541},[3603,22749,5621],{"class":5576},[3603,22751,3453],{"class":5541},[3603,22753,5626],{"class":5576},[3603,22755,5547],{"class":5541},[3603,22757,22758],{"class":3605,"line":3696},[3603,22759,3670],{"class":5541},[3353,22761,22762,5629],{},[3356,22763,22764],{},[3432,22765,22766],{},"Data\u002FAppDbContext.cs",[3561,22768,22770],{"className":5519,"code":22769,"language":5521,"meta":3569,"style":3569},"\u002F\u002F Data\u002FAppDbContext.cs\nusing Microsoft.EntityFrameworkCore;\nusing ProductImageManager.Models;\n\nnamespace ProductImageManager.Data;\n\npublic class AppDbContext(DbContextOptions\u003CAppDbContext> options)\n    : DbContext(options)\n{\n    public DbSet\u003CProduct> Products => Set\u003CProduct>();\n}\n",[3432,22771,22772,22777,22790,22802,22806,22819,22823,22849,22863,22867,22895],{"__ignoreMap":3569},[3603,22773,22774],{"class":3605,"line":3606},[3603,22775,22776],{"class":5214},"\u002F\u002F Data\u002FAppDbContext.cs\n",[3603,22778,22779,22781,22783,22785,22788],{"class":3605,"line":3612},[3603,22780,5534],{"class":5533},[3603,22782,15831],{"class":5537},[3603,22784,3453],{"class":5541},[3603,22786,22787],{"class":5537},"EntityFrameworkCore",[3603,22789,5547],{"class":5541},[3603,22791,22792,22794,22796,22798,22800],{"class":3605,"line":3618},[3603,22793,5534],{"class":5533},[3603,22795,22452],{"class":5537},[3603,22797,3453],{"class":5541},[3603,22799,22566],{"class":5537},[3603,22801,5547],{"class":5541},[3603,22803,22804],{"class":3605,"line":3624},[3603,22805,3628],{"emptyLinePlaceholder":3627},[3603,22807,22808,22810,22812,22814,22817],{"class":3605,"line":3631},[3603,22809,22559],{"class":5243},[3603,22811,22452],{"class":5537},[3603,22813,3453],{"class":5541},[3603,22815,22816],{"class":5537},"Data",[3603,22818,5547],{"class":5541},[3603,22820,22821],{"class":3605,"line":3637},[3603,22822,3628],{"emptyLinePlaceholder":3627},[3603,22824,22825,22827,22829,22832,22834,22837,22839,22842,22844,22847],{"class":3605,"line":3643},[3603,22826,10953],{"class":5243},[3603,22828,10956],{"class":5243},[3603,22830,22831],{"class":5537}," AppDbContext",[3603,22833,5707],{"class":5541},[3603,22835,22836],{"class":5537},"DbContextOptions",[3603,22838,11250],{"class":5541},[3603,22840,22841],{"class":5537},"AppDbContext",[3603,22843,11255],{"class":5541},[3603,22845,22846],{"class":5576},"options",[3603,22848,6924],{"class":5541},[3603,22850,22851,22854,22857,22859,22861],{"class":3605,"line":3649},[3603,22852,22853],{"class":5541},"    : ",[3603,22855,22856],{"class":5537},"DbContext",[3603,22858,5707],{"class":5541},[3603,22860,22846],{"class":5576},[3603,22862,6924],{"class":5541},[3603,22864,22865],{"class":3605,"line":3655},[3603,22866,5591],{"class":5541},[3603,22868,22869,22871,22874,22876,22879,22881,22884,22886,22889,22891,22893],{"class":3605,"line":3661},[3603,22870,11012],{"class":5243},[3603,22872,22873],{"class":5537}," DbSet",[3603,22875,11250],{"class":5541},[3603,22877,22878],{"class":5537},"Product",[3603,22880,11255],{"class":5541},[3603,22882,22883],{"class":5576},"Products",[3603,22885,16476],{"class":5541},[3603,22887,22888],{"class":5220},"Set",[3603,22890,11250],{"class":5541},[3603,22892,22878],{"class":5537},[3603,22894,15622],{"class":5541},[3603,22896,22897],{"class":3605,"line":3667},[3603,22898,3670],{"class":5541},[3405,22900],{},[3412,22902,22904],{"id":22903},"крок-4-productimageservice","Крок 4: ProductImageService",[3353,22906,22907,22912],{},[3356,22908,22909],{},[3432,22910,22911],{},"Services\u002FProductImageService.cs"," — інкапсулює всі S3 операції з зображеннями:",[3561,22914,22916],{"className":5519,"code":22915,"language":5521,"meta":3569,"style":3569},"\u002F\u002F Services\u002FProductImageService.cs\nusing Amazon.S3;\nusing Amazon.S3.Model;\n\nnamespace ProductImageManager.Services;\n\npublic class ProductImageService(IAmazonS3 s3, IConfiguration config)\n{\n    private readonly string _bucket = config[\"S3:BucketName\"]\n        ?? throw new InvalidOperationException(\"S3:BucketName is not configured\");\n\n    private const int UploadUrlExpiry  = 15; \u002F\u002F хвилин — короткий TTL для безпеки\n    private const int DisplayUrlExpiry = 60; \u002F\u002F хвилин — для відображення в UI\n\n    \u002F\u002F Унікальний S3 ключ для зображення продукту\n    \u002F\u002F Формат: products\u002F{productId}\u002F{guid}{extension}\n    \u002F\u002F Guid гарантує відсутність колізій при повторних завантаженнях\n    public static string BuildImageKey(int productId, string extension)\n        => $\"products\u002F{productId}\u002F{Guid.NewGuid()}{extension}\";\n\n    \u002F\u002F Presigned PUT URL — фронтенд завантажує файл напряму в S3\n    \u002F\u002F Content-Type підписується у URL → клієнт зобов'язаний надіслати той самий заголовок\n    public string GetUploadUrl(string key, string contentType) =>\n        s3.GetPreSignedURL(new GetPreSignedUrlRequest\n        {\n            BucketName  = _bucket,\n            Key         = key,\n            Verb        = HttpVerb.PUT,\n            ContentType = contentType,\n            Expires     = DateTime.UtcNow.AddMinutes(UploadUrlExpiry),\n        });\n\n    \u002F\u002F Presigned GET URL — тимчасове посилання для \u003Cimg src> або завантаження\n    public string GetDisplayUrl(string key) =>\n        s3.GetPreSignedURL(new GetPreSignedUrlRequest\n        {\n            BucketName = _bucket,\n            Key        = key,\n            Verb       = HttpVerb.GET,\n            Expires    = DateTime.UtcNow.AddMinutes(DisplayUrlExpiry),\n        });\n\n    \u002F\u002F Видалення при зміні або видаленні продукту — не залишаємо orphan файли\n    public Task DeleteAsync(string key) =>\n        s3.DeleteObjectAsync(_bucket, key);\n}\n",[3432,22917,22918,22923,22935,22951,22955,22967,22971,22994,22998,23018,23036,23040,23061,23081,23085,23090,23095,23100,23128,23165,23169,23174,23179,23202,23217,23221,23231,23242,23257,23267,23291,23295,23299,23304,23321,23335,23339,23349,23359,23374,23398,23402,23406,23411,23427,23445],{"__ignoreMap":3569},[3603,22919,22920],{"class":3605,"line":3606},[3603,22921,22922],{"class":5214},"\u002F\u002F Services\u002FProductImageService.cs\n",[3603,22924,22925,22927,22929,22931,22933],{"class":3605,"line":3612},[3603,22926,5534],{"class":5533},[3603,22928,5538],{"class":5537},[3603,22930,3453],{"class":5541},[3603,22932,5544],{"class":5537},[3603,22934,5547],{"class":5541},[3603,22936,22937,22939,22941,22943,22945,22947,22949],{"class":3605,"line":3618},[3603,22938,5534],{"class":5533},[3603,22940,5538],{"class":5537},[3603,22942,3453],{"class":5541},[3603,22944,5544],{"class":5537},[3603,22946,3453],{"class":5541},[3603,22948,5562],{"class":5537},[3603,22950,5547],{"class":5541},[3603,22952,22953],{"class":3605,"line":3624},[3603,22954,3628],{"emptyLinePlaceholder":3627},[3603,22956,22957,22959,22961,22963,22965],{"class":3605,"line":3631},[3603,22958,22559],{"class":5243},[3603,22960,22452],{"class":5537},[3603,22962,3453],{"class":5541},[3603,22964,15610],{"class":5537},[3603,22966,5547],{"class":5541},[3603,22968,22969],{"class":3605,"line":3637},[3603,22970,3628],{"emptyLinePlaceholder":3627},[3603,22972,22973,22975,22977,22980,22982,22984,22986,22988,22990,22992],{"class":3605,"line":3643},[3603,22974,10953],{"class":5243},[3603,22976,10956],{"class":5243},[3603,22978,22979],{"class":5537}," ProductImageService",[3603,22981,5707],{"class":5541},[3603,22983,11020],{"class":5537},[3603,22985,5225],{"class":5576},[3603,22987,3470],{"class":5541},[3603,22989,15907],{"class":5537},[3603,22991,14355],{"class":5576},[3603,22993,6924],{"class":5541},[3603,22995,22996],{"class":3605,"line":3649},[3603,22997,5591],{"class":5541},[3603,22999,23000,23002,23004,23006,23008,23010,23012,23014,23016],{"class":3605,"line":3655},[3603,23001,10968],{"class":5243},[3603,23003,10971],{"class":5243},[3603,23005,10994],{"class":5243},[3603,23007,15883],{"class":5576},[3603,23009,5580],{"class":5541},[3603,23011,14417],{"class":5576},[3603,23013,15943],{"class":5541},[3603,23015,15946],{"class":5224},[3603,23017,13913],{"class":5541},[3603,23019,23020,23023,23025,23027,23029,23031,23034],{"class":3605,"line":3661},[3603,23021,23022],{"class":5541},"        ?? ",[3603,23024,15952],{"class":5533},[3603,23026,15955],{"class":5243},[3603,23028,15958],{"class":5537},[3603,23030,5707],{"class":5541},[3603,23032,23033],{"class":5224},"\"S3:BucketName is not configured\"",[3603,23035,5713],{"class":5541},[3603,23037,23038],{"class":3605,"line":3667},[3603,23039,3628],{"emptyLinePlaceholder":3627},[3603,23041,23042,23044,23046,23048,23051,23054,23056,23058],{"class":3605,"line":3673},[3603,23043,10968],{"class":5243},[3603,23045,10991],{"class":5243},[3603,23047,22592],{"class":5243},[3603,23049,23050],{"class":5576}," UploadUrlExpiry",[3603,23052,23053],{"class":5541},"  = ",[3603,23055,12672],{"class":5943},[3603,23057,11318],{"class":5541},[3603,23059,23060],{"class":5214},"\u002F\u002F хвилин — короткий TTL для безпеки\n",[3603,23062,23063,23065,23067,23069,23072,23074,23076,23078],{"class":3605,"line":3678},[3603,23064,10968],{"class":5243},[3603,23066,10991],{"class":5243},[3603,23068,22592],{"class":5243},[3603,23070,23071],{"class":5576}," DisplayUrlExpiry",[3603,23073,5580],{"class":5541},[3603,23075,16596],{"class":5943},[3603,23077,11318],{"class":5541},[3603,23079,23080],{"class":5214},"\u002F\u002F хвилин — для відображення в UI\n",[3603,23082,23083],{"class":3605,"line":3684},[3603,23084,3628],{"emptyLinePlaceholder":3627},[3603,23086,23087],{"class":3605,"line":3690},[3603,23088,23089],{"class":5214},"    \u002F\u002F Унікальний S3 ключ для зображення продукту\n",[3603,23091,23092],{"class":3605,"line":3696},[3603,23093,23094],{"class":5214},"    \u002F\u002F Формат: products\u002F{productId}\u002F{guid}{extension}\n",[3603,23096,23097],{"class":3605,"line":3702},[3603,23098,23099],{"class":5214},"    \u002F\u002F Guid гарантує відсутність колізій при повторних завантаженнях\n",[3603,23101,23102,23104,23107,23109,23112,23114,23116,23119,23121,23123,23126],{"class":3605,"line":3708},[3603,23103,11012],{"class":5243},[3603,23105,23106],{"class":5243}," static",[3603,23108,10994],{"class":5243},[3603,23110,23111],{"class":5220}," BuildImageKey",[3603,23113,5707],{"class":5541},[3603,23115,12664],{"class":5243},[3603,23117,23118],{"class":5576}," productId",[3603,23120,3470],{"class":5541},[3603,23122,7598],{"class":5243},[3603,23124,23125],{"class":5576}," extension",[3603,23127,6924],{"class":5541},[3603,23129,23130,23133,23136,23138,23141,23143,23145,23147,23149,23151,23153,23156,23159,23161,23163],{"class":3605,"line":4667},[3603,23131,23132],{"class":5541},"        => ",[3603,23134,23135],{"class":5224},"$\"products\u002F",[3603,23137,5618],{"class":5617},[3603,23139,23140],{"class":5576},"productId",[3603,23142,5645],{"class":5617},[3603,23144,3575],{"class":5224},[3603,23146,5618],{"class":5617},[3603,23148,17416],{"class":5576},[3603,23150,3453],{"class":5617},[3603,23152,17421],{"class":5220},[3603,23154,23155],{"class":5617},"()}{",[3603,23157,23158],{"class":5576},"extension",[3603,23160,5645],{"class":5617},[3603,23162,6554],{"class":5224},[3603,23164,5547],{"class":5541},[3603,23166,23167],{"class":3605,"line":4673},[3603,23168,3628],{"emptyLinePlaceholder":3627},[3603,23170,23171],{"class":3605,"line":4679},[3603,23172,23173],{"class":5214},"    \u002F\u002F Presigned PUT URL — фронтенд завантажує файл напряму в S3\n",[3603,23175,23176],{"class":3605,"line":4685},[3603,23177,23178],{"class":5214},"    \u002F\u002F Content-Type підписується у URL → клієнт зобов'язаний надіслати той самий заголовок\n",[3603,23180,23181,23183,23185,23187,23189,23191,23193,23195,23197,23199],{"class":3605,"line":4691},[3603,23182,11012],{"class":5243},[3603,23184,10994],{"class":5243},[3603,23186,17675],{"class":5220},[3603,23188,5707],{"class":5541},[3603,23190,7598],{"class":5243},[3603,23192,11071],{"class":5576},[3603,23194,3470],{"class":5541},[3603,23196,7598],{"class":5243},[3603,23198,12821],{"class":5576},[3603,23200,23201],{"class":5541},") =>\n",[3603,23203,23204,23207,23209,23211,23213,23215],{"class":3605,"line":4696},[3603,23205,23206],{"class":5576},"        s3",[3603,23208,3453],{"class":5541},[3603,23210,12771],{"class":5220},[3603,23212,5707],{"class":5541},[3603,23214,5583],{"class":5243},[3603,23216,12691],{"class":5537},[3603,23218,23219],{"class":3605,"line":4701},[3603,23220,5873],{"class":5541},[3603,23222,23223,23225,23227,23229],{"class":3605,"line":4707},[3603,23224,11109],{"class":5576},[3603,23226,23053],{"class":5541},[3603,23228,16084],{"class":5576},[3603,23230,5604],{"class":5541},[3603,23232,23233,23235,23238,23240],{"class":3605,"line":4713},[3603,23234,11121],{"class":5576},[3603,23236,23237],{"class":5541},"         = ",[3603,23239,11126],{"class":5576},[3603,23241,5604],{"class":5541},[3603,23243,23244,23246,23249,23251,23253,23255],{"class":3605,"line":4719},[3603,23245,12746],{"class":5576},[3603,23247,23248],{"class":5541},"        = ",[3603,23250,12751],{"class":5576},[3603,23252,3453],{"class":5541},[3603,23254,6134],{"class":5576},[3603,23256,5604],{"class":5541},[3603,23258,23259,23261,23263,23265],{"class":3605,"line":4724},[3603,23260,12915],{"class":5576},[3603,23262,5580],{"class":5541},[3603,23264,16116],{"class":5576},[3603,23266,5604],{"class":5541},[3603,23268,23269,23271,23274,23276,23278,23280,23282,23284,23286,23289],{"class":3605,"line":4730},[3603,23270,12720],{"class":5576},[3603,23272,23273],{"class":5541},"     = ",[3603,23275,5621],{"class":5576},[3603,23277,3453],{"class":5541},[3603,23279,5626],{"class":5576},[3603,23281,3453],{"class":5541},[3603,23283,12733],{"class":5220},[3603,23285,5707],{"class":5541},[3603,23287,23288],{"class":5576},"UploadUrlExpiry",[3603,23290,12741],{"class":5541},[3603,23292,23293],{"class":3605,"line":4736},[3603,23294,16205],{"class":5541},[3603,23296,23297],{"class":3605,"line":4742},[3603,23298,3628],{"emptyLinePlaceholder":3627},[3603,23300,23301],{"class":3605,"line":4748},[3603,23302,23303],{"class":5214},"    \u002F\u002F Presigned GET URL — тимчасове посилання для \u003Cimg src> або завантаження\n",[3603,23305,23306,23308,23310,23313,23315,23317,23319],{"class":3605,"line":4753},[3603,23307,11012],{"class":5243},[3603,23309,10994],{"class":5243},[3603,23311,23312],{"class":5220}," GetDisplayUrl",[3603,23314,5707],{"class":5541},[3603,23316,7598],{"class":5243},[3603,23318,11071],{"class":5576},[3603,23320,23201],{"class":5541},[3603,23322,23323,23325,23327,23329,23331,23333],{"class":3605,"line":4758},[3603,23324,23206],{"class":5576},[3603,23326,3453],{"class":5541},[3603,23328,12771],{"class":5220},[3603,23330,5707],{"class":5541},[3603,23332,5583],{"class":5243},[3603,23334,12691],{"class":5537},[3603,23336,23337],{"class":3605,"line":6363},[3603,23338,5873],{"class":5541},[3603,23340,23341,23343,23345,23347],{"class":3605,"line":6368},[3603,23342,11109],{"class":5576},[3603,23344,5580],{"class":5541},[3603,23346,16084],{"class":5576},[3603,23348,5604],{"class":5541},[3603,23350,23351,23353,23355,23357],{"class":3605,"line":6830},[3603,23352,11121],{"class":5576},[3603,23354,23248],{"class":5541},[3603,23356,11126],{"class":5576},[3603,23358,5604],{"class":5541},[3603,23360,23361,23363,23366,23368,23370,23372],{"class":3605,"line":6835},[3603,23362,12746],{"class":5576},[3603,23364,23365],{"class":5541},"       = ",[3603,23367,12751],{"class":5576},[3603,23369,3453],{"class":5541},[3603,23371,6107],{"class":5576},[3603,23373,5604],{"class":5541},[3603,23375,23376,23378,23381,23383,23385,23387,23389,23391,23393,23396],{"class":3605,"line":6841},[3603,23377,12720],{"class":5576},[3603,23379,23380],{"class":5541},"    = ",[3603,23382,5621],{"class":5576},[3603,23384,3453],{"class":5541},[3603,23386,5626],{"class":5576},[3603,23388,3453],{"class":5541},[3603,23390,12733],{"class":5220},[3603,23392,5707],{"class":5541},[3603,23394,23395],{"class":5576},"DisplayUrlExpiry",[3603,23397,12741],{"class":5541},[3603,23399,23400],{"class":3605,"line":6858},[3603,23401,16205],{"class":5541},[3603,23403,23404],{"class":3605,"line":6871},[3603,23405,3628],{"emptyLinePlaceholder":3627},[3603,23407,23408],{"class":3605,"line":6881},[3603,23409,23410],{"class":5214},"    \u002F\u002F Видалення при зміні або видаленні продукту — не залишаємо orphan файли\n",[3603,23412,23413,23415,23417,23419,23421,23423,23425],{"class":3605,"line":6894},[3603,23414,11012],{"class":5243},[3603,23416,11054],{"class":5537},[3603,23418,16518],{"class":5220},[3603,23420,5707],{"class":5541},[3603,23422,7598],{"class":5243},[3603,23424,11071],{"class":5576},[3603,23426,23201],{"class":5541},[3603,23428,23429,23431,23433,23435,23437,23439,23441,23443],{"class":3605,"line":6915},[3603,23430,23206],{"class":5576},[3603,23432,3453],{"class":5541},[3603,23434,16543],{"class":5220},[3603,23436,5707],{"class":5541},[3603,23438,16084],{"class":5576},[3603,23440,3470],{"class":5541},[3603,23442,11126],{"class":5576},[3603,23444,5713],{"class":5541},[3603,23446,23447],{"class":3605,"line":6927},[3603,23448,3670],{"class":5541},[3405,23450],{},[3412,23452,23454],{"id":23453},"крок-5-endpoints-programcs","Крок 5: Endpoints (Program.cs)",[3353,23456,23457,23458,5629],{},"Замінюємо вміст ",[3356,23459,23460],{},[3432,23461,23462],{},"Program.cs",[3561,23464,23466],{"className":5519,"code":23465,"language":5521,"meta":3569,"style":3569},"\u002F\u002F Program.cs\nusing Amazon.S3;\nusing Microsoft.EntityFrameworkCore;\nusing ProductImageManager.Data;\nusing ProductImageManager.Models;\nusing ProductImageManager.Services;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services.AddEndpointsApiExplorer();\nbuilder.Services.AddSwaggerGen();\n\n\u002F\u002F EF Core + SQLite\nbuilder.Services.AddDbContext\u003CAppDbContext>(o =>\n    o.UseSqlite(builder.Configuration.GetConnectionString(\"Default\")\n                ?? \"Data Source=products.db\"));\n\n\u002F\u002F AWS SDK — читає регіон з appsettings, credentials з ~\u002F.aws або env vars\nbuilder.Services.AddAWSService\u003CIAmazonS3>();\nbuilder.Services.AddScoped\u003CProductImageService>();\n\n\u002F\u002F CORS для Vite dev server (якщо є React фронтенд)\nbuilder.Services.AddCors(o =>\n    o.AddDefaultPolicy(p => p\n        .WithOrigins(\"http:\u002F\u002Flocalhost:5173\")\n        .AllowAnyHeader()\n        .AllowAnyMethod()));\n\nvar app = builder.Build();\napp.UseCors();\n\n\u002F\u002F Створюємо таблиці при першому запуску (зручно для демо)\nusing (var scope = app.Services.CreateScope())\n    await scope.ServiceProvider.GetRequiredService\u003CAppDbContext>()\n               .Database.EnsureCreatedAsync();\n\nif (app.Environment.IsDevelopment())\n{\n    app.UseSwagger();\n    app.UseSwaggerUI();\n}\n\n\u002F\u002F ─── GET \u002Fapi\u002Fproducts ────────────────────────────────────────────────────────\n\u002F\u002F Список усіх продуктів — presigned GET URL генерується на льоту для кожного\napp.MapGet(\"\u002Fapi\u002Fproducts\", async (AppDbContext db, ProductImageService images) =>\n{\n    var products = await db.Products.AsNoTracking().ToListAsync();\n    return products.Select(p => new\n    {\n        p.Id, p.Name, p.Description, p.Price,\n        ImageUrl = p.ImageKey is not null ? images.GetDisplayUrl(p.ImageKey) : null,\n        p.CreatedAt,\n    });\n});\n\n\u002F\u002F ─── GET \u002Fapi\u002Fproducts\u002F{id} ───────────────────────────────────────────────────\napp.MapGet(\"\u002Fapi\u002Fproducts\u002F{id:int}\", async (int id, AppDbContext db, ProductImageService images) =>\n{\n    if (await db.Products.FindAsync(id) is not { } p)\n        return Results.NotFound();\n\n    return Results.Ok(new\n    {\n        p.Id, p.Name, p.Description, p.Price,\n        ImageUrl = p.ImageKey is not null ? images.GetDisplayUrl(p.ImageKey) : null,\n        p.CreatedAt,\n    });\n});\n\n\u002F\u002F ─── POST \u002Fapi\u002Fproducts ───────────────────────────────────────────────────────\n\u002F\u002F Створення продукту без зображення — зображення завантажується окремим кроком\napp.MapPost(\"\u002Fapi\u002Fproducts\", async (CreateProductDto dto, AppDbContext db) =>\n{\n    var product = new Product\n    {\n        Name        = dto.Name,\n        Description = dto.Description,\n        Price       = dto.Price,\n    };\n    db.Products.Add(product);\n    await db.SaveChangesAsync();\n\n    return Results.Created($\"\u002Fapi\u002Fproducts\u002F{product.Id}\", new { product.Id, product.Name });\n});\n\n\u002F\u002F ─── GET \u002Fapi\u002Fproducts\u002F{id}\u002Fupload-url?contentType=image\u002Fjpeg ─────────────────\n\u002F\u002F Генерує presigned PUT URL — фронтенд завантажує файл напряму в S3\napp.MapGet(\"\u002Fapi\u002Fproducts\u002F{id:int}\u002Fupload-url\",\n    async (int id, string contentType, AppDbContext db, ProductImageService images) =>\n    {\n        if (await db.Products.FindAsync(id) is null)\n            return Results.NotFound();\n\n        \u002F\u002F Приймаємо лише зображення — валідація до генерації URL\n        var allowedTypes = new[] { \"image\u002Fjpeg\", \"image\u002Fpng\", \"image\u002Fwebp\" };\n        if (!allowedTypes.Contains(contentType))\n            return Results.BadRequest($\"Allowed content types: {string.Join(\", \", allowedTypes)}\");\n\n        var extension = contentType switch\n        {\n            \"image\u002Fjpeg\" => \".jpg\",\n            \"image\u002Fpng\"  => \".png\",\n            _            => \".webp\",\n        };\n\n        var key       = ProductImageService.BuildImageKey(id, extension);\n        var uploadUrl = images.GetUploadUrl(key, contentType);\n\n        return Results.Ok(new { uploadUrl, key, expiresInMinutes = 15 });\n    });\n\n\u002F\u002F ─── PUT \u002Fapi\u002Fproducts\u002F{id}\u002Fimage ─────────────────────────────────────────────\n\u002F\u002F Фронтенд викликає після успішного PUT у S3 — зберігаємо S3 key у БД\napp.MapPut(\"\u002Fapi\u002Fproducts\u002F{id:int}\u002Fimage\",\n    async (int id, ConfirmImageDto dto, AppDbContext db, ProductImageService images) =>\n    {\n        if (await db.Products.FindAsync(id) is not { } product)\n            return Results.NotFound();\n\n        \u002F\u002F Старе зображення видаляємо з S3 — уникаємо orphan файлів\n        if (product.ImageKey is not null)\n            await images.DeleteAsync(product.ImageKey);\n\n        product.ImageKey = dto.Key;\n        await db.SaveChangesAsync();\n\n        return Results.Ok(new { imageUrl = images.GetDisplayUrl(dto.Key) });\n    });\n\n\u002F\u002F ─── DELETE \u002Fapi\u002Fproducts\u002F{id} ────────────────────────────────────────────────\n\u002F\u002F Видалення продукту разом із зображенням у S3\napp.MapDelete(\"\u002Fapi\u002Fproducts\u002F{id:int}\",\n    async (int id, AppDbContext db, ProductImageService images) =>\n    {\n        if (await db.Products.FindAsync(id) is not { } product)\n            return Results.NotFound();\n\n        \u002F\u002F Спочатку видаляємо з S3, потім з БД\n        if (product.ImageKey is not null)\n            await images.DeleteAsync(product.ImageKey);\n\n        db.Products.Remove(product);\n        await db.SaveChangesAsync();\n\n        return Results.NoContent();\n    });\n\napp.Run();\n\n\u002F\u002F ─── DTOs ─────────────────────────────────────────────────────────────────────\nrecord CreateProductDto(string Name, string Description, decimal Price);\nrecord ConfirmImageDto(string Key);\n",[3432,23467,23468,23473,23485,23497,23509,23521,23533,23537,23561,23565,23580,23595,23599,23604,23629,23659,23669,23673,23678,23696,23716,23720,23725,23744,23762,23777,23786,23796,23800,23818,23830,23834,23839,23865,23889,23904,23908,23929,23933,23945,23956,23960,23964,23969,23974,24009,24013,24043,24062,24066,24105,24153,24164,24169,24174,24178,24183,24221,24225,24264,24278,24282,24297,24301,24335,24375,24385,24389,24393,24397,24402,24407,24440,24444,24457,24461,24477,24492,24507,24511,24532,24545,24549,24599,24603,24607,24612,24617,24632,24663,24667,24697,24709,24713,24718,24747,24768,24809,24813,24826,24830,24842,24855,24868,24872,24876,24901,24927,24931,24963,24967,24971,24976,24981,24997,25028,25032,25066,25078,25082,25087,25107,25129,25133,25152,25164,25168,25206,25210,25214,25219,25224,25239,25263,25268,25303,25316,25321,25327,25348,25369,25374,25395,25408,25413,25427,25432,25437,25449,25454,25460,25492],{"__ignoreMap":3569},[3603,23469,23470],{"class":3605,"line":3606},[3603,23471,23472],{"class":5214},"\u002F\u002F Program.cs\n",[3603,23474,23475,23477,23479,23481,23483],{"class":3605,"line":3612},[3603,23476,5534],{"class":5533},[3603,23478,5538],{"class":5537},[3603,23480,3453],{"class":5541},[3603,23482,5544],{"class":5537},[3603,23484,5547],{"class":5541},[3603,23486,23487,23489,23491,23493,23495],{"class":3605,"line":3618},[3603,23488,5534],{"class":5533},[3603,23490,15831],{"class":5537},[3603,23492,3453],{"class":5541},[3603,23494,22787],{"class":5537},[3603,23496,5547],{"class":5541},[3603,23498,23499,23501,23503,23505,23507],{"class":3605,"line":3624},[3603,23500,5534],{"class":5533},[3603,23502,22452],{"class":5537},[3603,23504,3453],{"class":5541},[3603,23506,22816],{"class":5537},[3603,23508,5547],{"class":5541},[3603,23510,23511,23513,23515,23517,23519],{"class":3605,"line":3631},[3603,23512,5534],{"class":5533},[3603,23514,22452],{"class":5537},[3603,23516,3453],{"class":5541},[3603,23518,22566],{"class":5537},[3603,23520,5547],{"class":5541},[3603,23522,23523,23525,23527,23529,23531],{"class":3605,"line":3637},[3603,23524,5534],{"class":5533},[3603,23526,22452],{"class":5537},[3603,23528,3453],{"class":5541},[3603,23530,15610],{"class":5537},[3603,23532,5547],{"class":5541},[3603,23534,23535],{"class":3605,"line":3643},[3603,23536,3628],{"emptyLinePlaceholder":3627},[3603,23538,23539,23541,23544,23546,23549,23551,23554,23556,23559],{"class":3605,"line":3649},[3603,23540,5573],{"class":5243},[3603,23542,23543],{"class":5576}," builder",[3603,23545,5580],{"class":5541},[3603,23547,23548],{"class":5576},"WebApplication",[3603,23550,3453],{"class":5541},[3603,23552,23553],{"class":5220},"CreateBuilder",[3603,23555,5707],{"class":5541},[3603,23557,23558],{"class":5576},"args",[3603,23560,5713],{"class":5541},[3603,23562,23563],{"class":3605,"line":3655},[3603,23564,3628],{"emptyLinePlaceholder":3627},[3603,23566,23567,23569,23571,23573,23575,23578],{"class":3605,"line":3661},[3603,23568,15605],{"class":5576},[3603,23570,3453],{"class":5541},[3603,23572,15610],{"class":5576},[3603,23574,3453],{"class":5541},[3603,23576,23577],{"class":5220},"AddEndpointsApiExplorer",[3603,23579,16490],{"class":5541},[3603,23581,23582,23584,23586,23588,23590,23593],{"class":3605,"line":3667},[3603,23583,15605],{"class":5576},[3603,23585,3453],{"class":5541},[3603,23587,15610],{"class":5576},[3603,23589,3453],{"class":5541},[3603,23591,23592],{"class":5220},"AddSwaggerGen",[3603,23594,16490],{"class":5541},[3603,23596,23597],{"class":3605,"line":3673},[3603,23598,3628],{"emptyLinePlaceholder":3627},[3603,23600,23601],{"class":3605,"line":3678},[3603,23602,23603],{"class":5214},"\u002F\u002F EF Core + SQLite\n",[3603,23605,23606,23608,23610,23612,23614,23617,23619,23621,23624,23626],{"class":3605,"line":3684},[3603,23607,15605],{"class":5576},[3603,23609,3453],{"class":5541},[3603,23611,15610],{"class":5576},[3603,23613,3453],{"class":5541},[3603,23615,23616],{"class":5220},"AddDbContext",[3603,23618,11250],{"class":5541},[3603,23620,22841],{"class":5537},[3603,23622,23623],{"class":5541},">(",[3603,23625,16473],{"class":5576},[3603,23627,23628],{"class":5541}," =>\n",[3603,23630,23631,23634,23636,23639,23641,23643,23645,23647,23649,23652,23654,23657],{"class":3605,"line":3690},[3603,23632,23633],{"class":5576},"    o",[3603,23635,3453],{"class":5541},[3603,23637,23638],{"class":5220},"UseSqlite",[3603,23640,5707],{"class":5541},[3603,23642,15605],{"class":5576},[3603,23644,3453],{"class":5541},[3603,23646,15841],{"class":5576},[3603,23648,3453],{"class":5541},[3603,23650,23651],{"class":5220},"GetConnectionString",[3603,23653,5707],{"class":5541},[3603,23655,23656],{"class":5224},"\"Default\"",[3603,23658,6924],{"class":5541},[3603,23660,23661,23664,23667],{"class":3605,"line":3696},[3603,23662,23663],{"class":5541},"                ?? ",[3603,23665,23666],{"class":5224},"\"Data Source=products.db\"",[3603,23668,19577],{"class":5541},[3603,23670,23671],{"class":3605,"line":3702},[3603,23672,3628],{"emptyLinePlaceholder":3627},[3603,23674,23675],{"class":3605,"line":3708},[3603,23676,23677],{"class":5214},"\u002F\u002F AWS SDK — читає регіон з appsettings, credentials з ~\u002F.aws або env vars\n",[3603,23679,23680,23682,23684,23686,23688,23690,23692,23694],{"class":3605,"line":4667},[3603,23681,15605],{"class":5576},[3603,23683,3453],{"class":5541},[3603,23685,15610],{"class":5576},[3603,23687,3453],{"class":5541},[3603,23689,15615],{"class":5220},[3603,23691,11250],{"class":5541},[3603,23693,11020],{"class":5537},[3603,23695,15622],{"class":5541},[3603,23697,23698,23700,23702,23704,23706,23709,23711,23714],{"class":3605,"line":4673},[3603,23699,15605],{"class":5576},[3603,23701,3453],{"class":5541},[3603,23703,15610],{"class":5576},[3603,23705,3453],{"class":5541},[3603,23707,23708],{"class":5220},"AddScoped",[3603,23710,11250],{"class":5541},[3603,23712,23713],{"class":5537},"ProductImageService",[3603,23715,15622],{"class":5541},[3603,23717,23718],{"class":3605,"line":4679},[3603,23719,3628],{"emptyLinePlaceholder":3627},[3603,23721,23722],{"class":3605,"line":4685},[3603,23723,23724],{"class":5214},"\u002F\u002F CORS для Vite dev server (якщо є React фронтенд)\n",[3603,23726,23727,23729,23731,23733,23735,23738,23740,23742],{"class":3605,"line":4691},[3603,23728,15605],{"class":5576},[3603,23730,3453],{"class":5541},[3603,23732,15610],{"class":5576},[3603,23734,3453],{"class":5541},[3603,23736,23737],{"class":5220},"AddCors",[3603,23739,5707],{"class":5541},[3603,23741,16473],{"class":5576},[3603,23743,23628],{"class":5541},[3603,23745,23746,23748,23750,23753,23755,23757,23759],{"class":3605,"line":4696},[3603,23747,23633],{"class":5576},[3603,23749,3453],{"class":5541},[3603,23751,23752],{"class":5220},"AddDefaultPolicy",[3603,23754,5707],{"class":5541},[3603,23756,3353],{"class":5576},[3603,23758,16476],{"class":5541},[3603,23760,23761],{"class":5576},"p\n",[3603,23763,23764,23767,23770,23772,23775],{"class":3605,"line":4701},[3603,23765,23766],{"class":5541},"        .",[3603,23768,23769],{"class":5220},"WithOrigins",[3603,23771,5707],{"class":5541},[3603,23773,23774],{"class":5224},"\"http:\u002F\u002Flocalhost:5173\"",[3603,23776,6924],{"class":5541},[3603,23778,23779,23781,23784],{"class":3605,"line":4707},[3603,23780,23766],{"class":5541},[3603,23782,23783],{"class":5220},"AllowAnyHeader",[3603,23785,15281],{"class":5541},[3603,23787,23788,23790,23793],{"class":3605,"line":4713},[3603,23789,23766],{"class":5541},[3603,23791,23792],{"class":5220},"AllowAnyMethod",[3603,23794,23795],{"class":5541},"()));\n",[3603,23797,23798],{"class":3605,"line":4719},[3603,23799,3628],{"emptyLinePlaceholder":3627},[3603,23801,23802,23804,23807,23809,23811,23813,23816],{"class":3605,"line":4724},[3603,23803,5573],{"class":5243},[3603,23805,23806],{"class":5576}," app",[3603,23808,5580],{"class":5541},[3603,23810,15605],{"class":5576},[3603,23812,3453],{"class":5541},[3603,23814,23815],{"class":5220},"Build",[3603,23817,16490],{"class":5541},[3603,23819,23820,23823,23825,23828],{"class":3605,"line":4730},[3603,23821,23822],{"class":5576},"app",[3603,23824,3453],{"class":5541},[3603,23826,23827],{"class":5220},"UseCors",[3603,23829,16490],{"class":5541},[3603,23831,23832],{"class":3605,"line":4736},[3603,23833,3628],{"emptyLinePlaceholder":3627},[3603,23835,23836],{"class":3605,"line":4742},[3603,23837,23838],{"class":5214},"\u002F\u002F Створюємо таблиці при першому запуску (зручно для демо)\n",[3603,23840,23841,23843,23845,23847,23850,23852,23854,23856,23858,23860,23863],{"class":3605,"line":4748},[3603,23842,5534],{"class":5533},[3603,23844,10425],{"class":5541},[3603,23846,5573],{"class":5243},[3603,23848,23849],{"class":5576}," scope",[3603,23851,5580],{"class":5541},[3603,23853,23822],{"class":5576},[3603,23855,3453],{"class":5541},[3603,23857,15610],{"class":5576},[3603,23859,3453],{"class":5541},[3603,23861,23862],{"class":5220},"CreateScope",[3603,23864,15257],{"class":5541},[3603,23866,23867,23870,23872,23874,23877,23879,23882,23884,23886],{"class":3605,"line":4753},[3603,23868,23869],{"class":5243},"    await",[3603,23871,23849],{"class":5576},[3603,23873,3453],{"class":5541},[3603,23875,23876],{"class":5576},"ServiceProvider",[3603,23878,3453],{"class":5541},[3603,23880,23881],{"class":5220},"GetRequiredService",[3603,23883,11250],{"class":5541},[3603,23885,22841],{"class":5537},[3603,23887,23888],{"class":5541},">()\n",[3603,23890,23891,23894,23897,23899,23902],{"class":3605,"line":4758},[3603,23892,23893],{"class":5541},"               .",[3603,23895,23896],{"class":5576},"Database",[3603,23898,3453],{"class":5541},[3603,23900,23901],{"class":5220},"EnsureCreatedAsync",[3603,23903,16490],{"class":5541},[3603,23905,23906],{"class":3605,"line":6363},[3603,23907,3628],{"emptyLinePlaceholder":3627},[3603,23909,23910,23913,23915,23917,23919,23922,23924,23927],{"class":3605,"line":6368},[3603,23911,23912],{"class":5533},"if",[3603,23914,10425],{"class":5541},[3603,23916,23822],{"class":5576},[3603,23918,3453],{"class":5541},[3603,23920,23921],{"class":5576},"Environment",[3603,23923,3453],{"class":5541},[3603,23925,23926],{"class":5220},"IsDevelopment",[3603,23928,15257],{"class":5541},[3603,23930,23931],{"class":3605,"line":6830},[3603,23932,5591],{"class":5541},[3603,23934,23935,23938,23940,23943],{"class":3605,"line":6835},[3603,23936,23937],{"class":5576},"    app",[3603,23939,3453],{"class":5541},[3603,23941,23942],{"class":5220},"UseSwagger",[3603,23944,16490],{"class":5541},[3603,23946,23947,23949,23951,23954],{"class":3605,"line":6841},[3603,23948,23937],{"class":5576},[3603,23950,3453],{"class":5541},[3603,23952,23953],{"class":5220},"UseSwaggerUI",[3603,23955,16490],{"class":5541},[3603,23957,23958],{"class":3605,"line":6858},[3603,23959,3670],{"class":5541},[3603,23961,23962],{"class":3605,"line":6871},[3603,23963,3628],{"emptyLinePlaceholder":3627},[3603,23965,23966],{"class":3605,"line":6881},[3603,23967,23968],{"class":5214},"\u002F\u002F ─── GET \u002Fapi\u002Fproducts ────────────────────────────────────────────────────────\n",[3603,23970,23971],{"class":3605,"line":6894},[3603,23972,23973],{"class":5214},"\u002F\u002F Список усіх продуктів — presigned GET URL генерується на льоту для кожного\n",[3603,23975,23976,23978,23980,23983,23985,23988,23990,23993,23995,23997,24000,24002,24004,24007],{"class":3605,"line":6915},[3603,23977,23822],{"class":5576},[3603,23979,3453],{"class":5541},[3603,23981,23982],{"class":5220},"MapGet",[3603,23984,5707],{"class":5541},[3603,23986,23987],{"class":5224},"\"\u002Fapi\u002Fproducts\"",[3603,23989,3470],{"class":5541},[3603,23991,23992],{"class":5243},"async",[3603,23994,10425],{"class":5541},[3603,23996,22841],{"class":5537},[3603,23998,23999],{"class":5576}," db",[3603,24001,3470],{"class":5541},[3603,24003,23713],{"class":5537},[3603,24005,24006],{"class":5576}," images",[3603,24008,23201],{"class":5541},[3603,24010,24011],{"class":3605,"line":6927},[3603,24012,5591],{"class":5541},[3603,24014,24015,24017,24020,24022,24024,24026,24028,24030,24032,24035,24038,24041],{"class":3605,"line":6932},[3603,24016,14885],{"class":5243},[3603,24018,24019],{"class":5576}," products",[3603,24021,5580],{"class":5541},[3603,24023,5696],{"class":5243},[3603,24025,23999],{"class":5576},[3603,24027,3453],{"class":5541},[3603,24029,22883],{"class":5576},[3603,24031,3453],{"class":5541},[3603,24033,24034],{"class":5220},"AsNoTracking",[3603,24036,24037],{"class":5541},"().",[3603,24039,24040],{"class":5220},"ToListAsync",[3603,24042,16490],{"class":5541},[3603,24044,24045,24047,24049,24051,24053,24055,24057,24059],{"class":3605,"line":6946},[3603,24046,14931],{"class":5533},[3603,24048,24019],{"class":5576},[3603,24050,3453],{"class":5541},[3603,24052,16468],{"class":5220},[3603,24054,5707],{"class":5541},[3603,24056,3353],{"class":5576},[3603,24058,16476],{"class":5541},[3603,24060,24061],{"class":5243},"new\n",[3603,24063,24064],{"class":3605,"line":6951},[3603,24065,11086],{"class":5541},[3603,24067,24068,24071,24073,24076,24078,24080,24082,24085,24087,24089,24091,24094,24096,24098,24100,24103],{"class":3605,"line":6963},[3603,24069,24070],{"class":5576},"        p",[3603,24072,3453],{"class":5541},[3603,24074,24075],{"class":5576},"Id",[3603,24077,3470],{"class":5541},[3603,24079,3353],{"class":5576},[3603,24081,3453],{"class":5541},[3603,24083,24084],{"class":5576},"Name",[3603,24086,3470],{"class":5541},[3603,24088,3353],{"class":5576},[3603,24090,3453],{"class":5541},[3603,24092,24093],{"class":5576},"Description",[3603,24095,3470],{"class":5541},[3603,24097,3353],{"class":5576},[3603,24099,3453],{"class":5541},[3603,24101,24102],{"class":5576},"Price",[3603,24104,5604],{"class":5541},[3603,24106,24107,24110,24112,24114,24116,24118,24121,24124,24127,24130,24133,24135,24138,24140,24142,24144,24146,24149,24151],{"class":3605,"line":6976},[3603,24108,24109],{"class":5576},"        ImageUrl",[3603,24111,5580],{"class":5541},[3603,24113,3353],{"class":5576},[3603,24115,3453],{"class":5541},[3603,24117,22712],{"class":5576},[3603,24119,24120],{"class":5243}," is",[3603,24122,24123],{"class":5243}," not",[3603,24125,24126],{"class":5243}," null",[3603,24128,24129],{"class":5541}," ? ",[3603,24131,24132],{"class":5576},"images",[3603,24134,3453],{"class":5541},[3603,24136,24137],{"class":5220},"GetDisplayUrl",[3603,24139,5707],{"class":5541},[3603,24141,3353],{"class":5576},[3603,24143,3453],{"class":5541},[3603,24145,22712],{"class":5576},[3603,24147,24148],{"class":5541},") : ",[3603,24150,6124],{"class":5243},[3603,24152,5604],{"class":5541},[3603,24154,24155,24157,24159,24162],{"class":3605,"line":6985},[3603,24156,24070],{"class":5576},[3603,24158,3453],{"class":5541},[3603,24160,24161],{"class":5576},"CreatedAt",[3603,24163,5604],{"class":5541},[3603,24165,24166],{"class":3605,"line":6998},[3603,24167,24168],{"class":5541},"    });\n",[3603,24170,24171],{"class":3605,"line":7009},[3603,24172,24173],{"class":5541},"});\n",[3603,24175,24176],{"class":3605,"line":7014},[3603,24177,3628],{"emptyLinePlaceholder":3627},[3603,24179,24180],{"class":3605,"line":8491},[3603,24181,24182],{"class":5214},"\u002F\u002F ─── GET \u002Fapi\u002Fproducts\u002F{id} ───────────────────────────────────────────────────\n",[3603,24184,24185,24187,24189,24191,24193,24196,24198,24200,24202,24204,24207,24209,24211,24213,24215,24217,24219],{"class":3605,"line":8497},[3603,24186,23822],{"class":5576},[3603,24188,3453],{"class":5541},[3603,24190,23982],{"class":5220},[3603,24192,5707],{"class":5541},[3603,24194,24195],{"class":5224},"\"\u002Fapi\u002Fproducts\u002F{id:int}\"",[3603,24197,3470],{"class":5541},[3603,24199,23992],{"class":5243},[3603,24201,10425],{"class":5541},[3603,24203,12664],{"class":5243},[3603,24205,24206],{"class":5576}," id",[3603,24208,3470],{"class":5541},[3603,24210,22841],{"class":5537},[3603,24212,23999],{"class":5576},[3603,24214,3470],{"class":5541},[3603,24216,23713],{"class":5537},[3603,24218,24006],{"class":5576},[3603,24220,23201],{"class":5541},[3603,24222,24223],{"class":3605,"line":8503},[3603,24224,5591],{"class":5541},[3603,24226,24227,24230,24232,24234,24236,24238,24240,24242,24245,24247,24250,24252,24255,24257,24260,24262],{"class":3605,"line":8509},[3603,24228,24229],{"class":5533},"    if",[3603,24231,10425],{"class":5541},[3603,24233,5696],{"class":5243},[3603,24235,23999],{"class":5576},[3603,24237,3453],{"class":5541},[3603,24239,22883],{"class":5576},[3603,24241,3453],{"class":5541},[3603,24243,24244],{"class":5220},"FindAsync",[3603,24246,5707],{"class":5541},[3603,24248,24249],{"class":5576},"id",[3603,24251,15099],{"class":5541},[3603,24253,24254],{"class":5243},"is",[3603,24256,24123],{"class":5243},[3603,24258,24259],{"class":5541}," { } ",[3603,24261,3353],{"class":5576},[3603,24263,6924],{"class":5541},[3603,24265,24266,24268,24271,24273,24276],{"class":3605,"line":8515},[3603,24267,11308],{"class":5533},[3603,24269,24270],{"class":5576}," Results",[3603,24272,3453],{"class":5541},[3603,24274,24275],{"class":5220},"NotFound",[3603,24277,16490],{"class":5541},[3603,24279,24280],{"class":3605,"line":8521},[3603,24281,3628],{"emptyLinePlaceholder":3627},[3603,24283,24284,24286,24288,24290,24293,24295],{"class":3605,"line":8527},[3603,24285,14931],{"class":5533},[3603,24287,24270],{"class":5576},[3603,24289,3453],{"class":5541},[3603,24291,24292],{"class":5220},"Ok",[3603,24294,5707],{"class":5541},[3603,24296,24061],{"class":5243},[3603,24298,24299],{"class":3605,"line":8532},[3603,24300,11086],{"class":5541},[3603,24302,24303,24305,24307,24309,24311,24313,24315,24317,24319,24321,24323,24325,24327,24329,24331,24333],{"class":3605,"line":16493},[3603,24304,24070],{"class":5576},[3603,24306,3453],{"class":5541},[3603,24308,24075],{"class":5576},[3603,24310,3470],{"class":5541},[3603,24312,3353],{"class":5576},[3603,24314,3453],{"class":5541},[3603,24316,24084],{"class":5576},[3603,24318,3470],{"class":5541},[3603,24320,3353],{"class":5576},[3603,24322,3453],{"class":5541},[3603,24324,24093],{"class":5576},[3603,24326,3470],{"class":5541},[3603,24328,3353],{"class":5576},[3603,24330,3453],{"class":5541},[3603,24332,24102],{"class":5576},[3603,24334,5604],{"class":5541},[3603,24336,24337,24339,24341,24343,24345,24347,24349,24351,24353,24355,24357,24359,24361,24363,24365,24367,24369,24371,24373],{"class":3605,"line":16498},[3603,24338,24109],{"class":5576},[3603,24340,5580],{"class":5541},[3603,24342,3353],{"class":5576},[3603,24344,3453],{"class":5541},[3603,24346,22712],{"class":5576},[3603,24348,24120],{"class":5243},[3603,24350,24123],{"class":5243},[3603,24352,24126],{"class":5243},[3603,24354,24129],{"class":5541},[3603,24356,24132],{"class":5576},[3603,24358,3453],{"class":5541},[3603,24360,24137],{"class":5220},[3603,24362,5707],{"class":5541},[3603,24364,3353],{"class":5576},[3603,24366,3453],{"class":5541},[3603,24368,22712],{"class":5576},[3603,24370,24148],{"class":5541},[3603,24372,6124],{"class":5243},[3603,24374,5604],{"class":5541},[3603,24376,24377,24379,24381,24383],{"class":3605,"line":16503},[3603,24378,24070],{"class":5576},[3603,24380,3453],{"class":5541},[3603,24382,24161],{"class":5576},[3603,24384,5604],{"class":5541},[3603,24386,24387],{"class":3605,"line":16509},[3603,24388,24168],{"class":5541},[3603,24390,24391],{"class":3605,"line":16529},[3603,24392,24173],{"class":5541},[3603,24394,24395],{"class":3605,"line":16534},[3603,24396,3628],{"emptyLinePlaceholder":3627},[3603,24398,24399],{"class":3605,"line":16556},[3603,24400,24401],{"class":5214},"\u002F\u002F ─── POST \u002Fapi\u002Fproducts ───────────────────────────────────────────────────────\n",[3603,24403,24404],{"class":3605,"line":16561},[3603,24405,24406],{"class":5214},"\u002F\u002F Створення продукту без зображення — зображення завантажується окремим кроком\n",[3603,24408,24409,24411,24413,24416,24418,24420,24422,24424,24426,24429,24432,24434,24436,24438],{"class":3605,"line":16566},[3603,24410,23822],{"class":5576},[3603,24412,3453],{"class":5541},[3603,24414,24415],{"class":5220},"MapPost",[3603,24417,5707],{"class":5541},[3603,24419,23987],{"class":5224},[3603,24421,3470],{"class":5541},[3603,24423,23992],{"class":5243},[3603,24425,10425],{"class":5541},[3603,24427,24428],{"class":5537},"CreateProductDto",[3603,24430,24431],{"class":5576}," dto",[3603,24433,3470],{"class":5541},[3603,24435,22841],{"class":5537},[3603,24437,23999],{"class":5576},[3603,24439,23201],{"class":5541},[3603,24441,24442],{"class":3605,"line":16572},[3603,24443,5591],{"class":5541},[3603,24445,24446,24448,24451,24453,24455],{"class":3605,"line":16601},[3603,24447,14885],{"class":5243},[3603,24449,24450],{"class":5576}," product",[3603,24452,5580],{"class":5541},[3603,24454,5583],{"class":5243},[3603,24456,22581],{"class":5537},[3603,24458,24459],{"class":3605,"line":16606},[3603,24460,11086],{"class":5541},[3603,24462,24463,24466,24468,24471,24473,24475],{"class":3605,"line":16623},[3603,24464,24465],{"class":5576},"        Name",[3603,24467,23248],{"class":5541},[3603,24469,24470],{"class":5576},"dto",[3603,24472,3453],{"class":5541},[3603,24474,24084],{"class":5576},[3603,24476,5604],{"class":5541},[3603,24478,24479,24482,24484,24486,24488,24490],{"class":3605,"line":16628},[3603,24480,24481],{"class":5576},"        Description",[3603,24483,5580],{"class":5541},[3603,24485,24470],{"class":5576},[3603,24487,3453],{"class":5541},[3603,24489,24093],{"class":5576},[3603,24491,5604],{"class":5541},[3603,24493,24494,24497,24499,24501,24503,24505],{"class":3605,"line":16639},[3603,24495,24496],{"class":5576},"        Price",[3603,24498,23365],{"class":5541},[3603,24500,24470],{"class":5576},[3603,24502,3453],{"class":5541},[3603,24504,24102],{"class":5576},[3603,24506,5604],{"class":5541},[3603,24508,24509],{"class":3605,"line":16650},[3603,24510,14926],{"class":5541},[3603,24512,24513,24516,24518,24520,24522,24525,24527,24530],{"class":3605,"line":16673},[3603,24514,24515],{"class":5576},"    db",[3603,24517,3453],{"class":5541},[3603,24519,22883],{"class":5576},[3603,24521,3453],{"class":5541},[3603,24523,24524],{"class":5220},"Add",[3603,24526,5707],{"class":5541},[3603,24528,24529],{"class":5576},"product",[3603,24531,5713],{"class":5541},[3603,24533,24534,24536,24538,24540,24543],{"class":3605,"line":16686},[3603,24535,23869],{"class":5243},[3603,24537,23999],{"class":5576},[3603,24539,3453],{"class":5541},[3603,24541,24542],{"class":5220},"SaveChangesAsync",[3603,24544,16490],{"class":5541},[3603,24546,24547],{"class":3605,"line":16691},[3603,24548,3628],{"emptyLinePlaceholder":3627},[3603,24550,24551,24553,24555,24557,24560,24562,24565,24567,24569,24571,24573,24575,24577,24579,24581,24583,24585,24587,24589,24591,24593,24595,24597],{"class":3605,"line":16696},[3603,24552,14931],{"class":5533},[3603,24554,24270],{"class":5576},[3603,24556,3453],{"class":5541},[3603,24558,24559],{"class":5220},"Created",[3603,24561,5707],{"class":5541},[3603,24563,24564],{"class":5224},"$\"\u002Fapi\u002Fproducts\u002F",[3603,24566,5618],{"class":5617},[3603,24568,24529],{"class":5576},[3603,24570,3453],{"class":5617},[3603,24572,24075],{"class":5576},[3603,24574,5645],{"class":5617},[3603,24576,6554],{"class":5224},[3603,24578,3470],{"class":5541},[3603,24580,5583],{"class":5243},[3603,24582,12973],{"class":5541},[3603,24584,24529],{"class":5576},[3603,24586,3453],{"class":5541},[3603,24588,24075],{"class":5576},[3603,24590,3470],{"class":5541},[3603,24592,24529],{"class":5576},[3603,24594,3453],{"class":5541},[3603,24596,24084],{"class":5576},[3603,24598,17529],{"class":5541},[3603,24600,24601],{"class":3605,"line":16701},[3603,24602,24173],{"class":5541},[3603,24604,24605],{"class":3605,"line":16707},[3603,24606,3628],{"emptyLinePlaceholder":3627},[3603,24608,24609],{"class":3605,"line":16731},[3603,24610,24611],{"class":5214},"\u002F\u002F ─── GET \u002Fapi\u002Fproducts\u002F{id}\u002Fupload-url?contentType=image\u002Fjpeg ─────────────────\n",[3603,24613,24614],{"class":3605,"line":16746},[3603,24615,24616],{"class":5214},"\u002F\u002F Генерує presigned PUT URL — фронтенд завантажує файл напряму в S3\n",[3603,24618,24619,24621,24623,24625,24627,24630],{"class":3605,"line":16751},[3603,24620,23822],{"class":5576},[3603,24622,3453],{"class":5541},[3603,24624,23982],{"class":5220},[3603,24626,5707],{"class":5541},[3603,24628,24629],{"class":5224},"\"\u002Fapi\u002Fproducts\u002F{id:int}\u002Fupload-url\"",[3603,24631,5604],{"class":5541},[3603,24633,24634,24637,24639,24641,24643,24645,24647,24649,24651,24653,24655,24657,24659,24661],{"class":3605,"line":16768},[3603,24635,24636],{"class":5243},"    async",[3603,24638,10425],{"class":5541},[3603,24640,12664],{"class":5243},[3603,24642,24206],{"class":5576},[3603,24644,3470],{"class":5541},[3603,24646,7598],{"class":5243},[3603,24648,12821],{"class":5576},[3603,24650,3470],{"class":5541},[3603,24652,22841],{"class":5537},[3603,24654,23999],{"class":5576},[3603,24656,3470],{"class":5541},[3603,24658,23713],{"class":5537},[3603,24660,24006],{"class":5576},[3603,24662,23201],{"class":5541},[3603,24664,24665],{"class":3605,"line":16773},[3603,24666,11086],{"class":5541},[3603,24668,24669,24671,24673,24675,24677,24679,24681,24683,24685,24687,24689,24691,24693,24695],{"class":3605,"line":16784},[3603,24670,15090],{"class":5533},[3603,24672,10425],{"class":5541},[3603,24674,5696],{"class":5243},[3603,24676,23999],{"class":5576},[3603,24678,3453],{"class":5541},[3603,24680,22883],{"class":5576},[3603,24682,3453],{"class":5541},[3603,24684,24244],{"class":5220},[3603,24686,5707],{"class":5541},[3603,24688,24249],{"class":5576},[3603,24690,15099],{"class":5541},[3603,24692,24254],{"class":5243},[3603,24694,24126],{"class":5243},[3603,24696,6924],{"class":5541},[3603,24698,24699,24701,24703,24705,24707],{"class":3605,"line":16795},[3603,24700,15266],{"class":5533},[3603,24702,24270],{"class":5576},[3603,24704,3453],{"class":5541},[3603,24706,24275],{"class":5220},[3603,24708,16490],{"class":5541},[3603,24710,24711],{"class":3605,"line":16818},[3603,24712,3628],{"emptyLinePlaceholder":3627},[3603,24714,24715],{"class":3605,"line":16833},[3603,24716,24717],{"class":5214},"        \u002F\u002F Приймаємо лише зображення — валідація до генерації URL\n",[3603,24719,24720,24722,24725,24727,24729,24732,24734,24736,24739,24741,24744],{"class":3605,"line":16842},[3603,24721,11091],{"class":5243},[3603,24723,24724],{"class":5576}," allowedTypes",[3603,24726,5580],{"class":5541},[3603,24728,5583],{"class":5243},[3603,24730,24731],{"class":5541},"[] { ",[3603,24733,12263],{"class":5224},[3603,24735,3470],{"class":5541},[3603,24737,24738],{"class":5224},"\"image\u002Fpng\"",[3603,24740,3470],{"class":5541},[3603,24742,24743],{"class":5224},"\"image\u002Fwebp\"",[3603,24745,24746],{"class":5541}," };\n",[3603,24748,24749,24751,24753,24756,24758,24761,24763,24765],{"class":3605,"line":16847},[3603,24750,15090],{"class":5533},[3603,24752,15093],{"class":5541},[3603,24754,24755],{"class":5576},"allowedTypes",[3603,24757,3453],{"class":5541},[3603,24759,24760],{"class":5220},"Contains",[3603,24762,5707],{"class":5541},[3603,24764,16116],{"class":5576},[3603,24766,24767],{"class":5541},"))\n",[3603,24769,24770,24772,24774,24776,24779,24781,24784,24786,24788,24790,24793,24795,24798,24800,24802,24805,24807],{"class":3605,"line":16852},[3603,24771,15266],{"class":5533},[3603,24773,24270],{"class":5576},[3603,24775,3453],{"class":5541},[3603,24777,24778],{"class":5220},"BadRequest",[3603,24780,5707],{"class":5541},[3603,24782,24783],{"class":5224},"$\"Allowed content types: ",[3603,24785,5618],{"class":5617},[3603,24787,7598],{"class":5243},[3603,24789,3453],{"class":5617},[3603,24791,24792],{"class":5220},"Join",[3603,24794,5707],{"class":5617},[3603,24796,24797],{"class":5224},"\", \"",[3603,24799,3470],{"class":5617},[3603,24801,24755],{"class":5576},[3603,24803,24804],{"class":5617},")}",[3603,24806,6554],{"class":5224},[3603,24808,5713],{"class":5541},[3603,24810,24811],{"class":3605,"line":16857},[3603,24812,3628],{"emptyLinePlaceholder":3627},[3603,24814,24815,24817,24819,24821,24823],{"class":3605,"line":16863},[3603,24816,11091],{"class":5243},[3603,24818,23125],{"class":5576},[3603,24820,5580],{"class":5541},[3603,24822,16116],{"class":5576},[3603,24824,24825],{"class":5533}," switch\n",[3603,24827,24828],{"class":3605,"line":16869},[3603,24829,5873],{"class":5541},[3603,24831,24832,24835,24837,24840],{"class":3605,"line":16875},[3603,24833,24834],{"class":5224},"            \"image\u002Fjpeg\"",[3603,24836,16476],{"class":5541},[3603,24838,24839],{"class":5224},"\".jpg\"",[3603,24841,5604],{"class":5541},[3603,24843,24844,24847,24850,24853],{"class":3605,"line":16881},[3603,24845,24846],{"class":5224},"            \"image\u002Fpng\"",[3603,24848,24849],{"class":5541},"  => ",[3603,24851,24852],{"class":5224},"\".png\"",[3603,24854,5604],{"class":5541},[3603,24856,24857,24860,24863,24866],{"class":3605,"line":16887},[3603,24858,24859],{"class":5243},"            _",[3603,24861,24862],{"class":5541},"            => ",[3603,24864,24865],{"class":5224},"\".webp\"",[3603,24867,5604],{"class":5541},[3603,24869,24870],{"class":3605,"line":16893},[3603,24871,11179],{"class":5541},[3603,24873,24874],{"class":3605,"line":16920},[3603,24875,3628],{"emptyLinePlaceholder":3627},[3603,24877,24878,24880,24882,24884,24886,24888,24891,24893,24895,24897,24899],{"class":3605,"line":16925},[3603,24879,11091],{"class":5243},[3603,24881,11071],{"class":5576},[3603,24883,23365],{"class":5541},[3603,24885,23713],{"class":5576},[3603,24887,3453],{"class":5541},[3603,24889,24890],{"class":5220},"BuildImageKey",[3603,24892,5707],{"class":5541},[3603,24894,24249],{"class":5576},[3603,24896,3470],{"class":5541},[3603,24898,23158],{"class":5576},[3603,24900,5713],{"class":5541},[3603,24902,24903,24905,24908,24910,24912,24914,24917,24919,24921,24923,24925],{"class":3605,"line":16946},[3603,24904,11091],{"class":5243},[3603,24906,24907],{"class":5576}," uploadUrl",[3603,24909,5580],{"class":5541},[3603,24911,24132],{"class":5576},[3603,24913,3453],{"class":5541},[3603,24915,24916],{"class":5220},"GetUploadUrl",[3603,24918,5707],{"class":5541},[3603,24920,11126],{"class":5576},[3603,24922,3470],{"class":5541},[3603,24924,16116],{"class":5576},[3603,24926,5713],{"class":5541},[3603,24928,24929],{"class":3605,"line":16963},[3603,24930,3628],{"emptyLinePlaceholder":3627},[3603,24932,24933,24935,24937,24939,24941,24943,24945,24947,24949,24951,24953,24955,24957,24959,24961],{"class":3605,"line":16968},[3603,24934,11308],{"class":5533},[3603,24936,24270],{"class":5576},[3603,24938,3453],{"class":5541},[3603,24940,24292],{"class":5220},[3603,24942,5707],{"class":5541},[3603,24944,5583],{"class":5243},[3603,24946,12973],{"class":5541},[3603,24948,12977],{"class":5576},[3603,24950,3470],{"class":5541},[3603,24952,11126],{"class":5576},[3603,24954,3470],{"class":5541},[3603,24956,12738],{"class":5576},[3603,24958,5580],{"class":5541},[3603,24960,12672],{"class":5943},[3603,24962,17529],{"class":5541},[3603,24964,24965],{"class":3605,"line":16979},[3603,24966,24168],{"class":5541},[3603,24968,24969],{"class":3605,"line":16990},[3603,24970,3628],{"emptyLinePlaceholder":3627},[3603,24972,24973],{"class":3605,"line":17003},[3603,24974,24975],{"class":5214},"\u002F\u002F ─── PUT \u002Fapi\u002Fproducts\u002F{id}\u002Fimage ─────────────────────────────────────────────\n",[3603,24977,24978],{"class":3605,"line":17009},[3603,24979,24980],{"class":5214},"\u002F\u002F Фронтенд викликає після успішного PUT у S3 — зберігаємо S3 key у БД\n",[3603,24982,24983,24985,24987,24990,24992,24995],{"class":3605,"line":17032},[3603,24984,23822],{"class":5576},[3603,24986,3453],{"class":5541},[3603,24988,24989],{"class":5220},"MapPut",[3603,24991,5707],{"class":5541},[3603,24993,24994],{"class":5224},"\"\u002Fapi\u002Fproducts\u002F{id:int}\u002Fimage\"",[3603,24996,5604],{"class":5541},[3603,24998,24999,25001,25003,25005,25007,25009,25012,25014,25016,25018,25020,25022,25024,25026],{"class":3605,"line":17042},[3603,25000,24636],{"class":5243},[3603,25002,10425],{"class":5541},[3603,25004,12664],{"class":5243},[3603,25006,24206],{"class":5576},[3603,25008,3470],{"class":5541},[3603,25010,25011],{"class":5537},"ConfirmImageDto",[3603,25013,24431],{"class":5576},[3603,25015,3470],{"class":5541},[3603,25017,22841],{"class":5537},[3603,25019,23999],{"class":5576},[3603,25021,3470],{"class":5541},[3603,25023,23713],{"class":5537},[3603,25025,24006],{"class":5576},[3603,25027,23201],{"class":5541},[3603,25029,25030],{"class":3605,"line":17047},[3603,25031,11086],{"class":5541},[3603,25033,25034,25036,25038,25040,25042,25044,25046,25048,25050,25052,25054,25056,25058,25060,25062,25064],{"class":3605,"line":17052},[3603,25035,15090],{"class":5533},[3603,25037,10425],{"class":5541},[3603,25039,5696],{"class":5243},[3603,25041,23999],{"class":5576},[3603,25043,3453],{"class":5541},[3603,25045,22883],{"class":5576},[3603,25047,3453],{"class":5541},[3603,25049,24244],{"class":5220},[3603,25051,5707],{"class":5541},[3603,25053,24249],{"class":5576},[3603,25055,15099],{"class":5541},[3603,25057,24254],{"class":5243},[3603,25059,24123],{"class":5243},[3603,25061,24259],{"class":5541},[3603,25063,24529],{"class":5576},[3603,25065,6924],{"class":5541},[3603,25067,25068,25070,25072,25074,25076],{"class":3605,"line":17057},[3603,25069,15266],{"class":5533},[3603,25071,24270],{"class":5576},[3603,25073,3453],{"class":5541},[3603,25075,24275],{"class":5220},[3603,25077,16490],{"class":5541},[3603,25079,25080],{"class":3605,"line":17063},[3603,25081,3628],{"emptyLinePlaceholder":3627},[3603,25083,25084],{"class":3605,"line":17069},[3603,25085,25086],{"class":5214},"        \u002F\u002F Старе зображення видаляємо з S3 — уникаємо orphan файлів\n",[3603,25088,25089,25091,25093,25095,25097,25099,25101,25103,25105],{"class":3605,"line":17075},[3603,25090,15090],{"class":5533},[3603,25092,10425],{"class":5541},[3603,25094,24529],{"class":5576},[3603,25096,3453],{"class":5541},[3603,25098,22712],{"class":5576},[3603,25100,24120],{"class":5243},[3603,25102,24123],{"class":5243},[3603,25104,24126],{"class":5243},[3603,25106,6924],{"class":5541},[3603,25108,25109,25112,25114,25116,25119,25121,25123,25125,25127],{"class":3605,"line":17081},[3603,25110,25111],{"class":5243},"            await",[3603,25113,24006],{"class":5576},[3603,25115,3453],{"class":5541},[3603,25117,25118],{"class":5220},"DeleteAsync",[3603,25120,5707],{"class":5541},[3603,25122,24529],{"class":5576},[3603,25124,3453],{"class":5541},[3603,25126,22712],{"class":5576},[3603,25128,5713],{"class":5541},[3603,25130,25131],{"class":3605,"line":17109},[3603,25132,3628],{"emptyLinePlaceholder":3627},[3603,25134,25135,25138,25140,25142,25144,25146,25148,25150],{"class":3605,"line":17114},[3603,25136,25137],{"class":5576},"        product",[3603,25139,3453],{"class":5541},[3603,25141,22712],{"class":5576},[3603,25143,5580],{"class":5541},[3603,25145,24470],{"class":5576},[3603,25147,3453],{"class":5541},[3603,25149,3558],{"class":5576},[3603,25151,5547],{"class":5541},[3603,25153,25154,25156,25158,25160,25162],{"class":3605,"line":17131},[3603,25155,16058],{"class":5243},[3603,25157,23999],{"class":5576},[3603,25159,3453],{"class":5541},[3603,25161,24542],{"class":5220},[3603,25163,16490],{"class":5541},[3603,25165,25166],{"class":3605,"line":17136},[3603,25167,3628],{"emptyLinePlaceholder":3627},[3603,25169,25170,25172,25174,25176,25178,25180,25182,25184,25187,25189,25191,25193,25195,25197,25199,25201,25203],{"class":3605,"line":17148},[3603,25171,11308],{"class":5533},[3603,25173,24270],{"class":5576},[3603,25175,3453],{"class":5541},[3603,25177,24292],{"class":5220},[3603,25179,5707],{"class":5541},[3603,25181,5583],{"class":5243},[3603,25183,12973],{"class":5541},[3603,25185,25186],{"class":5576},"imageUrl",[3603,25188,5580],{"class":5541},[3603,25190,24132],{"class":5576},[3603,25192,3453],{"class":5541},[3603,25194,24137],{"class":5220},[3603,25196,5707],{"class":5541},[3603,25198,24470],{"class":5576},[3603,25200,3453],{"class":5541},[3603,25202,3558],{"class":5576},[3603,25204,25205],{"class":5541},") });\n",[3603,25207,25208],{"class":3605,"line":17161},[3603,25209,24168],{"class":5541},[3603,25211,25212],{"class":3605,"line":17173},[3603,25213,3628],{"emptyLinePlaceholder":3627},[3603,25215,25216],{"class":3605,"line":17184},[3603,25217,25218],{"class":5214},"\u002F\u002F ─── DELETE \u002Fapi\u002Fproducts\u002F{id} ────────────────────────────────────────────────\n",[3603,25220,25221],{"class":3605,"line":17189},[3603,25222,25223],{"class":5214},"\u002F\u002F Видалення продукту разом із зображенням у S3\n",[3603,25225,25226,25228,25230,25233,25235,25237],{"class":3605,"line":17194},[3603,25227,23822],{"class":5576},[3603,25229,3453],{"class":5541},[3603,25231,25232],{"class":5220},"MapDelete",[3603,25234,5707],{"class":5541},[3603,25236,24195],{"class":5224},[3603,25238,5604],{"class":5541},[3603,25240,25241,25243,25245,25247,25249,25251,25253,25255,25257,25259,25261],{"class":3605,"line":20027},[3603,25242,24636],{"class":5243},[3603,25244,10425],{"class":5541},[3603,25246,12664],{"class":5243},[3603,25248,24206],{"class":5576},[3603,25250,3470],{"class":5541},[3603,25252,22841],{"class":5537},[3603,25254,23999],{"class":5576},[3603,25256,3470],{"class":5541},[3603,25258,23713],{"class":5537},[3603,25260,24006],{"class":5576},[3603,25262,23201],{"class":5541},[3603,25264,25266],{"class":3605,"line":25265},134,[3603,25267,11086],{"class":5541},[3603,25269,25271,25273,25275,25277,25279,25281,25283,25285,25287,25289,25291,25293,25295,25297,25299,25301],{"class":3605,"line":25270},135,[3603,25272,15090],{"class":5533},[3603,25274,10425],{"class":5541},[3603,25276,5696],{"class":5243},[3603,25278,23999],{"class":5576},[3603,25280,3453],{"class":5541},[3603,25282,22883],{"class":5576},[3603,25284,3453],{"class":5541},[3603,25286,24244],{"class":5220},[3603,25288,5707],{"class":5541},[3603,25290,24249],{"class":5576},[3603,25292,15099],{"class":5541},[3603,25294,24254],{"class":5243},[3603,25296,24123],{"class":5243},[3603,25298,24259],{"class":5541},[3603,25300,24529],{"class":5576},[3603,25302,6924],{"class":5541},[3603,25304,25306,25308,25310,25312,25314],{"class":3605,"line":25305},136,[3603,25307,15266],{"class":5533},[3603,25309,24270],{"class":5576},[3603,25311,3453],{"class":5541},[3603,25313,24275],{"class":5220},[3603,25315,16490],{"class":5541},[3603,25317,25319],{"class":3605,"line":25318},137,[3603,25320,3628],{"emptyLinePlaceholder":3627},[3603,25322,25324],{"class":3605,"line":25323},138,[3603,25325,25326],{"class":5214},"        \u002F\u002F Спочатку видаляємо з S3, потім з БД\n",[3603,25328,25330,25332,25334,25336,25338,25340,25342,25344,25346],{"class":3605,"line":25329},139,[3603,25331,15090],{"class":5533},[3603,25333,10425],{"class":5541},[3603,25335,24529],{"class":5576},[3603,25337,3453],{"class":5541},[3603,25339,22712],{"class":5576},[3603,25341,24120],{"class":5243},[3603,25343,24123],{"class":5243},[3603,25345,24126],{"class":5243},[3603,25347,6924],{"class":5541},[3603,25349,25351,25353,25355,25357,25359,25361,25363,25365,25367],{"class":3605,"line":25350},140,[3603,25352,25111],{"class":5243},[3603,25354,24006],{"class":5576},[3603,25356,3453],{"class":5541},[3603,25358,25118],{"class":5220},[3603,25360,5707],{"class":5541},[3603,25362,24529],{"class":5576},[3603,25364,3453],{"class":5541},[3603,25366,22712],{"class":5576},[3603,25368,5713],{"class":5541},[3603,25370,25372],{"class":3605,"line":25371},141,[3603,25373,3628],{"emptyLinePlaceholder":3627},[3603,25375,25377,25380,25382,25384,25386,25389,25391,25393],{"class":3605,"line":25376},142,[3603,25378,25379],{"class":5576},"        db",[3603,25381,3453],{"class":5541},[3603,25383,22883],{"class":5576},[3603,25385,3453],{"class":5541},[3603,25387,25388],{"class":5220},"Remove",[3603,25390,5707],{"class":5541},[3603,25392,24529],{"class":5576},[3603,25394,5713],{"class":5541},[3603,25396,25398,25400,25402,25404,25406],{"class":3605,"line":25397},143,[3603,25399,16058],{"class":5243},[3603,25401,23999],{"class":5576},[3603,25403,3453],{"class":5541},[3603,25405,24542],{"class":5220},[3603,25407,16490],{"class":5541},[3603,25409,25411],{"class":3605,"line":25410},144,[3603,25412,3628],{"emptyLinePlaceholder":3627},[3603,25414,25416,25418,25420,25422,25425],{"class":3605,"line":25415},145,[3603,25417,11308],{"class":5533},[3603,25419,24270],{"class":5576},[3603,25421,3453],{"class":5541},[3603,25423,25424],{"class":5220},"NoContent",[3603,25426,16490],{"class":5541},[3603,25428,25430],{"class":3605,"line":25429},146,[3603,25431,24168],{"class":5541},[3603,25433,25435],{"class":3605,"line":25434},147,[3603,25436,3628],{"emptyLinePlaceholder":3627},[3603,25438,25440,25442,25444,25447],{"class":3605,"line":25439},148,[3603,25441,23822],{"class":5576},[3603,25443,3453],{"class":5541},[3603,25445,25446],{"class":5220},"Run",[3603,25448,16490],{"class":5541},[3603,25450,25452],{"class":3605,"line":25451},149,[3603,25453,3628],{"emptyLinePlaceholder":3627},[3603,25455,25457],{"class":3605,"line":25456},150,[3603,25458,25459],{"class":5214},"\u002F\u002F ─── DTOs ─────────────────────────────────────────────────────────────────────\n",[3603,25461,25463,25466,25469,25471,25473,25476,25478,25480,25483,25485,25488,25490],{"class":3605,"line":25462},151,[3603,25464,25465],{"class":5243},"record",[3603,25467,25468],{"class":5537}," CreateProductDto",[3603,25470,5707],{"class":5541},[3603,25472,7598],{"class":5243},[3603,25474,25475],{"class":5576}," Name",[3603,25477,3470],{"class":5541},[3603,25479,7598],{"class":5243},[3603,25481,25482],{"class":5576}," Description",[3603,25484,3470],{"class":5541},[3603,25486,25487],{"class":5243},"decimal",[3603,25489,22675],{"class":5576},[3603,25491,5713],{"class":5541},[3603,25493,25495,25497,25500,25502,25504,25507],{"class":3605,"line":25494},152,[3603,25496,25465],{"class":5243},[3603,25498,25499],{"class":5537}," ConfirmImageDto",[3603,25501,5707],{"class":5541},[3603,25503,7598],{"class":5243},[3603,25505,25506],{"class":5576}," Key",[3603,25508,5713],{"class":5541},[3405,25510],{},[3412,25512,25514],{"id":25513},"крок-6-конфігурація","Крок 6: Конфігурація",[3353,25516,25517,5629],{},[3356,25518,25519],{},[3432,25520,15549],{},[3561,25522,25524],{"className":5851,"code":25523,"language":5853,"meta":3569,"style":3569},"{\n    \"ConnectionStrings\": {\n        \"Default\": \"Data Source=products.db\"\n    },\n    \"AWS\": {\n        \"Region\": \"eu-central-1\"\n    },\n    \"S3\": {\n        \"BucketName\": \"product-images-2024\"\n    },\n    \"Logging\": {\n        \"LogLevel\": {\n            \"Default\": \"Information\"\n        }\n    }\n}\n",[3432,25525,25526,25530,25537,25547,25551,25557,25565,25569,25575,25583,25587,25594,25601,25611,25615,25619],{"__ignoreMap":3569},[3603,25527,25528],{"class":3605,"line":3606},[3603,25529,5591],{"class":5541},[3603,25531,25532,25535],{"class":3605,"line":3612},[3603,25533,25534],{"class":5864},"    \"ConnectionStrings\"",[3603,25536,5906],{"class":5541},[3603,25538,25539,25542,25544],{"class":3605,"line":3618},[3603,25540,25541],{"class":5864},"        \"Default\"",[3603,25543,5881],{"class":5541},[3603,25545,25546],{"class":5224},"\"Data Source=products.db\"\n",[3603,25548,25549],{"class":3605,"line":3624},[3603,25550,15697],{"class":5541},[3603,25552,25553,25555],{"class":3605,"line":3631},[3603,25554,15681],{"class":5864},[3603,25556,5906],{"class":5541},[3603,25558,25559,25561,25563],{"class":3605,"line":3637},[3603,25560,15688],{"class":5864},[3603,25562,5881],{"class":5541},[3603,25564,6520],{"class":5224},[3603,25566,25567],{"class":3605,"line":3643},[3603,25568,15697],{"class":5541},[3603,25570,25571,25573],{"class":3605,"line":3649},[3603,25572,15702],{"class":5864},[3603,25574,5906],{"class":5541},[3603,25576,25577,25579,25581],{"class":3605,"line":3655},[3603,25578,15709],{"class":5864},[3603,25580,5881],{"class":5541},[3603,25582,22244],{"class":5224},[3603,25584,25585],{"class":3605,"line":3661},[3603,25586,15697],{"class":5541},[3603,25588,25589,25592],{"class":3605,"line":3667},[3603,25590,25591],{"class":5864},"    \"Logging\"",[3603,25593,5906],{"class":5541},[3603,25595,25596,25599],{"class":3605,"line":3673},[3603,25597,25598],{"class":5864},"        \"LogLevel\"",[3603,25600,5906],{"class":5541},[3603,25602,25603,25606,25608],{"class":3605,"line":3678},[3603,25604,25605],{"class":5864},"            \"Default\"",[3603,25607,5881],{"class":5541},[3603,25609,25610],{"class":5224},"\"Information\"\n",[3603,25612,25613],{"class":3605,"line":3684},[3603,25614,6050],{"class":5541},[3603,25616,25617],{"class":3605,"line":3690},[3603,25618,3664],{"class":5541},[3603,25620,25621],{"class":3605,"line":3696},[3603,25622,3670],{"class":5541},[3353,25624,25625,25626,25631],{},"AWS credentials — ",[3356,25627,25628,25629],{},"ніколи не вказуйте в ",[3432,25630,15549],{},". SDK шукає їх у такому порядку:",[6382,25633,25634,25641,25650],{},[3372,25635,25636,25638,25639,6430],{},[3432,25637,15648],{}," (для локальної розробки після ",[3432,25640,15652],{},[3372,25642,25643,25644,3470,25647],{},"Змінні середовища ",[3432,25645,25646],{},"AWS_ACCESS_KEY_ID",[3432,25648,25649],{},"AWS_SECRET_ACCESS_KEY",[3372,25651,25652,25653,25656,25657,25660],{},"IAM Role — через ",[3356,25654,25655],{},"Instance Profile"," (EC2) або ",[3356,25658,25659],{},"Task Role"," (ECS) у продакшні — рекомендований спосіб",[3353,25662,25663],{},[3356,25664,25665],{},"Продакшн (рекомендовано): IAM Role без жодних ключів у коді",[3353,25667,25668],{},[3356,25669,25670],{},"IAM Role vs Instance Profile — в чому різниця?",[3353,25672,25673,25676],{},[3356,25674,25675],{},"IAM Role"," — це набір дозволів (policy). Сама по собі вона нічого не робить, це просто «документ із правилами»: «дозволено робити PutObject у bucket X».",[3353,25678,25679,25681],{},[3356,25680,25655],{}," — це «контейнер», через який IAM Role прикріплюється до EC2 інстансу. EC2 не може взяти Role напряму — йому потрібен Instance Profile як проміжна ланка.",[3561,25683,25686],{"className":25684,"code":25685,"language":3566},[3564],"IAM Role (дозволи) → Instance Profile (контейнер) → EC2 Instance\n",[3432,25687,25685],{"__ignoreMap":3569},[3397,25689,25690,25691,25693,25694,25697],{},"Коли створюєте роль через ",[3356,25692,6375],{}," з типом EC2 — консоль автоматично створює Instance Profile з тим самим іменем. Тому ця деталь часто непомітна. Через ",[3356,25695,25696],{},"CLI"," — треба створювати окремо, що й показано нижче.",[3353,25699,25700],{},"EC2 — прикріпіть Instance Profile до інстансу:",[3561,25702,25704],{"className":5205,"code":25703,"language":5207,"meta":3569,"style":3569},"# 1. Створити IAM Role з потрібними S3 дозволами\naws iam create-role \\\n    --role-name product-image-manager-role \\\n    --assume-role-policy-document '{\n        \"Version\": \"2012-10-17\",\n        \"Statement\": [{\n            \"Effect\": \"Allow\",\n            \"Principal\": { \"Service\": \"ec2.amazonaws.com\" },\n            \"Action\": \"sts:AssumeRole\"\n        }]\n    }'\n\n# 2. Прикріпити S3 policy (лише потрібний bucket — принцип найменших привілеїв)\naws iam put-role-policy \\\n    --role-name product-image-manager-role \\\n    --policy-name s3-product-images \\\n    --policy-document '{\n        \"Version\": \"2012-10-17\",\n        \"Statement\": [{\n            \"Effect\": \"Allow\",\n            \"Action\": [\"s3:PutObject\", \"s3:GetObject\", \"s3:DeleteObject\"],\n            \"Resource\": \"arn:aws:s3:::product-images-2024\u002F*\"\n        }]\n    }'\n\n# 3. Створити Instance Profile та прикріпити Role\n# Instance Profile — це окрутня обгортка для ролі, потрібна саме для EC2.\n# create-instance-profile: реєструємо профіль у IAM\n# add-role-to-instance-profile: вкладаємо роль у профіль (один профіль = одна роль)\naws iam create-instance-profile --instance-profile-name product-image-manager-profile\naws iam add-role-to-instance-profile \\\n    --instance-profile-name product-image-manager-profile \\\n    --role-name product-image-manager-role\n\n# 4. Прикріпити до EC2 інстансу\naws ec2 associate-iam-instance-profile \\\n    --instance-id i-0abc123def456 \\\n    --iam-instance-profile Name=product-image-manager-profile\n",[3432,25705,25706,25711,25723,25733,25740,25745,25750,25754,25759,25764,25768,25773,25777,25782,25793,25801,25811,25818,25822,25826,25830,25835,25840,25844,25848,25852,25857,25862,25867,25872,25887,25898,25908,25915,25919,25924,25936,25946],{"__ignoreMap":3569},[3603,25707,25708],{"class":3605,"line":3606},[3603,25709,25710],{"class":5214},"# 1. Створити IAM Role з потрібними S3 дозволами\n",[3603,25712,25713,25715,25718,25721],{"class":3605,"line":3612},[3603,25714,5221],{"class":5220},[3603,25716,25717],{"class":5224}," iam",[3603,25719,25720],{"class":5224}," create-role",[3603,25722,5238],{"class":5237},[3603,25724,25725,25728,25731],{"class":3605,"line":3618},[3603,25726,25727],{"class":5243},"    --role-name",[3603,25729,25730],{"class":5224}," product-image-manager-role",[3603,25732,5238],{"class":5237},[3603,25734,25735,25738],{"class":3605,"line":3624},[3603,25736,25737],{"class":5243},"    --assume-role-policy-document",[3603,25739,5442],{"class":5224},[3603,25741,25742],{"class":3605,"line":3631},[3603,25743,25744],{"class":5224},"        \"Version\": \"2012-10-17\",\n",[3603,25746,25747],{"class":3605,"line":3637},[3603,25748,25749],{"class":5224},"        \"Statement\": [{\n",[3603,25751,25752],{"class":3605,"line":3643},[3603,25753,9721],{"class":5224},[3603,25755,25756],{"class":3605,"line":3649},[3603,25757,25758],{"class":5224},"            \"Principal\": { \"Service\": \"ec2.amazonaws.com\" },\n",[3603,25760,25761],{"class":3605,"line":3655},[3603,25762,25763],{"class":5224},"            \"Action\": \"sts:AssumeRole\"\n",[3603,25765,25766],{"class":3605,"line":3661},[3603,25767,11633],{"class":5224},[3603,25769,25770],{"class":3605,"line":3667},[3603,25771,25772],{"class":5224},"    }'\n",[3603,25774,25775],{"class":3605,"line":3673},[3603,25776,3628],{"emptyLinePlaceholder":3627},[3603,25778,25779],{"class":3605,"line":3678},[3603,25780,25781],{"class":5214},"# 2. Прикріпити S3 policy (лише потрібний bucket — принцип найменших привілеїв)\n",[3603,25783,25784,25786,25788,25791],{"class":3605,"line":3684},[3603,25785,5221],{"class":5220},[3603,25787,25717],{"class":5224},[3603,25789,25790],{"class":5224}," put-role-policy",[3603,25792,5238],{"class":5237},[3603,25794,25795,25797,25799],{"class":3605,"line":3690},[3603,25796,25727],{"class":5243},[3603,25798,25730],{"class":5224},[3603,25800,5238],{"class":5237},[3603,25802,25803,25806,25809],{"class":3605,"line":3696},[3603,25804,25805],{"class":5243},"    --policy-name",[3603,25807,25808],{"class":5224}," s3-product-images",[3603,25810,5238],{"class":5237},[3603,25812,25813,25816],{"class":3605,"line":3702},[3603,25814,25815],{"class":5243},"    --policy-document",[3603,25817,5442],{"class":5224},[3603,25819,25820],{"class":3605,"line":3708},[3603,25821,25744],{"class":5224},[3603,25823,25824],{"class":3605,"line":4667},[3603,25825,25749],{"class":5224},[3603,25827,25828],{"class":3605,"line":4673},[3603,25829,9721],{"class":5224},[3603,25831,25832],{"class":3605,"line":4679},[3603,25833,25834],{"class":5224},"            \"Action\": [\"s3:PutObject\", \"s3:GetObject\", \"s3:DeleteObject\"],\n",[3603,25836,25837],{"class":3605,"line":4685},[3603,25838,25839],{"class":5224},"            \"Resource\": \"arn:aws:s3:::product-images-2024\u002F*\"\n",[3603,25841,25842],{"class":3605,"line":4691},[3603,25843,11633],{"class":5224},[3603,25845,25846],{"class":3605,"line":4696},[3603,25847,25772],{"class":5224},[3603,25849,25850],{"class":3605,"line":4701},[3603,25851,3628],{"emptyLinePlaceholder":3627},[3603,25853,25854],{"class":3605,"line":4707},[3603,25855,25856],{"class":5214},"# 3. Створити Instance Profile та прикріпити Role\n",[3603,25858,25859],{"class":3605,"line":4713},[3603,25860,25861],{"class":5214},"# Instance Profile — це окрутня обгортка для ролі, потрібна саме для EC2.\n",[3603,25863,25864],{"class":3605,"line":4719},[3603,25865,25866],{"class":5214},"# create-instance-profile: реєструємо профіль у IAM\n",[3603,25868,25869],{"class":3605,"line":4724},[3603,25870,25871],{"class":5214},"# add-role-to-instance-profile: вкладаємо роль у профіль (один профіль = одна роль)\n",[3603,25873,25874,25876,25878,25881,25884],{"class":3605,"line":4730},[3603,25875,5221],{"class":5220},[3603,25877,25717],{"class":5224},[3603,25879,25880],{"class":5224}," create-instance-profile",[3603,25882,25883],{"class":5243}," --instance-profile-name",[3603,25885,25886],{"class":5224}," product-image-manager-profile\n",[3603,25888,25889,25891,25893,25896],{"class":3605,"line":4736},[3603,25890,5221],{"class":5220},[3603,25892,25717],{"class":5224},[3603,25894,25895],{"class":5224}," add-role-to-instance-profile",[3603,25897,5238],{"class":5237},[3603,25899,25900,25903,25906],{"class":3605,"line":4742},[3603,25901,25902],{"class":5243},"    --instance-profile-name",[3603,25904,25905],{"class":5224}," product-image-manager-profile",[3603,25907,5238],{"class":5237},[3603,25909,25910,25912],{"class":3605,"line":4748},[3603,25911,25727],{"class":5243},[3603,25913,25914],{"class":5224}," product-image-manager-role\n",[3603,25916,25917],{"class":3605,"line":4753},[3603,25918,3628],{"emptyLinePlaceholder":3627},[3603,25920,25921],{"class":3605,"line":4758},[3603,25922,25923],{"class":5214},"# 4. Прикріпити до EC2 інстансу\n",[3603,25925,25926,25928,25931,25934],{"class":3605,"line":6363},[3603,25927,5221],{"class":5220},[3603,25929,25930],{"class":5224}," ec2",[3603,25932,25933],{"class":5224}," associate-iam-instance-profile",[3603,25935,5238],{"class":5237},[3603,25937,25938,25941,25944],{"class":3605,"line":6368},[3603,25939,25940],{"class":5243},"    --instance-id",[3603,25942,25943],{"class":5224}," i-0abc123def456",[3603,25945,5238],{"class":5237},[3603,25947,25948,25951],{"class":3605,"line":6830},[3603,25949,25950],{"class":5243},"    --iam-instance-profile",[3603,25952,25953],{"class":5224}," Name=product-image-manager-profile\n",[3353,25955,25956],{},[3356,25957,25958],{},"Як EC2 отримує credentials після прикріплення профілю?",[3353,25960,25961,25962,25965,25966,25969],{},"Після прикріплення Instance Profile, EC2 автоматично отримує доступ до ",[3356,25963,25964],{},"IMDS (Instance Metadata Service)"," — спеціальний HTTP-ендпоінт, доступний тільки зсередини інстансу за адресою ",[3432,25967,25968],{},"http:\u002F\u002F169.254.169.254",". AWS SDK робить запит до нього і отримує тимчасові credentials, які автоматично оновлюються кожні ~1 годину.",[4801,25971,25973,25980,25988,25996,25999,26006,26013,26026,26037,26049,26064,26068,26071,26078],{"title":25972},"Як AWS SDK отримує credentials через IMDS (відбувається автоматично)",[4805,25974,25976],{"className":25975},[3605],[3603,25977,25979],{"className":25978},[4811],"# SDK (і boto3, і AWSSDK.NET) робить це приховано від вас",[4805,25981,25983,25987],{"className":25982},[3605],[3603,25984,25986],{"className":25985},[4819],"SDK → IMDS:","  GET http:\u002F\u002F169.254.169.254\u002Flatest\u002Fmeta-data\u002Fiam\u002Fsecurity-credentials\u002F",[4805,25989,25991,25995],{"className":25990},[3605],[3603,25992,25994],{"className":25993},[4819],"IMDS → SDK:","  product-image-manager-role",[4805,25997,4841],{"className":25998},[3605],[4805,26000,26002,26005],{"className":26001},[3605],[3603,26003,25986],{"className":26004},[4819],"  GET http:\u002F\u002F169.254.169.254\u002Flatest\u002Fmeta-data\u002Fiam\u002Fsecurity-credentials\u002Fproduct-image-manager-role",[4805,26007,26009,26012],{"className":26008},[3605],[3603,26010,25994],{"className":26011},[4819],"  {",[4805,26014,26016,26017,26021,26022,7063],{"className":26015},[3605],"               ",[3603,26018,26020],{"className":26019},[7044],"\"AccessKeyId\"",":     ",[3603,26023,26025],{"className":26024},[4811],"\"ASIA...тимчасовий ключ\"",[4805,26027,26016,26029,5881,26033,7063],{"className":26028},[3605],[3603,26030,26032],{"className":26031},[7044],"\"SecretAccessKey\"",[3603,26034,26036],{"className":26035},[4811],"\"****\"",[4805,26038,26016,26040,26044,26045,7063],{"className":26039},[3605],[3603,26041,26043],{"className":26042},[7044],"\"Token\"",":           ",[3603,26046,26048],{"className":26047},[4811],"\"IQoJ...session token\"",[4805,26050,26016,26052,26055,26056,7040,26060],{"className":26051},[3605],[3603,26053,8190],{"className":26054},[7044],":      ",[3603,26057,26059],{"className":26058},[4819],"\"2026-05-28T15:30:00Z\"",[3603,26061,26063],{"className":26062},[4811],"← оновлюється кожну годину",[4805,26065,26067],{"className":26066},[3605],"             }",[4805,26069,4841],{"className":26070},[3605],[4805,26072,26074],{"className":26073},[3605],[3603,26075,26077],{"className":26076},[4811],"# SDK кешує ці credentials і перезапитує за 5 хвилин до закінчення",[4805,26079,26081],{"className":26080},[3605],[3603,26082,26084],{"className":26083},[4811],"# Жоден ключ не зберігається на диску чи в коді — тільки в пам'яті процесу",[3353,26086,26087,26088,26091,26092,9535],{},"У коді — нічого змінювати не потрібно. ",[3432,26089,26090],{},"AddAWSService\u003CIAmazonS3>()"," автоматично отримає тимчасові credentials через IMDS (",[3432,26093,25968],{},[3353,26095,26096,26097,26100],{},"ECS — вкажіть ",[3432,26098,26099],{},"taskRoleArn"," у Task Definition:",[3561,26102,26104],{"className":5851,"code":26103,"language":5853,"meta":3569,"style":3569},"{\n    \"family\": \"product-image-manager\",\n    \"taskRoleArn\": \"arn:aws:iam::123456789012:role\u002Fproduct-image-manager-role\",\n    \"containerDefinitions\": [\n        {\n            \"name\": \"api\",\n            \"image\": \"your-ecr-repo\u002Fproduct-image-manager:latest\",\n            \"portMappings\": [{ \"containerPort\": 5000 }]\n        }\n    ]\n}\n",[3432,26105,26106,26110,26122,26134,26141,26145,26157,26169,26188,26192,26196],{"__ignoreMap":3569},[3603,26107,26108],{"class":3605,"line":3606},[3603,26109,5591],{"class":5541},[3603,26111,26112,26115,26117,26120],{"class":3605,"line":3612},[3603,26113,26114],{"class":5864},"    \"family\"",[3603,26116,5881],{"class":5541},[3603,26118,26119],{"class":5224},"\"product-image-manager\"",[3603,26121,5604],{"class":5541},[3603,26123,26124,26127,26129,26132],{"class":3605,"line":3618},[3603,26125,26126],{"class":5864},"    \"taskRoleArn\"",[3603,26128,5881],{"class":5541},[3603,26130,26131],{"class":5224},"\"arn:aws:iam::123456789012:role\u002Fproduct-image-manager-role\"",[3603,26133,5604],{"class":5541},[3603,26135,26136,26139],{"class":3605,"line":3624},[3603,26137,26138],{"class":5864},"    \"containerDefinitions\"",[3603,26140,5868],{"class":5541},[3603,26142,26143],{"class":3605,"line":3631},[3603,26144,5873],{"class":5541},[3603,26146,26147,26150,26152,26155],{"class":3605,"line":3637},[3603,26148,26149],{"class":5864},"            \"name\"",[3603,26151,5881],{"class":5541},[3603,26153,26154],{"class":5224},"\"api\"",[3603,26156,5604],{"class":5541},[3603,26158,26159,26162,26164,26167],{"class":3605,"line":3643},[3603,26160,26161],{"class":5864},"            \"image\"",[3603,26163,5881],{"class":5541},[3603,26165,26166],{"class":5224},"\"your-ecr-repo\u002Fproduct-image-manager:latest\"",[3603,26168,5604],{"class":5541},[3603,26170,26171,26174,26177,26180,26182,26185],{"class":3605,"line":3649},[3603,26172,26173],{"class":5864},"            \"portMappings\"",[3603,26175,26176],{"class":5541},": [{ ",[3603,26178,26179],{"class":5864},"\"containerPort\"",[3603,26181,5881],{"class":5541},[3603,26183,26184],{"class":5943},"5000",[3603,26186,26187],{"class":5541}," }]\n",[3603,26189,26190],{"class":3605,"line":3655},[3603,26191,6050],{"class":5541},[3603,26193,26194],{"class":3605,"line":3661},[3603,26195,6055],{"class":5541},[3603,26197,26198],{"class":3605,"line":3667},[3603,26199,3670],{"class":5541},[3353,26201,26202],{},"SDK отримає credentials через ECS Task Metadata endpoint — так само автоматично.",[3353,26204,26205],{},[3356,26206,26207,26208],{},"Локальна розробка (альтернатива): статичні ключі через ",[3432,26209,15652],{},[3561,26211,26213],{"className":5205,"code":26212,"language":5207,"meta":3569,"style":3569},"aws configure\n# AWS Access Key ID:     AKIA...\n# AWS Secret Access Key: ****...\n# Default region:        eu-central-1\n# Default output format: json\n",[3432,26214,26215,26222,26227,26232,26237],{"__ignoreMap":3569},[3603,26216,26217,26219],{"class":3605,"line":3606},[3603,26218,5221],{"class":5220},[3603,26220,26221],{"class":5224}," configure\n",[3603,26223,26224],{"class":3605,"line":3612},[3603,26225,26226],{"class":5214},"# AWS Access Key ID:     AKIA...\n",[3603,26228,26229],{"class":3605,"line":3618},[3603,26230,26231],{"class":5214},"# AWS Secret Access Key: ****...\n",[3603,26233,26234],{"class":3605,"line":3624},[3603,26235,26236],{"class":5214},"# Default region:        eu-central-1\n",[3603,26238,26239],{"class":3605,"line":3631},[3603,26240,26241],{"class":5214},"# Default output format: json\n",[3405,26243],{},[3412,26245,26247],{"id":26246},"крок-7-демонстрація-повний-flow","Крок 7: Демонстрація (повний flow)",[3561,26249,26251],{"className":5205,"code":26250,"language":5207,"meta":3569,"style":3569},"dotnet run\n# http:\u002F\u002Flocalhost:5000\u002Fswagger — Swagger UI\n",[3432,26252,26253,26260],{"__ignoreMap":3569},[3603,26254,26255,26257],{"class":3605,"line":3606},[3603,26256,15564],{"class":5220},[3603,26258,26259],{"class":5224}," run\n",[3603,26261,26262],{"class":3605,"line":3612},[3603,26263,26264],{"class":5214},"# http:\u002F\u002Flocalhost:5000\u002Fswagger — Swagger UI\n",[3353,26266,26267],{},[3356,26268,26269],{},"Кроки через curl:",[3561,26271,26273],{"className":5205,"code":26272,"language":5207,"meta":3569,"style":3569},"BASE=\"http:\u002F\u002Flocalhost:5000\"\n\n# ── 1. Створити продукт ──────────────────────────────────────────────────────\ncurl -s -X POST \"$BASE\u002Fapi\u002Fproducts\" \\\n    -H \"Content-Type: application\u002Fjson\" \\\n    -d '{\"name\":\"Gaming Laptop\",\"description\":\"RTX 4090, 32GB RAM\",\"price\":2499.99}'\n",[3432,26274,26275,26285,26289,26294,26317,26327],{"__ignoreMap":3569},[3603,26276,26277,26280,26282],{"class":3605,"line":3606},[3603,26278,26279],{"class":5576},"BASE",[3603,26281,6507],{"class":5541},[3603,26283,26284],{"class":5224},"\"http:\u002F\u002Flocalhost:5000\"\n",[3603,26286,26287],{"class":3605,"line":3612},[3603,26288,3628],{"emptyLinePlaceholder":3627},[3603,26290,26291],{"class":3605,"line":3618},[3603,26292,26293],{"class":5214},"# ── 1. Створити продукт ──────────────────────────────────────────────────────\n",[3603,26295,26296,26298,26301,26304,26307,26309,26312,26315],{"class":3605,"line":3624},[3603,26297,21782],{"class":5220},[3603,26299,26300],{"class":5243}," -s",[3603,26302,26303],{"class":5243}," -X",[3603,26305,26306],{"class":5224}," POST",[3603,26308,6548],{"class":5224},[3603,26310,26311],{"class":5576},"$BASE",[3603,26313,26314],{"class":5224},"\u002Fapi\u002Fproducts\"",[3603,26316,5238],{"class":5237},[3603,26318,26319,26322,26325],{"class":3605,"line":3631},[3603,26320,26321],{"class":5243},"    -H",[3603,26323,26324],{"class":5224}," \"Content-Type: application\u002Fjson\"",[3603,26326,5238],{"class":5237},[3603,26328,26329,26332],{"class":3605,"line":3637},[3603,26330,26331],{"class":5243},"    -d",[3603,26333,26334],{"class":5224}," '{\"name\":\"Gaming Laptop\",\"description\":\"RTX 4090, 32GB RAM\",\"price\":2499.99}'\n",[4801,26336,26338,26347,26350,26357,26365],{"title":26337},"POST \u002Fapi\u002Fproducts",[4805,26339,26341,4841,26344],{"className":26340},[3605],[3603,26342,7030],{"className":26343},[7029],[3356,26345,26346],{},"curl -X POST ...\u002Fapi\u002Fproducts -d '{\"name\":\"Gaming Laptop\",...}'",[4805,26348,5618],{"className":26349},[3605],[4805,26351,26353,26354,7063],{"className":26352},[3605],"  \"id\": ",[3603,26355,15161],{"className":26356},[7085],[4805,26358,26360,26361],{"className":26359},[3605],"  \"name\": ",[3603,26362,26364],{"className":26363},[4811],"\"Gaming Laptop\"",[4805,26366,5645],{"className":26367},[3605],[3561,26369,26371],{"className":5205,"code":26370,"language":5207,"meta":3569,"style":3569},"# ── 2. Отримати Presigned PUT URL для завантаження зображення ────────────────\ncurl -s \"$BASE\u002Fapi\u002Fproducts\u002F1\u002Fupload-url?contentType=image\u002Fjpeg\"\n",[3432,26372,26373,26378],{"__ignoreMap":3569},[3603,26374,26375],{"class":3605,"line":3606},[3603,26376,26377],{"class":5214},"# ── 2. Отримати Presigned PUT URL для завантаження зображення ────────────────\n",[3603,26379,26380,26382,26384,26386,26388],{"class":3605,"line":3612},[3603,26381,21782],{"class":5220},[3603,26383,26300],{"class":5243},[3603,26385,6548],{"class":5224},[3603,26387,26311],{"class":5576},[3603,26389,26390],{"class":5224},"\u002Fapi\u002Fproducts\u002F1\u002Fupload-url?contentType=image\u002Fjpeg\"\n",[4801,26392,26394,26397,26405,26413,26417],{"title":26393},"GET \u002Fapi\u002Fproducts\u002F1\u002Fupload-url",[4805,26395,5618],{"className":26396},[3605],[4805,26398,26400,26401,7063],{"className":26399},[3605],"  \"uploadUrl\": ",[3603,26402,26404],{"className":26403},[7085],"\"https:\u002F\u002Fproduct-images-2024.s3.eu-central-1.amazonaws.com\u002Fproducts\u002F1\u002Ff3a2...jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&...\"",[4805,26406,26408,26409,7063],{"className":26407},[3605],"  \"key\": ",[3603,26410,26412],{"className":26411},[4811],"\"products\u002F1\u002Ff3a2c8d1-4b5e-4f6a-8c9d-1e2f3a4b5c6d.jpg\"",[4805,26414,26416],{"className":26415},[3605],"  \"expiresInMinutes\": 15",[4805,26418,5645],{"className":26419},[3605],[3561,26421,26423],{"className":5205,"code":26422,"language":5207,"meta":3569,"style":3569},"# ── 3. Завантажити зображення НАПРЯМУ в S3 (без ASP.NET!) ────────────────────\nUPLOAD_URL=\"https:\u002F\u002Fproduct-images-2024.s3.eu-central-1.amazonaws.com\u002Fproducts\u002F1\u002Ff3a2...jpg?...\"\nS3_KEY=\"products\u002F1\u002Ff3a2c8d1-4b5e-4f6a-8c9d-1e2f3a4b5c6d.jpg\"\n\ncurl -s -X PUT \"$UPLOAD_URL\" \\\n    -H \"Content-Type: image\u002Fjpeg\" \\\n    --upload-file .\u002Flaptop.jpg\n# Порожня відповідь — 200 OK означає успіх\n",[3432,26424,26425,26430,26440,26450,26454,26474,26483,26491],{"__ignoreMap":3569},[3603,26426,26427],{"class":3605,"line":3606},[3603,26428,26429],{"class":5214},"# ── 3. Завантажити зображення НАПРЯМУ в S3 (без ASP.NET!) ────────────────────\n",[3603,26431,26432,26435,26437],{"class":3605,"line":3612},[3603,26433,26434],{"class":5576},"UPLOAD_URL",[3603,26436,6507],{"class":5541},[3603,26438,26439],{"class":5224},"\"https:\u002F\u002Fproduct-images-2024.s3.eu-central-1.amazonaws.com\u002Fproducts\u002F1\u002Ff3a2...jpg?...\"\n",[3603,26441,26442,26445,26447],{"class":3605,"line":3618},[3603,26443,26444],{"class":5576},"S3_KEY",[3603,26446,6507],{"class":5541},[3603,26448,26449],{"class":5224},"\"products\u002F1\u002Ff3a2c8d1-4b5e-4f6a-8c9d-1e2f3a4b5c6d.jpg\"\n",[3603,26451,26452],{"class":3605,"line":3624},[3603,26453,3628],{"emptyLinePlaceholder":3627},[3603,26455,26456,26458,26460,26462,26465,26467,26470,26472],{"class":3605,"line":3631},[3603,26457,21782],{"class":5220},[3603,26459,26300],{"class":5243},[3603,26461,26303],{"class":5243},[3603,26463,26464],{"class":5224}," PUT",[3603,26466,6548],{"class":5224},[3603,26468,26469],{"class":5576},"$UPLOAD_URL",[3603,26471,6554],{"class":5224},[3603,26473,5238],{"class":5237},[3603,26475,26476,26478,26481],{"class":3605,"line":3637},[3603,26477,26321],{"class":5243},[3603,26479,26480],{"class":5224}," \"Content-Type: image\u002Fjpeg\"",[3603,26482,5238],{"class":5237},[3603,26484,26485,26488],{"class":3605,"line":3643},[3603,26486,26487],{"class":5243},"    --upload-file",[3603,26489,26490],{"class":5224}," .\u002Flaptop.jpg\n",[3603,26492,26493],{"class":3605,"line":3649},[3603,26494,26495],{"class":5214},"# Порожня відповідь — 200 OK означає успіх\n",[4801,26497,26499,26508,26515],{"title":26498},"PUT → S3 presigned URL (прямо з браузера)",[4805,26500,26502,4841,26505],{"className":26501},[3605],[3603,26503,7030],{"className":26504},[7029],[3356,26506,26507],{},"curl -X PUT \"$UPLOAD_URL\" -H \"Content-Type: image\u002Fjpeg\" --upload-file .\u002Flaptop.jpg",[4805,26509,26511],{"className":26510},[3605],[3603,26512,26514],{"className":26513},[21837],"# (empty response body)",[4805,26516,26518,26522],{"className":26517},[3605],[3603,26519,26521],{"className":26520},[4811],"HTTP 200 OK"," — файл збережено в S3 без участі ASP.NET",[3561,26524,26526],{"className":5205,"code":26525,"language":5207,"meta":3569,"style":3569},"# ── 4. Підтвердити завантаження — зберегти S3 key у БД ───────────────────────\ncurl -s -X PUT \"$BASE\u002Fapi\u002Fproducts\u002F1\u002Fimage\" \\\n    -H \"Content-Type: application\u002Fjson\" \\\n    -d \"{\\\"key\\\":\\\"$S3_KEY\\\"}\"\n",[3432,26527,26528,26533,26552,26560],{"__ignoreMap":3569},[3603,26529,26530],{"class":3605,"line":3606},[3603,26531,26532],{"class":5214},"# ── 4. Підтвердити завантаження — зберегти S3 key у БД ───────────────────────\n",[3603,26534,26535,26537,26539,26541,26543,26545,26547,26550],{"class":3605,"line":3612},[3603,26536,21782],{"class":5220},[3603,26538,26300],{"class":5243},[3603,26540,26303],{"class":5243},[3603,26542,26464],{"class":5224},[3603,26544,6548],{"class":5224},[3603,26546,26311],{"class":5576},[3603,26548,26549],{"class":5224},"\u002Fapi\u002Fproducts\u002F1\u002Fimage\"",[3603,26551,5238],{"class":5237},[3603,26553,26554,26556,26558],{"class":3605,"line":3618},[3603,26555,26321],{"class":5243},[3603,26557,26324],{"class":5224},[3603,26559,5238],{"class":5237},[3603,26561,26562,26564,26567,26569,26571,26573,26575,26577,26580,26582],{"class":3605,"line":3624},[3603,26563,26331],{"class":5243},[3603,26565,26566],{"class":5224}," \"{",[3603,26568,11698],{"class":5237},[3603,26570,11126],{"class":5224},[3603,26572,11698],{"class":5237},[3603,26574,5629],{"class":5224},[3603,26576,11698],{"class":5237},[3603,26578,26579],{"class":5576},"$S3_KEY",[3603,26581,11698],{"class":5237},[3603,26583,26584],{"class":5224},"}\"\n",[4801,26586,26588,26591,26599],{"title":26587},"PUT \u002Fapi\u002Fproducts\u002F1\u002Fimage",[4805,26589,5618],{"className":26590},[3605],[4805,26592,26594,26595],{"className":26593},[3605],"  \"imageUrl\": ",[3603,26596,26598],{"className":26597},[4811],"\"https:\u002F\u002Fproduct-images-2024.s3.eu-central-1.amazonaws.com\u002Fproducts\u002F1\u002Ff3a2...jpg?X-Amz-Expires=3600&...\"",[4805,26600,5645],{"className":26601},[3605],[3561,26603,26605],{"className":5205,"code":26604,"language":5207,"meta":3569,"style":3569},"# ── 5. Отримати продукт з presigned GET URL (вставити в \u003Cimg src>) ───────────\ncurl -s \"$BASE\u002Fapi\u002Fproducts\u002F1\"\n",[3432,26606,26607,26612],{"__ignoreMap":3569},[3603,26608,26609],{"class":3605,"line":3606},[3603,26610,26611],{"class":5214},"# ── 5. Отримати продукт з presigned GET URL (вставити в \u003Cimg src>) ───────────\n",[3603,26613,26614,26616,26618,26620,26622],{"class":3605,"line":3612},[3603,26615,21782],{"class":5220},[3603,26617,26300],{"class":5243},[3603,26619,6548],{"class":5224},[3603,26621,26311],{"class":5576},[3603,26623,26624],{"class":5224},"\u002Fapi\u002Fproducts\u002F1\"\n",[4801,26626,26628,26631,26635,26639,26643,26647,26653,26657],{"title":26627},"GET \u002Fapi\u002Fproducts\u002F1",[4805,26629,5618],{"className":26630},[3605],[4805,26632,26634],{"className":26633},[3605],"  \"id\": 1,",[4805,26636,26638],{"className":26637},[3605],"  \"name\": \"Gaming Laptop\",",[4805,26640,26642],{"className":26641},[3605],"  \"description\": \"RTX 4090, 32GB RAM\",",[4805,26644,26646],{"className":26645},[3605],"  \"price\": 2499.99,",[4805,26648,26594,26650,7063],{"className":26649},[3605],[3603,26651,26598],{"className":26652},[7085],[4805,26654,26656],{"className":26655},[3605],"  \"createdAt\": \"2024-01-15T10:30:00Z\"",[4805,26658,5645],{"className":26659},[3605],[3561,26661,26663],{"className":5205,"code":26662,"language":5207,"meta":3569,"style":3569},"# ── 6. Видалення продукту (зображення з S3 видаляється автоматично) ──────────\ncurl -s -X DELETE \"$BASE\u002Fapi\u002Fproducts\u002F1\"\n# HTTP 204 No Content\n",[3432,26664,26665,26670,26687],{"__ignoreMap":3569},[3603,26666,26667],{"class":3605,"line":3606},[3603,26668,26669],{"class":5214},"# ── 6. Видалення продукту (зображення з S3 видаляється автоматично) ──────────\n",[3603,26671,26672,26674,26676,26678,26681,26683,26685],{"class":3605,"line":3612},[3603,26673,21782],{"class":5220},[3603,26675,26300],{"class":5243},[3603,26677,26303],{"class":5243},[3603,26679,26680],{"class":5224}," DELETE",[3603,26682,6548],{"class":5224},[3603,26684,26311],{"class":5576},[3603,26686,26624],{"class":5224},[3603,26688,26689],{"class":3605,"line":3618},[3603,26690,26691],{"class":5214},"# HTTP 204 No Content\n",[3405,26693],{},[3412,26695,26697],{"id":26696},"крок-8-обовязково-очищення","Крок 8: ОБОВ'ЯЗКОВО — Очищення",[3561,26699,26701],{"className":5205,"code":26700,"language":5207,"meta":3569,"style":3569},"BUCKET=\"product-images-2024\"\nREGION=\"eu-central-1\"\n\n# Видалити всі зображення\naws s3 rm s3:\u002F\u002F$BUCKET --recursive --region $REGION\n\n# Видалити сам bucket\naws s3api delete-bucket --bucket $BUCKET --region $REGION\n",[3432,26702,26703,26711,26719,26723,26728,26749,26753,26758],{"__ignoreMap":3569},[3603,26704,26705,26707,26709],{"class":3605,"line":3606},[3603,26706,6504],{"class":5576},[3603,26708,6507],{"class":5541},[3603,26710,22244],{"class":5224},[3603,26712,26713,26715,26717],{"class":3605,"line":3612},[3603,26714,6515],{"class":5576},[3603,26716,6507],{"class":5541},[3603,26718,6520],{"class":5224},[3603,26720,26721],{"class":3605,"line":3618},[3603,26722,3628],{"emptyLinePlaceholder":3627},[3603,26724,26725],{"class":3605,"line":3624},[3603,26726,26727],{"class":5214},"# Видалити всі зображення\n",[3603,26729,26730,26732,26734,26737,26740,26742,26745,26747],{"class":3605,"line":3631},[3603,26731,5221],{"class":5220},[3603,26733,5225],{"class":5224},[3603,26735,26736],{"class":5224}," rm",[3603,26738,26739],{"class":5224}," s3:\u002F\u002F",[3603,26741,6551],{"class":5576},[3603,26743,26744],{"class":5243}," --recursive",[3603,26746,6612],{"class":5243},[3603,26748,22089],{"class":5576},[3603,26750,26751],{"class":3605,"line":3637},[3603,26752,3628],{"emptyLinePlaceholder":3627},[3603,26754,26755],{"class":3605,"line":3643},[3603,26756,26757],{"class":5214},"# Видалити сам bucket\n",[3603,26759,26760,26762,26764,26766,26768,26770,26772],{"class":3605,"line":3649},[3603,26761,5221],{"class":5220},[3603,26763,5365],{"class":5224},[3603,26765,22121],{"class":5224},[3603,26767,21030],{"class":5243},[3603,26769,20954],{"class":5576},[3603,26771,6612],{"class":5243},[3603,26773,22089],{"class":5576},[3405,26775],{},[3348,26777,26779],{"id":26778},"minio-s3-сумісне-сховище-для-локальної-розробки","MinIO — S3-сумісне сховище для локальної розробки",[3353,26781,26782],{},"AWS S3 — чудовий сервіс, але він платний і вимагає підключення до інтернету. Під час локальної розробки запускати весь код проти реального S3 незручно: платите за кожен запит, потрібні AWS-ключі у кожного розробника, CI\u002FCD залежить від зовнішньої мережі. MinIO вирішує всі ці проблеми.",[3353,26784,26785,26788,26789,26792,26793,3470,26796,3470,26799,26802],{},[3356,26786,26787],{},"MinIO"," — відкрите, S3-сумісне об'єктне сховище. Це означає, що ваш .NET код, React фронтенд і будь-який інший клієнт, написаний для AWS S3, ",[3356,26790,26791],{},"без жодних змін"," працює з MinIO — достатньо замінити URL ендпоінту. MinIO реалізує той самий HTTP API, що й S3: ",[3432,26794,26795],{},"PutObject",[3432,26797,26798],{},"GetObject",[3432,26800,26801],{},"DeleteObject",", Presigned URLs, Multipart Upload, Bucket Policies, CORS — все.",[4762,26804,26805,26823,26841,26859],{},[4765,26806,26809],{"icon":26807,"title":26808},"i-heroicons-computer-desktop","Локальна розробка",[3369,26810,26811,26814,26817,26820],{},[3372,26812,26813],{},"Запуск одною командою через Docker",[3372,26815,26816],{},"Нема витрат — безплатно",[3372,26818,26819],{},"Нема залежності від інтернету",[3372,26821,26822],{},"Ізольоване середовище для кожного розробника",[4765,26824,26827],{"icon":26825,"title":26826},"i-heroicons-cog-6-tooth","CI\u002FCD Pipeline",[3369,26828,26829,26832,26835,26838],{},[3372,26830,26831],{},"MinIO в GitHub Actions \u002F GitLab CI як сервіс",[3372,26833,26834],{},"Тести не потребують mock — реальний S3 API",[3372,26836,26837],{},"Швидко (локальна мережа між контейнерами)",[3372,26839,26840],{},"Повторювані результати без flaky S3 запитів",[4765,26842,26845],{"icon":26843,"title":26844},"i-heroicons-server","Self-hosted \u002F On-premise",[3369,26846,26847,26850,26853,26856],{},[3372,26848,26849],{},"Власні сервери — повний контроль",[3372,26851,26852],{},"GDPR\u002Fcompliance: дані не виходять з вашої інфраструктури",[3372,26854,26855],{},"Кластерний режим (erasure coding) для надійності",[3372,26857,26858],{},"Kubernetes Operator для автоматичного масштабування",[4765,26860,26863],{"icon":26861,"title":26862},"i-heroicons-arrows-right-left","Міграція та гібрид",[3369,26864,26865,26868,26871],{},[3372,26866,26867],{},"Той самий SDK — перемикання між MinIO і S3 за конфігом",[3372,26869,26870],{},"MinIO Gateway: проксі поверх Azure Blob, GCS",[3372,26872,26873],{},"Поступова міграція з AWS до власної інфраструктури",[3405,26875],{},[3412,26877,26879],{"id":26878},"minio-vs-aws-s3-порівняння","MinIO vs AWS S3 — порівняння",[3902,26881,26882,26894],{},[3905,26883,26884],{},[3908,26885,26886,26889,26892],{},[3911,26887,26888],{},"Характеристика",[3911,26890,26891],{},"AWS S3",[3911,26893,26787],{},[3924,26895,26896,26906,26917,26927,26938,26949,26960,26971,26982,26992,27001,27010,27019,27028,27038],{},[3908,26897,26898,26900,26903],{},[3929,26899,10403],{},[3929,26901,26902],{},"Managed Cloud SaaS",[3929,26904,26905],{},"Self-hosted \u002F Open Source",[3908,26907,26908,26911,26914],{},[3929,26909,26910],{},"API сумісність",[3929,26912,26913],{},"S3 (оригінал)",[3929,26915,26916],{},"100% S3-compatible",[3908,26918,26919,26921,26924],{},[3929,26920,4188],{},[3929,26922,26923],{},"$0.023\u002FGB\u002Fміс + запити",[3929,26925,26926],{},"Безкоштовно (лише сервер)",[3908,26928,26929,26932,26935],{},[3929,26930,26931],{},"Локальний запуск",[3929,26933,26934],{},"❌",[3929,26936,26937],{},"✅ Docker \u002F binary",[3908,26939,26940,26943,26946],{},[3929,26941,26942],{},"SLA",[3929,26944,26945],{},"99.999999999% (11 дев'яток)",[3929,26947,26948],{},"Залежить від вашого сервера",[3908,26950,26951,26954,26957],{},[3929,26952,26953],{},"Масштабування",[3929,26955,26956],{},"Необмежене (керує AWS)",[3929,26958,26959],{},"До сотень петабайт (DIY)",[3908,26961,26962,26965,26968],{},[3929,26963,26964],{},"Шифрування",[3929,26966,26967],{},"SSE-S3, SSE-KMS, SSE-C",[3929,26969,26970],{},"AES-256, TLS, Vault",[3908,26972,26973,26976,26979],{},[3929,26974,26975],{},"IAM\u002FRBAC",[3929,26977,26978],{},"AWS IAM",[3929,26980,26981],{},"MinIO вбудований RBAC",[3908,26983,26984,26987,26990],{},[3929,26985,26986],{},"Versioning",[3929,26988,26989],{},"✅",[3929,26991,26989],{},[3908,26993,26994,26997,26999],{},[3929,26995,26996],{},"Lifecycle Policies",[3929,26998,26989],{},[3929,27000,26989],{},[3908,27002,27003,27006,27008],{},[3929,27004,27005],{},"Presigned URLs",[3929,27007,26989],{},[3929,27009,26989],{},[3908,27011,27012,27015,27017],{},[3929,27013,27014],{},"Static Website Hosting",[3929,27016,26989],{},[3929,27018,26989],{},[3908,27020,27021,27024,27026],{},[3929,27022,27023],{},"Object Lock (WORM)",[3929,27025,26989],{},[3929,27027,26989],{},[3908,27029,27030,27033,27035],{},[3929,27031,27032],{},"Консоль",[3929,27034,6375],{},[3929,27036,27037],{},"MinIO Console (веб-UI)",[3908,27039,27040,27043,27046],{},[3929,27041,27042],{},"Ідеально для",[3929,27044,27045],{},"Продакшн",[3929,27047,27048],{},"Локальна розробка + on-premise",[3397,27050,27051],{},"Для продакшну зі стандартними вимогами → AWS S3. Для локальної розробки, CI\u002FCD та on-premise → MinIO. Один і той самий код — різна конфігурація.",[3405,27053],{},[3412,27055,27057],{"id":27056},"архітектура-minio","Архітектура MinIO",[27059,27060,27061],"mermaid",{},[3561,27062,27065],{"className":27063,"code":27064,"language":27059,"meta":3569,"style":3569},"language-mermaid shiki shiki-themes light-plus dark-plus dark-plus","graph LR\n    subgraph DEV[\"💻 Локальна розробка\"]\n        dotnet[\"ASP.NET API\u003Cbr\u002F>localhost:5000\"]\n        react[\"React SPA\u003Cbr\u002F>localhost:3000\"]\n    end\n\n    subgraph MINIO[\"🪣 MinIO\u003Cbr\u002F>localhost:9000\"]\n        api[\"S3 API\u003Cbr\u002F>:9000\"]\n        console[\"Web Console\u003Cbr\u002F>:9001\"]\n        b1[\"bucket: uploads\"]\n        b2[\"bucket: avatars\"]\n        b3[\"bucket: documents\"]\n        api --> b1\n        api --> b2\n        api --> b3\n    end\n\n    subgraph PROD[\"☁️ Продакшн\"]\n        s3[\"AWS S3\"]\n    end\n\n    dotnet -->|\"AWSSDK.S3\u003Cbr\u002F>endpoint=localhost:9000\"| api\n    react -->|\"Presigned URL\u003Cbr\u002F>PUT\u002FGET\"| api\n    console -->|\"Браузер\"| api\n\n    dotnet -.->|\"Та сама конфігурація\u003Cbr\u002F>endpoint=s3.amazonaws.com\"| s3\n\n    style DEV fill:#1e293b,stroke:#3b82f6,color:#e2e8f0\n    style MINIO fill:#1e293b,stroke:#f59e0b,color:#e2e8f0\n    style PROD fill:#1e293b,stroke:#10b981,color:#e2e8f0\n",[3432,27066,27067,27072,27077,27082,27087,27092,27096,27101,27106,27111,27116,27121,27126,27131,27136,27141,27145,27149,27154,27159,27163,27167,27172,27177,27182,27186,27191,27195,27200,27205],{"__ignoreMap":3569},[3603,27068,27069],{"class":3605,"line":3606},[3603,27070,27071],{},"graph LR\n",[3603,27073,27074],{"class":3605,"line":3612},[3603,27075,27076],{},"    subgraph DEV[\"💻 Локальна розробка\"]\n",[3603,27078,27079],{"class":3605,"line":3618},[3603,27080,27081],{},"        dotnet[\"ASP.NET API\u003Cbr\u002F>localhost:5000\"]\n",[3603,27083,27084],{"class":3605,"line":3624},[3603,27085,27086],{},"        react[\"React SPA\u003Cbr\u002F>localhost:3000\"]\n",[3603,27088,27089],{"class":3605,"line":3631},[3603,27090,27091],{},"    end\n",[3603,27093,27094],{"class":3605,"line":3637},[3603,27095,3628],{"emptyLinePlaceholder":3627},[3603,27097,27098],{"class":3605,"line":3643},[3603,27099,27100],{},"    subgraph MINIO[\"🪣 MinIO\u003Cbr\u002F>localhost:9000\"]\n",[3603,27102,27103],{"class":3605,"line":3649},[3603,27104,27105],{},"        api[\"S3 API\u003Cbr\u002F>:9000\"]\n",[3603,27107,27108],{"class":3605,"line":3655},[3603,27109,27110],{},"        console[\"Web Console\u003Cbr\u002F>:9001\"]\n",[3603,27112,27113],{"class":3605,"line":3661},[3603,27114,27115],{},"        b1[\"bucket: uploads\"]\n",[3603,27117,27118],{"class":3605,"line":3667},[3603,27119,27120],{},"        b2[\"bucket: avatars\"]\n",[3603,27122,27123],{"class":3605,"line":3673},[3603,27124,27125],{},"        b3[\"bucket: documents\"]\n",[3603,27127,27128],{"class":3605,"line":3678},[3603,27129,27130],{},"        api --> b1\n",[3603,27132,27133],{"class":3605,"line":3684},[3603,27134,27135],{},"        api --> b2\n",[3603,27137,27138],{"class":3605,"line":3690},[3603,27139,27140],{},"        api --> b3\n",[3603,27142,27143],{"class":3605,"line":3696},[3603,27144,27091],{},[3603,27146,27147],{"class":3605,"line":3702},[3603,27148,3628],{"emptyLinePlaceholder":3627},[3603,27150,27151],{"class":3605,"line":3708},[3603,27152,27153],{},"    subgraph PROD[\"☁️ Продакшн\"]\n",[3603,27155,27156],{"class":3605,"line":4667},[3603,27157,27158],{},"        s3[\"AWS S3\"]\n",[3603,27160,27161],{"class":3605,"line":4673},[3603,27162,27091],{},[3603,27164,27165],{"class":3605,"line":4679},[3603,27166,3628],{"emptyLinePlaceholder":3627},[3603,27168,27169],{"class":3605,"line":4685},[3603,27170,27171],{},"    dotnet -->|\"AWSSDK.S3\u003Cbr\u002F>endpoint=localhost:9000\"| api\n",[3603,27173,27174],{"class":3605,"line":4691},[3603,27175,27176],{},"    react -->|\"Presigned URL\u003Cbr\u002F>PUT\u002FGET\"| api\n",[3603,27178,27179],{"class":3605,"line":4696},[3603,27180,27181],{},"    console -->|\"Браузер\"| api\n",[3603,27183,27184],{"class":3605,"line":4701},[3603,27185,3628],{"emptyLinePlaceholder":3627},[3603,27187,27188],{"class":3605,"line":4707},[3603,27189,27190],{},"    dotnet -.->|\"Та сама конфігурація\u003Cbr\u002F>endpoint=s3.amazonaws.com\"| s3\n",[3603,27192,27193],{"class":3605,"line":4713},[3603,27194,3628],{"emptyLinePlaceholder":3627},[3603,27196,27197],{"class":3605,"line":4719},[3603,27198,27199],{},"    style DEV fill:#1e293b,stroke:#3b82f6,color:#e2e8f0\n",[3603,27201,27202],{"class":3605,"line":4724},[3603,27203,27204],{},"    style MINIO fill:#1e293b,stroke:#f59e0b,color:#e2e8f0\n",[3603,27206,27207],{"class":3605,"line":4730},[3603,27208,27209],{},"    style PROD fill:#1e293b,stroke:#10b981,color:#e2e8f0\n",[3353,27211,27212,27215,27216,27219],{},[3356,27213,27214],{},"Ключова ідея",": .NET SDK не знає, з ким розмовляє. Він надсилає HTTP запити за S3 протоколом. MinIO відповідає за тим самим протоколом. У продакшні ви змінюєте ",[3432,27217,27218],{},"endpoint"," у конфігурації — і більше нічого.",[3405,27221],{},[3412,27223,27225],{"id":27224},"встановлення-та-запуск","Встановлення та запуск",[5197,27227,27228],{},[5200,27229,27231,27323],{"label":27230},"Docker (швидко)",[3561,27232,27234],{"className":5205,"code":27233,"language":5207,"meta":3569,"style":3569},"docker run -d \\\n  --name minio \\\n  -p 9000:9000 \\\n  -p 9001:9001 \\\n  -e MINIO_ROOT_USER=minioadmin \\\n  -e MINIO_ROOT_PASSWORD=minioadmin \\\n  -v minio-data:\u002Fdata \\\n  quay.io\u002Fminio\u002Fminio server \u002Fdata --console-address \":9001\"\n",[3432,27235,27236,27248,27258,27268,27277,27287,27296,27306],{"__ignoreMap":3569},[3603,27237,27238,27241,27243,27246],{"class":3605,"line":3606},[3603,27239,27240],{"class":5220},"docker",[3603,27242,20737],{"class":5224},[3603,27244,27245],{"class":5243}," -d",[3603,27247,5238],{"class":5237},[3603,27249,27250,27253,27256],{"class":3605,"line":3612},[3603,27251,27252],{"class":5243},"  --name",[3603,27254,27255],{"class":5224}," minio",[3603,27257,5238],{"class":5237},[3603,27259,27260,27263,27266],{"class":3605,"line":3618},[3603,27261,27262],{"class":5243},"  -p",[3603,27264,27265],{"class":5224}," 9000:9000",[3603,27267,5238],{"class":5237},[3603,27269,27270,27272,27275],{"class":3605,"line":3624},[3603,27271,27262],{"class":5243},[3603,27273,27274],{"class":5224}," 9001:9001",[3603,27276,5238],{"class":5237},[3603,27278,27279,27282,27285],{"class":3605,"line":3631},[3603,27280,27281],{"class":5243},"  -e",[3603,27283,27284],{"class":5224}," MINIO_ROOT_USER=minioadmin",[3603,27286,5238],{"class":5237},[3603,27288,27289,27291,27294],{"class":3605,"line":3637},[3603,27290,27281],{"class":5243},[3603,27292,27293],{"class":5224}," MINIO_ROOT_PASSWORD=minioadmin",[3603,27295,5238],{"class":5237},[3603,27297,27298,27301,27304],{"class":3605,"line":3643},[3603,27299,27300],{"class":5243},"  -v",[3603,27302,27303],{"class":5224}," minio-data:\u002Fdata",[3603,27305,5238],{"class":5237},[3603,27307,27308,27311,27314,27317,27320],{"class":3605,"line":3649},[3603,27309,27310],{"class":5224},"  quay.io\u002Fminio\u002Fminio",[3603,27312,27313],{"class":5224}," server",[3603,27315,27316],{"class":5224}," \u002Fdata",[3603,27318,27319],{"class":5243}," --console-address",[3603,27321,27322],{"class":5224}," \":9001\"\n",[4801,27324,27326,27335,27342,27349,27356,27363,27370,27377,27380,27389,27396],{"title":27325},"docker run minio",[4805,27327,27329,4841,27332],{"className":27328},[3605],[3603,27330,7030],{"className":27331},[7029],[3356,27333,27334],{},"docker run -d --name minio -p 9000:9000 -p 9001:9001 ...",[4805,27336,27338],{"className":27337},[3605],[3603,27339,27341],{"className":27340},[7085],"Unable to find image 'quay.io\u002Fminio\u002Fminio:latest' locally",[4805,27343,27345],{"className":27344},[3605],[3603,27346,27348],{"className":27347},[7029],"latest: Pulling from minio\u002Fminio",[4805,27350,27352],{"className":27351},[3605],[3603,27353,27355],{"className":27354},[7029],"a803e7c4b030: Pull complete",[4805,27357,27359],{"className":27358},[3605],[3603,27360,27362],{"className":27361},[7029],"f23a5c4e84f2: Pull complete",[4805,27364,27366],{"className":27365},[3605],[3603,27367,27369],{"className":27368},[4811],"Status: Downloaded newer image for quay.io\u002Fminio\u002Fminio:latest",[4805,27371,27373],{"className":27372},[3605],[3603,27374,27376],{"className":27375},[7044],"7f3a2c1d8e9b4f5a6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b",[4805,27378,4841],{"className":27379},[3605],[4805,27381,27383,4841,27386],{"className":27382},[3605],[3603,27384,7030],{"className":27385},[7029],[3356,27387,27388],{},"docker ps",[4805,27390,27392],{"className":27391},[3605],[3603,27393,27395],{"className":27394},[7044],"CONTAINER ID   IMAGE                    COMMAND                PORTS",[4805,27397,27399],{"className":27398},[3605],[3603,27400,27402],{"className":27401},[4811],"7f3a2c1d8e9b   quay.io\u002Fminio\u002Fminio      \"\u002Fusr\u002Fbin\u002Fdocker-en…\"  0.0.0.0:9000->9000\u002Ftcp, 0.0.0.0:9001->9001\u002Ftcp",[3353,27404,27405],{},"::",[5200,27407,27409,27416,27809],{"label":27408},"Docker Compose (для проекту)",[3353,27410,27411,27412,27415],{},"Додайте MinIO як сервіс у ",[3432,27413,27414],{},"docker-compose.yml"," поряд з вашим API і БД:",[3561,27417,27421],{"className":27418,"code":27419,"language":27420,"meta":3569,"style":3569},"language-markdown shiki shiki-themes light-plus dark-plus dark-plus","::code-tree\n\n```yaml [docker-compose.yml]\nservices:\n    api:\n        build: .\n        ports:\n            - '5000:8080'\n        environment:\n            - AWS__ServiceURL=http:\u002F\u002Fminio:9000\n            - AWS__AccessKey=minioadmin\n            - AWS__SecretKey=minioadmin\n            - AWS__BucketName=uploads\n        depends_on:\n            minio:\n                condition: service_healthy\n\n    minio:\n        image: quay.io\u002Fminio\u002Fminio:latest\n        command: server \u002Fdata --console-address \":9001\"\n        ports:\n            - '9000:9000'\n            - '9001:9001'\n        environment:\n            MINIO_ROOT_USER: minioadmin\n            MINIO_ROOT_PASSWORD: minioadmin\n        volumes:\n            - minio-data:\u002Fdata\n        healthcheck:\n            test: ['CMD', 'curl', '-f', 'http:\u002F\u002Flocalhost:9000\u002Fminio\u002Fhealth\u002Flive']\n            interval: 5s\n            timeout: 5s\n            retries: 5\n\n    # Автоматичне створення bucket при старті\n    minio-init:\n        image: quay.io\u002Fminio\u002Fmc:latest\n        depends_on:\n            minio:\n                condition: service_healthy\n        entrypoint: >\n            \u002Fbin\u002Fsh -c \"\n            mc alias set local http:\u002F\u002Fminio:9000 minioadmin minioadmin &&\n            mc mb --ignore-existing local\u002Fuploads &&\n            mc mb --ignore-existing local\u002Favatars &&\n            mc anonymous set download local\u002Favatars &&\n            echo 'MinIO initialized'\n            \"\n\nvolumes:\n    minio-data:\n```\n\n::\n","markdown",[3432,27422,27423,27428,27432,27437,27445,27452,27461,27468,27477,27484,27491,27498,27505,27512,27519,27526,27536,27540,27547,27557,27567,27573,27580,27587,27593,27603,27612,27619,27626,27633,27660,27670,27679,27688,27692,27697,27704,27713,27719,27725,27733,27742,27747,27752,27757,27762,27767,27772,27777,27781,27788,27795,27800,27804],{"__ignoreMap":3569},[3603,27424,27425],{"class":3605,"line":3606},[3603,27426,27427],{"class":5541},"::code-tree\n",[3603,27429,27430],{"class":3605,"line":3612},[3603,27431,3628],{"emptyLinePlaceholder":3627},[3603,27433,27434],{"class":3605,"line":3618},[3603,27435,27436],{"class":5541},"```yaml [docker-compose.yml]\n",[3603,27438,27439,27442],{"class":3605,"line":3624},[3603,27440,27441],{"class":15368},"services",[3603,27443,27444],{"class":5617},":\n",[3603,27446,27447,27450],{"class":3605,"line":3631},[3603,27448,27449],{"class":15368},"    api",[3603,27451,27444],{"class":5617},[3603,27453,27454,27457,27459],{"class":3605,"line":3637},[3603,27455,27456],{"class":15368},"        build",[3603,27458,5881],{"class":5617},[3603,27460,20127],{"class":5943},[3603,27462,27463,27466],{"class":3605,"line":3643},[3603,27464,27465],{"class":15368},"        ports",[3603,27467,27444],{"class":5617},[3603,27469,27470,27473],{"class":3605,"line":3649},[3603,27471,27472],{"class":5617},"            - ",[3603,27474,27476],{"class":27475},"su9tN","'5000:8080'\n",[3603,27478,27479,27482],{"class":3605,"line":3655},[3603,27480,27481],{"class":15368},"        environment",[3603,27483,27444],{"class":5617},[3603,27485,27486,27488],{"class":3605,"line":3661},[3603,27487,27472],{"class":5617},[3603,27489,27490],{"class":27475},"AWS__ServiceURL=http:\u002F\u002Fminio:9000\n",[3603,27492,27493,27495],{"class":3605,"line":3667},[3603,27494,27472],{"class":5617},[3603,27496,27497],{"class":27475},"AWS__AccessKey=minioadmin\n",[3603,27499,27500,27502],{"class":3605,"line":3673},[3603,27501,27472],{"class":5617},[3603,27503,27504],{"class":27475},"AWS__SecretKey=minioadmin\n",[3603,27506,27507,27509],{"class":3605,"line":3678},[3603,27508,27472],{"class":5617},[3603,27510,27511],{"class":27475},"AWS__BucketName=uploads\n",[3603,27513,27514,27517],{"class":3605,"line":3684},[3603,27515,27516],{"class":15368},"        depends_on",[3603,27518,27444],{"class":5617},[3603,27520,27521,27524],{"class":3605,"line":3690},[3603,27522,27523],{"class":15368},"            minio",[3603,27525,27444],{"class":5617},[3603,27527,27528,27531,27533],{"class":3605,"line":3696},[3603,27529,27530],{"class":15368},"                condition",[3603,27532,5881],{"class":5617},[3603,27534,27535],{"class":27475},"service_healthy\n",[3603,27537,27538],{"class":3605,"line":3702},[3603,27539,3628],{"emptyLinePlaceholder":3627},[3603,27541,27542,27545],{"class":3605,"line":3708},[3603,27543,27544],{"class":15368},"    minio",[3603,27546,27444],{"class":5617},[3603,27548,27549,27552,27554],{"class":3605,"line":4667},[3603,27550,27551],{"class":15368},"        image",[3603,27553,5881],{"class":5617},[3603,27555,27556],{"class":27475},"quay.io\u002Fminio\u002Fminio:latest\n",[3603,27558,27559,27562,27564],{"class":3605,"line":4673},[3603,27560,27561],{"class":15368},"        command",[3603,27563,5881],{"class":5617},[3603,27565,27566],{"class":27475},"server \u002Fdata --console-address \":9001\"\n",[3603,27568,27569,27571],{"class":3605,"line":4679},[3603,27570,27465],{"class":15368},[3603,27572,27444],{"class":5617},[3603,27574,27575,27577],{"class":3605,"line":4685},[3603,27576,27472],{"class":5617},[3603,27578,27579],{"class":27475},"'9000:9000'\n",[3603,27581,27582,27584],{"class":3605,"line":4691},[3603,27583,27472],{"class":5617},[3603,27585,27586],{"class":27475},"'9001:9001'\n",[3603,27588,27589,27591],{"class":3605,"line":4696},[3603,27590,27481],{"class":15368},[3603,27592,27444],{"class":5617},[3603,27594,27595,27598,27600],{"class":3605,"line":4701},[3603,27596,27597],{"class":15368},"            MINIO_ROOT_USER",[3603,27599,5881],{"class":5617},[3603,27601,27602],{"class":27475},"minioadmin\n",[3603,27604,27605,27608,27610],{"class":3605,"line":4707},[3603,27606,27607],{"class":15368},"            MINIO_ROOT_PASSWORD",[3603,27609,5881],{"class":5617},[3603,27611,27602],{"class":27475},[3603,27613,27614,27617],{"class":3605,"line":4713},[3603,27615,27616],{"class":15368},"        volumes",[3603,27618,27444],{"class":5617},[3603,27620,27621,27623],{"class":3605,"line":4719},[3603,27622,27472],{"class":5617},[3603,27624,27625],{"class":27475},"minio-data:\u002Fdata\n",[3603,27627,27628,27631],{"class":3605,"line":4724},[3603,27629,27630],{"class":15368},"        healthcheck",[3603,27632,27444],{"class":5617},[3603,27634,27635,27638,27640,27643,27645,27648,27650,27653,27655,27658],{"class":3605,"line":4730},[3603,27636,27637],{"class":15368},"            test",[3603,27639,7046],{"class":5617},[3603,27641,27642],{"class":27475},"'CMD'",[3603,27644,3470],{"class":5617},[3603,27646,27647],{"class":27475},"'curl'",[3603,27649,3470],{"class":5617},[3603,27651,27652],{"class":27475},"'-f'",[3603,27654,3470],{"class":5617},[3603,27656,27657],{"class":27475},"'http:\u002F\u002Flocalhost:9000\u002Fminio\u002Fhealth\u002Flive'",[3603,27659,13913],{"class":5617},[3603,27661,27662,27665,27667],{"class":3605,"line":4736},[3603,27663,27664],{"class":15368},"            interval",[3603,27666,5881],{"class":5617},[3603,27668,27669],{"class":27475},"5s\n",[3603,27671,27672,27675,27677],{"class":3605,"line":4742},[3603,27673,27674],{"class":15368},"            timeout",[3603,27676,5881],{"class":5617},[3603,27678,27669],{"class":27475},[3603,27680,27681,27684,27686],{"class":3605,"line":4748},[3603,27682,27683],{"class":15368},"            retries",[3603,27685,5881],{"class":5617},[3603,27687,7548],{"class":5943},[3603,27689,27690],{"class":3605,"line":4753},[3603,27691,3628],{"emptyLinePlaceholder":3627},[3603,27693,27694],{"class":3605,"line":4758},[3603,27695,27696],{"class":5214},"    # Автоматичне створення bucket при старті\n",[3603,27698,27699,27702],{"class":3605,"line":6363},[3603,27700,27701],{"class":15368},"    minio-init",[3603,27703,27444],{"class":5617},[3603,27705,27706,27708,27710],{"class":3605,"line":6368},[3603,27707,27551],{"class":15368},[3603,27709,5881],{"class":5617},[3603,27711,27712],{"class":27475},"quay.io\u002Fminio\u002Fmc:latest\n",[3603,27714,27715,27717],{"class":3605,"line":6830},[3603,27716,27516],{"class":15368},[3603,27718,27444],{"class":5617},[3603,27720,27721,27723],{"class":3605,"line":6835},[3603,27722,27523],{"class":15368},[3603,27724,27444],{"class":5617},[3603,27726,27727,27729,27731],{"class":3605,"line":6841},[3603,27728,27530],{"class":15368},[3603,27730,5881],{"class":5617},[3603,27732,27535],{"class":27475},[3603,27734,27735,27738,27740],{"class":3605,"line":6858},[3603,27736,27737],{"class":15368},"        entrypoint",[3603,27739,5881],{"class":5617},[3603,27741,18114],{"class":5533},[3603,27743,27744],{"class":3605,"line":6871},[3603,27745,27746],{"class":27475},"            \u002Fbin\u002Fsh -c \"\n",[3603,27748,27749],{"class":3605,"line":6881},[3603,27750,27751],{"class":27475},"            mc alias set local http:\u002F\u002Fminio:9000 minioadmin minioadmin &&\n",[3603,27753,27754],{"class":3605,"line":6894},[3603,27755,27756],{"class":27475},"            mc mb --ignore-existing local\u002Fuploads &&\n",[3603,27758,27759],{"class":3605,"line":6915},[3603,27760,27761],{"class":27475},"            mc mb --ignore-existing local\u002Favatars &&\n",[3603,27763,27764],{"class":3605,"line":6927},[3603,27765,27766],{"class":27475},"            mc anonymous set download local\u002Favatars &&\n",[3603,27768,27769],{"class":3605,"line":6932},[3603,27770,27771],{"class":27475},"            echo 'MinIO initialized'\n",[3603,27773,27774],{"class":3605,"line":6946},[3603,27775,27776],{"class":27475},"            \"\n",[3603,27778,27779],{"class":3605,"line":6951},[3603,27780,3628],{"emptyLinePlaceholder":3627},[3603,27782,27783,27786],{"class":3605,"line":6963},[3603,27784,27785],{"class":15368},"volumes",[3603,27787,27444],{"class":5617},[3603,27789,27790,27793],{"class":3605,"line":6976},[3603,27791,27792],{"class":15368},"    minio-data",[3603,27794,27444],{"class":5617},[3603,27796,27797],{"class":3605,"line":6985},[3603,27798,27799],{"class":5541},"```\n",[3603,27801,27802],{"class":3605,"line":6998},[3603,27803,3628],{"emptyLinePlaceholder":3627},[3603,27805,27806],{"class":3605,"line":7009},[3603,27807,27808],{"class":5541},"::\n",[4801,27810,27812,27821,27828,27840,27851,27862,27865,27874,27882,27889,27896],{"title":27811},"docker compose up",[4805,27813,27815,4841,27818],{"className":27814},[3605],[3603,27816,7030],{"className":27817},[7029],[3356,27819,27820],{},"docker compose up -d",[4805,27822,27824],{"className":27823},[3605],[3603,27825,27827],{"className":27826},[7029],"[+] Running 3\u002F3",[4805,27829,4841,27831,27835,27836],{"className":27830},[3605],[3603,27832,27834],{"className":27833},[4811],"✔"," Container project-minio-1       ",[3603,27837,27839],{"className":27838},[4811],"Healthy",[4805,27841,4841,27843,27846,27847],{"className":27842},[3605],[3603,27844,27834],{"className":27845},[4811]," Container project-minio-init-1  ",[3603,27848,27850],{"className":27849},[4811],"Exited (0)",[4805,27852,4841,27854,27857,27858],{"className":27853},[3605],[3603,27855,27834],{"className":27856},[4811]," Container project-api-1         ",[3603,27859,27861],{"className":27860},[4811],"Started",[4805,27863,4841],{"className":27864},[3605],[4805,27866,27868,4841,27871],{"className":27867},[3605],[3603,27869,7030],{"className":27870},[7029],[3356,27872,27873],{},"docker compose logs minio-init",[4805,27875,27877,27881],{"className":27876},[3605],[3603,27878,27880],{"className":27879},[7029],"minio-init-1  |"," Added `local` successfully.",[4805,27883,27885,27888],{"className":27884},[3605],[3603,27886,27880],{"className":27887},[7029]," Bucket created successfully `local\u002Fuploads`.",[4805,27890,27892,27895],{"className":27891},[3605],[3603,27893,27880],{"className":27894},[7029]," Bucket created successfully `local\u002Favatars`.",[4805,27897,27899,4841,27902],{"className":27898},[3605],[3603,27900,27880],{"className":27901},[7029],[3603,27903,27905],{"className":27904},[4811],"MinIO initialized",[5200,27907,27909,27915,28035],{"label":27908},"MinIO Client (mc)",[3353,27910,27911,27914],{},[3432,27912,27913],{},"mc"," — CLI для управління MinIO (і S3). Встановлюється окремо.",[3561,27916,27918],{"className":5205,"code":27917,"language":5207,"meta":3569,"style":3569},"# macOS\nbrew install minio\u002Fstable\u002Fmc\n\n# Linux\ncurl -O https:\u002F\u002Fdl.min.io\u002Fclient\u002Fmc\u002Frelease\u002Flinux-amd64\u002Fmc\nchmod +x mc && sudo mv mc \u002Fusr\u002Flocal\u002Fbin\u002F\n\n# Налаштування alias\nmc alias set local http:\u002F\u002Flocalhost:9000 minioadmin minioadmin\nmc alias set prod s3 YOUR_ACCESS_KEY YOUR_SECRET_KEY --api S3v4\n",[3432,27919,27920,27925,27935,27939,27944,27954,27979,27983,27988,28010],{"__ignoreMap":3569},[3603,27921,27922],{"class":3605,"line":3606},[3603,27923,27924],{"class":5214},"# macOS\n",[3603,27926,27927,27930,27932],{"class":3605,"line":3612},[3603,27928,27929],{"class":5220},"brew",[3603,27931,17931],{"class":5224},[3603,27933,27934],{"class":5224}," minio\u002Fstable\u002Fmc\n",[3603,27936,27937],{"class":3605,"line":3618},[3603,27938,3628],{"emptyLinePlaceholder":3627},[3603,27940,27941],{"class":3605,"line":3624},[3603,27942,27943],{"class":5214},"# Linux\n",[3603,27945,27946,27948,27951],{"class":3605,"line":3631},[3603,27947,21782],{"class":5220},[3603,27949,27950],{"class":5243}," -O",[3603,27952,27953],{"class":5224}," https:\u002F\u002Fdl.min.io\u002Fclient\u002Fmc\u002Frelease\u002Flinux-amd64\u002Fmc\n",[3603,27955,27956,27959,27962,27965,27968,27971,27974,27976],{"class":3605,"line":3637},[3603,27957,27958],{"class":5220},"chmod",[3603,27960,27961],{"class":5224}," +x",[3603,27963,27964],{"class":5224}," mc",[3603,27966,27967],{"class":5541}," && ",[3603,27969,27970],{"class":5220},"sudo",[3603,27972,27973],{"class":5224}," mv",[3603,27975,27964],{"class":5224},[3603,27977,27978],{"class":5224}," \u002Fusr\u002Flocal\u002Fbin\u002F\n",[3603,27980,27981],{"class":3605,"line":3643},[3603,27982,3628],{"emptyLinePlaceholder":3627},[3603,27984,27985],{"class":3605,"line":3649},[3603,27986,27987],{"class":5214},"# Налаштування alias\n",[3603,27989,27990,27992,27995,27998,28001,28004,28007],{"class":3605,"line":3655},[3603,27991,27913],{"class":5220},[3603,27993,27994],{"class":5224}," alias",[3603,27996,27997],{"class":5224}," set",[3603,27999,28000],{"class":5224}," local",[3603,28002,28003],{"class":5224}," http:\u002F\u002Flocalhost:9000",[3603,28005,28006],{"class":5224}," minioadmin",[3603,28008,28009],{"class":5224}," minioadmin\n",[3603,28011,28012,28014,28016,28018,28021,28023,28026,28029,28032],{"class":3605,"line":3661},[3603,28013,27913],{"class":5220},[3603,28015,27994],{"class":5224},[3603,28017,27997],{"class":5224},[3603,28019,28020],{"class":5224}," prod",[3603,28022,5225],{"class":5224},[3603,28024,28025],{"class":5224}," YOUR_ACCESS_KEY",[3603,28027,28028],{"class":5224}," YOUR_SECRET_KEY",[3603,28030,28031],{"class":5243}," --api",[3603,28033,28034],{"class":5224}," S3v4\n",[4801,28036,28038,28047,28054,28057,28066,28070,28074,28078,28081,28090,28102,28112,28122,28125,28134,28141,28144,28153,28157,28165,28173,28181,28188,28192],{"title":28037},"mc — типові команди",[4805,28039,28041,4841,28044],{"className":28040},[3605],[3603,28042,7030],{"className":28043},[7029],[3356,28045,28046],{},"mc alias set local http:\u002F\u002Flocalhost:9000 minioadmin minioadmin",[4805,28048,28050],{"className":28049},[3605],[3603,28051,28053],{"className":28052},[4811],"Added `local` successfully.",[4805,28055,4841],{"className":28056},[3605],[4805,28058,28060,4841,28063],{"className":28059},[3605],[3603,28061,7030],{"className":28062},[7029],[3356,28064,28065],{},"mc mb local\u002Fuploads local\u002Favatars local\u002Fdocuments",[4805,28067,28069],{"className":28068},[3605],"Bucket created successfully `local\u002Fuploads`.",[4805,28071,28073],{"className":28072},[3605],"Bucket created successfully `local\u002Favatars`.",[4805,28075,28077],{"className":28076},[3605],"Bucket created successfully `local\u002Fdocuments`.",[4805,28079,4841],{"className":28080},[3605],[4805,28082,28084,4841,28087],{"className":28083},[3605],[3603,28085,7030],{"className":28086},[7029],[3356,28088,28089],{},"mc ls local\u002F",[4805,28091,28093,28097,28098],{"className":28092},[3605],[3603,28094,28096],{"className":28095},[7044],"[2026-05-28 14:22:01 UTC]","     0B ",[3603,28099,28101],{"className":28100},[7085],"avatars\u002F",[4805,28103,28105,28097,28108],{"className":28104},[3605],[3603,28106,28096],{"className":28107},[7044],[3603,28109,28111],{"className":28110},[7085],"documents\u002F",[4805,28113,28115,28097,28118],{"className":28114},[3605],[3603,28116,28096],{"className":28117},[7044],[3603,28119,28121],{"className":28120},[7085],"uploads\u002F",[4805,28123,4841],{"className":28124},[3605],[4805,28126,28128,4841,28131],{"className":28127},[3605],[3603,28129,7030],{"className":28130},[7029],[3356,28132,28133],{},"mc cp .\u002Fphoto.jpg local\u002Favatars\u002Fuser-42\u002Fphoto.jpg",[4805,28135,28137],{"className":28136},[3605],[3603,28138,28140],{"className":28139},[4811],"...photo.jpg: 284 KiB \u002F 284 KiB ━━━━━━━━━━━━━━━━ 100% 1.2 MiB\u002Fs",[4805,28142,4841],{"className":28143},[3605],[4805,28145,28147,4841,28150],{"className":28146},[3605],[3603,28148,7030],{"className":28149},[7029],[3356,28151,28152],{},"mc stat local\u002Favatars\u002Fuser-42\u002Fphoto.jpg",[4805,28154,28156],{"className":28155},[3605],"Name      : photo.jpg",[4805,28158,28160,28161],{"className":28159},[3605],"Date      : ",[3603,28162,28164],{"className":28163},[7044],"2026-05-28 14:23:17 UTC",[4805,28166,28168,28169],{"className":28167},[3605],"Size      : ",[3603,28170,28172],{"className":28171},[7085],"291 KiB",[4805,28174,28176,28177],{"className":28175},[3605],"ETag      : ",[3603,28178,28180],{"className":28179},[7029],"d41d8cd98f00b204e9800998ecf8427e",[4805,28182,28184,28185],{"className":28183},[3605],"Type      : ",[3603,28186,17365],{"className":28187},[4811],[4805,28189,28191],{"className":28190},[3605],"Metadata  :",[4805,28193,7040,28195,28198],{"className":28194},[3605],[3603,28196,3522],{"className":28197},[7044],": image\u002Fjpeg",[3353,28200,27405],{},[3405,28202],{},[3412,28204,28206],{"id":28205},"minio-console-веб-інтерфейс","MinIO Console — веб-інтерфейс",[3353,28208,28209,28210,28213],{},"MinIO постачається з вбудованим веб-інтерфейсом на порту ",[3432,28211,28212],{},"9001",". Він дозволяє керувати bucket-ами, об'єктами, доступами та налаштуваннями без CLI.",[3353,28215,28216,4841,28219,28222,28223,28226,28227],{},[3356,28217,28218],{},"Відкрийте браузер:",[3432,28220,28221],{},"http:\u002F\u002Flocalhost:9001"," → логін: ",[3432,28224,28225],{},"minioadmin"," \u002F ",[3432,28228,28225],{},[3397,28230,28231,28232,28235,28236,28239,28240,28243,28244,3453],{},"Для продакшну обов'язково змініть credentials: ",[3432,28233,28234],{},"MINIO_ROOT_USER"," і ",[3432,28237,28238],{},"MINIO_ROOT_PASSWORD"," — мінімум 8 символів. Зберігайте у ",[3432,28241,28242],{},".env"," файлі, який додано до ",[3432,28245,28246],{},".gitignore",[3353,28248,28249],{},"Через консоль можна:",[3369,28251,28252,28255,28258,28261,28264,28267],{},[3372,28253,28254],{},"Створювати та видаляти bucket-и",[3372,28256,28257],{},"Завантажувати, переглядати та видаляти об'єкти",[3372,28259,28260],{},"Налаштовувати Access Policy (Public \u002F Private)",[3372,28262,28263],{},"Керувати lifecycle rules (аналог S3 Lifecycle)",[3372,28265,28266],{},"Генерувати Access Key \u002F Secret Key для застосунків",[3372,28268,28269],{},"Переглядати логи подій та метрики",[3405,28271],{},[3412,28273,28275],{"id":28274},"підключення-net-sdk-до-minio","Підключення .NET SDK до MinIO",[3353,28277,28278,28279,28281],{},"MinIO повністю S3-сумісний — той самий ",[3432,28280,15530],{}," пакет, лише інша конфігурація ендпоінту.",[3353,28283,28284],{},[3356,28285,28286,28287,5629],{},"Конфігурація в ",[3432,28288,15549],{},[28290,28291,28292,28372],"code-group",{},[3561,28293,28296],{"className":5851,"code":28294,"filename":28295,"language":5853,"meta":3569,"style":3569},"{\n    \"AWS\": {\n        \"ServiceURL\": \"http:\u002F\u002Flocalhost:9000\",\n        \"AccessKey\": \"minioadmin\",\n        \"SecretKey\": \"minioadmin\",\n        \"BucketName\": \"uploads\",\n        \"ForcePathStyle\": true\n    }\n}\n","appsettings.Development.json",[3432,28297,28298,28302,28308,28320,28332,28343,28354,28364,28368],{"__ignoreMap":3569},[3603,28299,28300],{"class":3605,"line":3606},[3603,28301,5591],{"class":5541},[3603,28303,28304,28306],{"class":3605,"line":3612},[3603,28305,15681],{"class":5864},[3603,28307,5906],{"class":5541},[3603,28309,28310,28313,28315,28318],{"class":3605,"line":3618},[3603,28311,28312],{"class":5864},"        \"ServiceURL\"",[3603,28314,5881],{"class":5541},[3603,28316,28317],{"class":5224},"\"http:\u002F\u002Flocalhost:9000\"",[3603,28319,5604],{"class":5541},[3603,28321,28322,28325,28327,28330],{"class":3605,"line":3624},[3603,28323,28324],{"class":5864},"        \"AccessKey\"",[3603,28326,5881],{"class":5541},[3603,28328,28329],{"class":5224},"\"minioadmin\"",[3603,28331,5604],{"class":5541},[3603,28333,28334,28337,28339,28341],{"class":3605,"line":3631},[3603,28335,28336],{"class":5864},"        \"SecretKey\"",[3603,28338,5881],{"class":5541},[3603,28340,28329],{"class":5224},[3603,28342,5604],{"class":5541},[3603,28344,28345,28347,28349,28352],{"class":3605,"line":3637},[3603,28346,15709],{"class":5864},[3603,28348,5881],{"class":5541},[3603,28350,28351],{"class":5224},"\"uploads\"",[3603,28353,5604],{"class":5541},[3603,28355,28356,28359,28361],{"class":3605,"line":3643},[3603,28357,28358],{"class":5864},"        \"ForcePathStyle\"",[3603,28360,5881],{"class":5541},[3603,28362,28363],{"class":5243},"true\n",[3603,28365,28366],{"class":3605,"line":3649},[3603,28367,3664],{"class":5541},[3603,28369,28370],{"class":3605,"line":3655},[3603,28371,3670],{"class":5541},[3561,28373,28376],{"className":5851,"code":28374,"filename":28375,"language":5853,"meta":3569,"style":3569},"{\n    \"AWS\": {\n        \"Region\": \"eu-central-1\",\n        \"BucketName\": \"your-prod-bucket\"\n    }\n}\n","appsettings.Production.json",[3432,28377,28378,28382,28388,28399,28408,28412],{"__ignoreMap":3569},[3603,28379,28380],{"class":3605,"line":3606},[3603,28381,5591],{"class":5541},[3603,28383,28384,28386],{"class":3605,"line":3612},[3603,28385,15681],{"class":5864},[3603,28387,5906],{"class":5541},[3603,28389,28390,28392,28394,28397],{"class":3605,"line":3618},[3603,28391,15688],{"class":5864},[3603,28393,5881],{"class":5541},[3603,28395,28396],{"class":5224},"\"eu-central-1\"",[3603,28398,5604],{"class":5541},[3603,28400,28401,28403,28405],{"class":3605,"line":3624},[3603,28402,15709],{"class":5864},[3603,28404,5881],{"class":5541},[3603,28406,28407],{"class":5224},"\"your-prod-bucket\"\n",[3603,28409,28410],{"class":3605,"line":3631},[3603,28411,3664],{"class":5541},[3603,28413,28414],{"class":3605,"line":3637},[3603,28415,3670],{"class":5541},[3397,28417,28418,28421,28422,28425,28426,28429,28430,28433],{},[3432,28419,28420],{},"ForcePathStyle: true"," — обов'язкова опція для MinIO. AWS S3 використовує ",[3432,28423,28424],{},"bucket.s3.amazonaws.com"," (virtual-hosted style), а MinIO — ",[3432,28427,28428],{},"localhost:9000\u002Fbucket"," (path style). Без цієї опції SDK буде намагатися звернутись до ",[3432,28431,28432],{},"uploads.localhost:9000",", що не працюватиме.",[3353,28435,28436],{},[3356,28437,28438,28439,5629],{},"Реєстрація сервісів у ",[3432,28440,23462],{},[3561,28442,28444],{"className":5519,"code":28443,"language":5521,"meta":3569,"style":3569},"var builder = WebApplication.CreateBuilder(args);\n\n\u002F\u002F Спільна конфігурація для MinIO та AWS S3\nbuilder.Services.AddSingleton\u003CIAmazonS3>(sp =>\n{\n    var config = builder.Configuration.GetSection(\"AWS\");\n    var serviceUrl = config[\"ServiceURL\"]; \u002F\u002F null у продакшні\n\n    var s3Config = new AmazonS3Config\n    {\n        ForcePathStyle = bool.TryParse(config[\"ForcePathStyle\"], out var fps) && fps\n    };\n\n    \u002F\u002F MinIO: явний URL ендпоінту\n    \u002F\u002F AWS S3: регіон (ServiceURL = null → SDK бере регіон)\n    if (!string.IsNullOrEmpty(serviceUrl))\n        s3Config.ServiceURL = serviceUrl;\n    else\n        s3Config.RegionEndpoint = RegionEndpoint.GetBySystemName(config[\"Region\"] ?? \"eu-central-1\");\n\n    var credentials = new BasicAWSCredentials(\n        config[\"AccessKey\"] ?? throw new InvalidOperationException(\"AWS AccessKey required for MinIO\"),\n        config[\"SecretKey\"] ?? throw new InvalidOperationException(\"AWS SecretKey required for MinIO\")\n    );\n\n    \u002F\u002F У продакшні з IAM Role — credentials беруться автоматично через IMDS\n    if (string.IsNullOrEmpty(serviceUrl))\n        return new AmazonS3Client(s3Config); \u002F\u002F IAM Role \u002F env vars\n\n    return new AmazonS3Client(credentials, s3Config);\n});\n",[3432,28445,28446,28466,28470,28475,28499,28503,28528,28550,28554,28567,28571,28612,28616,28620,28625,28630,28650,28666,28671,28703,28707,28723,28748,28772,28777,28781,28786,28804,28823,28827,28846],{"__ignoreMap":3569},[3603,28447,28448,28450,28452,28454,28456,28458,28460,28462,28464],{"class":3605,"line":3606},[3603,28449,5573],{"class":5243},[3603,28451,23543],{"class":5576},[3603,28453,5580],{"class":5541},[3603,28455,23548],{"class":5576},[3603,28457,3453],{"class":5541},[3603,28459,23553],{"class":5220},[3603,28461,5707],{"class":5541},[3603,28463,23558],{"class":5576},[3603,28465,5713],{"class":5541},[3603,28467,28468],{"class":3605,"line":3612},[3603,28469,3628],{"emptyLinePlaceholder":3627},[3603,28471,28472],{"class":3605,"line":3618},[3603,28473,28474],{"class":5214},"\u002F\u002F Спільна конфігурація для MinIO та AWS S3\n",[3603,28476,28477,28479,28481,28483,28485,28488,28490,28492,28494,28497],{"class":3605,"line":3624},[3603,28478,15605],{"class":5576},[3603,28480,3453],{"class":5541},[3603,28482,15610],{"class":5576},[3603,28484,3453],{"class":5541},[3603,28486,28487],{"class":5220},"AddSingleton",[3603,28489,11250],{"class":5541},[3603,28491,11020],{"class":5537},[3603,28493,23623],{"class":5541},[3603,28495,28496],{"class":5576},"sp",[3603,28498,23628],{"class":5541},[3603,28500,28501],{"class":3605,"line":3631},[3603,28502,5591],{"class":5541},[3603,28504,28505,28507,28509,28511,28513,28515,28517,28519,28522,28524,28526],{"class":3605,"line":3637},[3603,28506,14885],{"class":5243},[3603,28508,14355],{"class":5576},[3603,28510,5580],{"class":5541},[3603,28512,15605],{"class":5576},[3603,28514,3453],{"class":5541},[3603,28516,15841],{"class":5576},[3603,28518,3453],{"class":5541},[3603,28520,28521],{"class":5220},"GetSection",[3603,28523,5707],{"class":5541},[3603,28525,9998],{"class":5224},[3603,28527,5713],{"class":5541},[3603,28529,28530,28532,28535,28537,28539,28541,28544,28547],{"class":3605,"line":3643},[3603,28531,14885],{"class":5243},[3603,28533,28534],{"class":5576}," serviceUrl",[3603,28536,5580],{"class":5541},[3603,28538,14417],{"class":5576},[3603,28540,15943],{"class":5541},[3603,28542,28543],{"class":5224},"\"ServiceURL\"",[3603,28545,28546],{"class":5541},"]; ",[3603,28548,28549],{"class":5214},"\u002F\u002F null у продакшні\n",[3603,28551,28552],{"class":3605,"line":3649},[3603,28553,3628],{"emptyLinePlaceholder":3627},[3603,28555,28556,28558,28561,28563,28565],{"class":3605,"line":3655},[3603,28557,14885],{"class":5243},[3603,28559,28560],{"class":5576}," s3Config",[3603,28562,5580],{"class":5541},[3603,28564,5583],{"class":5243},[3603,28566,14362],{"class":5537},[3603,28568,28569],{"class":3605,"line":3661},[3603,28570,11086],{"class":5541},[3603,28572,28573,28576,28578,28581,28583,28586,28588,28590,28592,28595,28598,28601,28603,28606,28609],{"class":3605,"line":3667},[3603,28574,28575],{"class":5576},"        ForcePathStyle",[3603,28577,5580],{"class":5541},[3603,28579,28580],{"class":5243},"bool",[3603,28582,3453],{"class":5541},[3603,28584,28585],{"class":5220},"TryParse",[3603,28587,5707],{"class":5541},[3603,28589,14417],{"class":5576},[3603,28591,15943],{"class":5541},[3603,28593,28594],{"class":5224},"\"ForcePathStyle\"",[3603,28596,28597],{"class":5541},"], ",[3603,28599,28600],{"class":5243},"out",[3603,28602,16033],{"class":5243},[3603,28604,28605],{"class":5576}," fps",[3603,28607,28608],{"class":5541},") && ",[3603,28610,28611],{"class":5576},"fps\n",[3603,28613,28614],{"class":3605,"line":3673},[3603,28615,14926],{"class":5541},[3603,28617,28618],{"class":3605,"line":3678},[3603,28619,3628],{"emptyLinePlaceholder":3627},[3603,28621,28622],{"class":3605,"line":3684},[3603,28623,28624],{"class":5214},"    \u002F\u002F MinIO: явний URL ендпоінту\n",[3603,28626,28627],{"class":3605,"line":3690},[3603,28628,28629],{"class":5214},"    \u002F\u002F AWS S3: регіон (ServiceURL = null → SDK бере регіон)\n",[3603,28631,28632,28634,28636,28638,28640,28643,28645,28648],{"class":3605,"line":3696},[3603,28633,24229],{"class":5533},[3603,28635,15093],{"class":5541},[3603,28637,7598],{"class":5243},[3603,28639,3453],{"class":5541},[3603,28641,28642],{"class":5220},"IsNullOrEmpty",[3603,28644,5707],{"class":5541},[3603,28646,28647],{"class":5576},"serviceUrl",[3603,28649,24767],{"class":5541},[3603,28651,28652,28655,28657,28660,28662,28664],{"class":3605,"line":3702},[3603,28653,28654],{"class":5576},"        s3Config",[3603,28656,3453],{"class":5541},[3603,28658,28659],{"class":5576},"ServiceURL",[3603,28661,5580],{"class":5541},[3603,28663,28647],{"class":5576},[3603,28665,5547],{"class":5541},[3603,28667,28668],{"class":3605,"line":3708},[3603,28669,28670],{"class":5533},"    else\n",[3603,28672,28673,28675,28677,28679,28681,28683,28685,28688,28690,28692,28694,28697,28699,28701],{"class":3605,"line":4667},[3603,28674,28654],{"class":5576},[3603,28676,3453],{"class":5541},[3603,28678,14390],{"class":5576},[3603,28680,5580],{"class":5541},[3603,28682,14390],{"class":5576},[3603,28684,3453],{"class":5541},[3603,28686,28687],{"class":5220},"GetBySystemName",[3603,28689,5707],{"class":5541},[3603,28691,14417],{"class":5576},[3603,28693,15943],{"class":5541},[3603,28695,28696],{"class":5224},"\"Region\"",[3603,28698,15949],{"class":5541},[3603,28700,28396],{"class":5224},[3603,28702,5713],{"class":5541},[3603,28704,28705],{"class":3605,"line":4673},[3603,28706,3628],{"emptyLinePlaceholder":3627},[3603,28708,28709,28711,28714,28716,28718,28721],{"class":3605,"line":4679},[3603,28710,14885],{"class":5243},[3603,28712,28713],{"class":5576}," credentials",[3603,28715,5580],{"class":5541},[3603,28717,5583],{"class":5243},[3603,28719,28720],{"class":5537}," BasicAWSCredentials",[3603,28722,14825],{"class":5541},[3603,28724,28725,28728,28730,28733,28735,28737,28739,28741,28743,28746],{"class":3605,"line":4685},[3603,28726,28727],{"class":5576},"        config",[3603,28729,15943],{"class":5541},[3603,28731,28732],{"class":5224},"\"AccessKey\"",[3603,28734,15949],{"class":5541},[3603,28736,15952],{"class":5533},[3603,28738,15955],{"class":5243},[3603,28740,15958],{"class":5537},[3603,28742,5707],{"class":5541},[3603,28744,28745],{"class":5224},"\"AWS AccessKey required for MinIO\"",[3603,28747,12741],{"class":5541},[3603,28749,28750,28752,28754,28757,28759,28761,28763,28765,28767,28770],{"class":3605,"line":4691},[3603,28751,28727],{"class":5576},[3603,28753,15943],{"class":5541},[3603,28755,28756],{"class":5224},"\"SecretKey\"",[3603,28758,15949],{"class":5541},[3603,28760,15952],{"class":5533},[3603,28762,15955],{"class":5243},[3603,28764,15958],{"class":5537},[3603,28766,5707],{"class":5541},[3603,28768,28769],{"class":5224},"\"AWS SecretKey required for MinIO\"",[3603,28771,6924],{"class":5541},[3603,28773,28774],{"class":3605,"line":4696},[3603,28775,28776],{"class":5541},"    );\n",[3603,28778,28779],{"class":3605,"line":4701},[3603,28780,3628],{"emptyLinePlaceholder":3627},[3603,28782,28783],{"class":3605,"line":4707},[3603,28784,28785],{"class":5214},"    \u002F\u002F У продакшні з IAM Role — credentials беруться автоматично через IMDS\n",[3603,28787,28788,28790,28792,28794,28796,28798,28800,28802],{"class":3605,"line":4713},[3603,28789,24229],{"class":5533},[3603,28791,10425],{"class":5541},[3603,28793,7598],{"class":5243},[3603,28795,3453],{"class":5541},[3603,28797,28642],{"class":5220},[3603,28799,5707],{"class":5541},[3603,28801,28647],{"class":5576},[3603,28803,24767],{"class":5541},[3603,28805,28806,28808,28810,28812,28814,28817,28820],{"class":3605,"line":4719},[3603,28807,11308],{"class":5533},[3603,28809,15955],{"class":5243},[3603,28811,14412],{"class":5537},[3603,28813,5707],{"class":5541},[3603,28815,28816],{"class":5576},"s3Config",[3603,28818,28819],{"class":5541},"); ",[3603,28821,28822],{"class":5214},"\u002F\u002F IAM Role \u002F env vars\n",[3603,28824,28825],{"class":3605,"line":4724},[3603,28826,3628],{"emptyLinePlaceholder":3627},[3603,28828,28829,28831,28833,28835,28837,28840,28842,28844],{"class":3605,"line":4730},[3603,28830,14931],{"class":5533},[3603,28832,15955],{"class":5243},[3603,28834,14412],{"class":5537},[3603,28836,5707],{"class":5541},[3603,28838,28839],{"class":5576},"credentials",[3603,28841,3470],{"class":5541},[3603,28843,28816],{"class":5576},[3603,28845,5713],{"class":5541},[3603,28847,28848],{"class":3605,"line":4736},[3603,28849,24173],{"class":5541},[3353,28851,28852],{},[3356,28853,28854],{},"Загальний сервіс — працює однаково з MinIO та S3:",[3561,28856,28858],{"className":5519,"code":28857,"language":5521,"meta":3569,"style":3569},"public class FileStorageService(IAmazonS3 s3, IConfiguration config)\n{\n    private readonly string _bucket = config[\"AWS:BucketName\"]!;\n\n    \u002F\u002F Завантажити файл → той самий код для MinIO і S3\n    public async Task\u003Cstring> UploadAsync(Stream content, string key, string contentType)\n    {\n        await s3.PutObjectAsync(new PutObjectRequest\n        {\n            BucketName = _bucket,\n            Key = key,\n            InputStream = content,\n            ContentType = contentType\n        });\n        return key;\n    }\n\n    \u002F\u002F Presigned URL → той самий код\n    public string GeneratePresignedUrl(string key, TimeSpan expiry)\n    {\n        return s3.GetPreSignedURL(new GetPreSignedUrlRequest\n        {\n            BucketName = _bucket,\n            Key = key,\n            Expires = DateTime.UtcNow.Add(expiry),\n            Verb = HttpVerb.GET\n        });\n    }\n\n    \u002F\u002F Видалити → той самий код\n    public async Task DeleteAsync(string key)\n    {\n        await s3.DeleteObjectAsync(_bucket, key);\n    }\n}\n",[3432,28859,28860,28883,28887,28909,28913,28918,28955,28959,28975,28979,28989,28999,29010,29018,29022,29030,29034,29038,29043,29068,29072,29088,29092,29102,29112,29135,29147,29151,29155,29159,29164,29182,29186,29206,29210],{"__ignoreMap":3569},[3603,28861,28862,28864,28866,28869,28871,28873,28875,28877,28879,28881],{"class":3605,"line":3606},[3603,28863,10953],{"class":5243},[3603,28865,10956],{"class":5243},[3603,28867,28868],{"class":5537}," FileStorageService",[3603,28870,5707],{"class":5541},[3603,28872,11020],{"class":5537},[3603,28874,5225],{"class":5576},[3603,28876,3470],{"class":5541},[3603,28878,15907],{"class":5537},[3603,28880,14355],{"class":5576},[3603,28882,6924],{"class":5541},[3603,28884,28885],{"class":3605,"line":3612},[3603,28886,5591],{"class":5541},[3603,28888,28889,28891,28893,28895,28897,28899,28901,28903,28906],{"class":3605,"line":3618},[3603,28890,10968],{"class":5243},[3603,28892,10971],{"class":5243},[3603,28894,10994],{"class":5243},[3603,28896,15883],{"class":5576},[3603,28898,5580],{"class":5541},[3603,28900,14417],{"class":5576},[3603,28902,15943],{"class":5541},[3603,28904,28905],{"class":5224},"\"AWS:BucketName\"",[3603,28907,28908],{"class":5541},"]!;\n",[3603,28910,28911],{"class":3605,"line":3624},[3603,28912,3628],{"emptyLinePlaceholder":3627},[3603,28914,28915],{"class":3605,"line":3631},[3603,28916,28917],{"class":5214},"    \u002F\u002F Завантажити файл → той самий код для MinIO і S3\n",[3603,28919,28920,28922,28924,28926,28928,28930,28932,28934,28936,28938,28941,28943,28945,28947,28949,28951,28953],{"class":3605,"line":3637},[3603,28921,11012],{"class":5243},[3603,28923,11051],{"class":5243},[3603,28925,11054],{"class":5537},[3603,28927,11250],{"class":5541},[3603,28929,7598],{"class":5243},[3603,28931,11255],{"class":5541},[3603,28933,16000],{"class":5220},[3603,28935,5707],{"class":5541},[3603,28937,11076],{"class":5537},[3603,28939,28940],{"class":5576}," content",[3603,28942,3470],{"class":5541},[3603,28944,7598],{"class":5243},[3603,28946,11071],{"class":5576},[3603,28948,3470],{"class":5541},[3603,28950,7598],{"class":5243},[3603,28952,12821],{"class":5576},[3603,28954,6924],{"class":5541},[3603,28956,28957],{"class":3605,"line":3643},[3603,28958,11086],{"class":5541},[3603,28960,28961,28963,28965,28967,28969,28971,28973],{"class":3605,"line":3649},[3603,28962,16058],{"class":5243},[3603,28964,5225],{"class":5576},[3603,28966,3453],{"class":5541},[3603,28968,5704],{"class":5220},[3603,28970,5707],{"class":5541},[3603,28972,5583],{"class":5243},[3603,28974,5586],{"class":5537},[3603,28976,28977],{"class":3605,"line":3655},[3603,28978,5873],{"class":5541},[3603,28980,28981,28983,28985,28987],{"class":3605,"line":3661},[3603,28982,11109],{"class":5576},[3603,28984,5580],{"class":5541},[3603,28986,16084],{"class":5576},[3603,28988,5604],{"class":5541},[3603,28990,28991,28993,28995,28997],{"class":3605,"line":3667},[3603,28992,11121],{"class":5576},[3603,28994,5580],{"class":5541},[3603,28996,11126],{"class":5576},[3603,28998,5604],{"class":5541},[3603,29000,29001,29003,29005,29008],{"class":3605,"line":3673},[3603,29002,11133],{"class":5576},[3603,29004,5580],{"class":5541},[3603,29006,29007],{"class":5576},"content",[3603,29009,5604],{"class":5541},[3603,29011,29012,29014,29016],{"class":3605,"line":3678},[3603,29013,12915],{"class":5576},[3603,29015,5580],{"class":5541},[3603,29017,12920],{"class":5576},[3603,29019,29020],{"class":3605,"line":3684},[3603,29021,16205],{"class":5541},[3603,29023,29024,29026,29028],{"class":3605,"line":3690},[3603,29025,11308],{"class":5533},[3603,29027,11071],{"class":5576},[3603,29029,5547],{"class":5541},[3603,29031,29032],{"class":3605,"line":3696},[3603,29033,3664],{"class":5541},[3603,29035,29036],{"class":3605,"line":3702},[3603,29037,3628],{"emptyLinePlaceholder":3627},[3603,29039,29040],{"class":3605,"line":3708},[3603,29041,29042],{"class":5214},"    \u002F\u002F Presigned URL → той самий код\n",[3603,29044,29045,29047,29049,29052,29054,29056,29058,29060,29063,29066],{"class":3605,"line":4667},[3603,29046,11012],{"class":5243},[3603,29048,10994],{"class":5243},[3603,29050,29051],{"class":5220}," GeneratePresignedUrl",[3603,29053,5707],{"class":5541},[3603,29055,7598],{"class":5243},[3603,29057,11071],{"class":5576},[3603,29059,3470],{"class":5541},[3603,29061,29062],{"class":5537},"TimeSpan",[3603,29064,29065],{"class":5576}," expiry",[3603,29067,6924],{"class":5541},[3603,29069,29070],{"class":3605,"line":4673},[3603,29071,11086],{"class":5541},[3603,29073,29074,29076,29078,29080,29082,29084,29086],{"class":3605,"line":4679},[3603,29075,11308],{"class":5533},[3603,29077,5225],{"class":5576},[3603,29079,3453],{"class":5541},[3603,29081,12771],{"class":5220},[3603,29083,5707],{"class":5541},[3603,29085,5583],{"class":5243},[3603,29087,12691],{"class":5537},[3603,29089,29090],{"class":3605,"line":4685},[3603,29091,5873],{"class":5541},[3603,29093,29094,29096,29098,29100],{"class":3605,"line":4691},[3603,29095,11109],{"class":5576},[3603,29097,5580],{"class":5541},[3603,29099,16084],{"class":5576},[3603,29101,5604],{"class":5541},[3603,29103,29104,29106,29108,29110],{"class":3605,"line":4696},[3603,29105,11121],{"class":5576},[3603,29107,5580],{"class":5541},[3603,29109,11126],{"class":5576},[3603,29111,5604],{"class":5541},[3603,29113,29114,29116,29118,29120,29122,29124,29126,29128,29130,29133],{"class":3605,"line":4701},[3603,29115,12720],{"class":5576},[3603,29117,5580],{"class":5541},[3603,29119,5621],{"class":5576},[3603,29121,3453],{"class":5541},[3603,29123,5626],{"class":5576},[3603,29125,3453],{"class":5541},[3603,29127,24524],{"class":5220},[3603,29129,5707],{"class":5541},[3603,29131,29132],{"class":5576},"expiry",[3603,29134,12741],{"class":5541},[3603,29136,29137,29139,29141,29143,29145],{"class":3605,"line":4707},[3603,29138,12746],{"class":5576},[3603,29140,5580],{"class":5541},[3603,29142,12751],{"class":5576},[3603,29144,3453],{"class":5541},[3603,29146,12756],{"class":5576},[3603,29148,29149],{"class":3605,"line":4713},[3603,29150,16205],{"class":5541},[3603,29152,29153],{"class":3605,"line":4719},[3603,29154,3664],{"class":5541},[3603,29156,29157],{"class":3605,"line":4724},[3603,29158,3628],{"emptyLinePlaceholder":3627},[3603,29160,29161],{"class":3605,"line":4730},[3603,29162,29163],{"class":5214},"    \u002F\u002F Видалити → той самий код\n",[3603,29165,29166,29168,29170,29172,29174,29176,29178,29180],{"class":3605,"line":4736},[3603,29167,11012],{"class":5243},[3603,29169,11051],{"class":5243},[3603,29171,11054],{"class":5537},[3603,29173,16518],{"class":5220},[3603,29175,5707],{"class":5541},[3603,29177,7598],{"class":5243},[3603,29179,11071],{"class":5576},[3603,29181,6924],{"class":5541},[3603,29183,29184],{"class":3605,"line":4742},[3603,29185,11086],{"class":5541},[3603,29187,29188,29190,29192,29194,29196,29198,29200,29202,29204],{"class":3605,"line":4748},[3603,29189,16058],{"class":5243},[3603,29191,5225],{"class":5576},[3603,29193,3453],{"class":5541},[3603,29195,16543],{"class":5220},[3603,29197,5707],{"class":5541},[3603,29199,16084],{"class":5576},[3603,29201,3470],{"class":5541},[3603,29203,11126],{"class":5576},[3603,29205,5713],{"class":5541},[3603,29207,29208],{"class":3605,"line":4753},[3603,29209,3664],{"class":5541},[3603,29211,29212],{"class":3605,"line":4758},[3603,29213,3670],{"class":5541},[4801,29215,29217,29226,29234,29241,29244,29253,29260,29263,29273,29283,29286,29289],{"title":29216},"Тест завантаження через MinIO (локально)",[4805,29218,29220,4841,29223],{"className":29219},[3605],[3603,29221,7030],{"className":29222},[7029],[3356,29224,29225],{},"dotnet run",[4805,29227,29229,29233],{"className":29228},[3605],[3603,29230,29232],{"className":29231},[4811],"info:"," Application started. Press Ctrl+C to shut down.",[4805,29235,29237,29240],{"className":29236},[3605],[3603,29238,29232],{"className":29239},[7044]," Now listening on: http:\u002F\u002Flocalhost:5000",[4805,29242,4841],{"className":29243},[3605],[4805,29245,29247,4841,29250],{"className":29246},[3605],[3603,29248,7030],{"className":29249},[7029],[3356,29251,29252],{},"curl -X POST http:\u002F\u002Flocalhost:5000\u002Fapi\u002Ffiles \\",[4805,29254,29256,29257],{"className":29255},[3605],"     ",[3356,29258,29259],{},"-F \"file=@photo.jpg\" -F \"folder=avatars\"",[4805,29261,5618],{"className":29262},[3605],[4805,29264,7040,29266,5881,29269,7063],{"className":29265},[3605],[3603,29267,12382],{"className":29268},[7044],[3603,29270,29272],{"className":29271},[4811],"\"avatars\u002Fuser-1\u002F2026-05-28-photo.jpg\"",[4805,29274,7040,29276,5881,29279],{"className":29275},[3605],[3603,29277,12129],{"className":29278},[7044],[3603,29280,29282],{"className":29281},[7085],"\"http:\u002F\u002Flocalhost:9000\u002Fuploads\u002Favatars\u002Fuser-1\u002F2026-05-28-photo.jpg?X-Amz-...\"",[4805,29284,5645],{"className":29285},[3605],[4805,29287,4841],{"className":29288},[3605],[4805,29290,29292],{"className":29291},[3605],[3603,29293,29295],{"className":29294},[7029],"# Відкрийте URL у браузері — файл доступний через MinIO",[3405,29297],{},[3412,29299,29301],{"id":29300},"minio-у-тестах-net","MinIO у тестах (.NET)",[3353,29303,29304,29305,29308],{},"MinIO ідеально підходить для ",[3356,29306,29307],{},"інтеграційних тестів"," — замість mock-ів ви тестуєте реальну S3-поведінку локально.",[3561,29310,29312],{"className":5519,"code":29311,"language":5521,"meta":3569,"style":3569},"\u002F\u002F TestContainers — запуск MinIO у тестах автоматично\n\u002F\u002F dotnet add package Testcontainers.Minio\n\npublic class FileStorageTests : IAsyncLifetime\n{\n    private MinioContainer _minio = null!;\n    private IAmazonS3 _s3 = null!;\n\n    public async Task InitializeAsync()\n    {\n        _minio = new MinioBuilder()\n            .WithUsername(\"testuser\")\n            .WithPassword(\"testpass12\")\n            .Build();\n\n        await _minio.StartAsync();\n\n        _s3 = new AmazonS3Client(\n            new BasicAWSCredentials(_minio.GetAccessKey(), _minio.GetSecretKey()),\n            new AmazonS3Config\n            {\n                ServiceURL = _minio.GetConnectionString(), \u002F\u002F http:\u002F\u002Flocalhost:PORT\n                ForcePathStyle = true\n            }\n        );\n\n        \u002F\u002F Створити тестовий bucket\n        await _s3.PutBucketAsync(\"test-bucket\");\n    }\n\n    [Fact]\n    public async Task UploadAndDownload_ShouldWork()\n    {\n        var content = \"Hello, MinIO!\"u8.ToArray();\n        using var stream = new MemoryStream(content);\n\n        await _s3.PutObjectAsync(new PutObjectRequest\n        {\n            BucketName = \"test-bucket\",\n            Key = \"test\u002Fhello.txt\",\n            InputStream = stream,\n            ContentType = \"text\u002Fplain\"\n        });\n\n        var response = await _s3.GetObjectAsync(\"test-bucket\", \"test\u002Fhello.txt\");\n        using var reader = new StreamReader(response.ResponseStream);\n        var result = await reader.ReadToEndAsync();\n\n        Assert.Equal(\"Hello, MinIO!\", result);\n    }\n\n    public async Task DisposeAsync() => await _minio.DisposeAsync();\n}\n",[3432,29313,29314,29319,29324,29328,29342,29346,29363,29377,29381,29394,29398,29412,29427,29441,29449,29453,29466,29470,29482,29512,29518,29522,29540,29549,29553,29558,29562,29567,29585,29589,29593,29602,29615,29619,29640,29661,29665,29681,29685,29695,29706,29716,29725,29729,29733,29759,29786,29806,29810,29831,29835,29839,29864],{"__ignoreMap":3569},[3603,29315,29316],{"class":3605,"line":3606},[3603,29317,29318],{"class":5214},"\u002F\u002F TestContainers — запуск MinIO у тестах автоматично\n",[3603,29320,29321],{"class":3605,"line":3612},[3603,29322,29323],{"class":5214},"\u002F\u002F dotnet add package Testcontainers.Minio\n",[3603,29325,29326],{"class":3605,"line":3618},[3603,29327,3628],{"emptyLinePlaceholder":3627},[3603,29329,29330,29332,29334,29337,29339],{"class":3605,"line":3624},[3603,29331,10953],{"class":5243},[3603,29333,10956],{"class":5243},[3603,29335,29336],{"class":5537}," FileStorageTests",[3603,29338,17252],{"class":5541},[3603,29340,29341],{"class":5537},"IAsyncLifetime\n",[3603,29343,29344],{"class":3605,"line":3631},[3603,29345,5591],{"class":5541},[3603,29347,29348,29350,29353,29356,29358,29360],{"class":3605,"line":3637},[3603,29349,10968],{"class":5243},[3603,29351,29352],{"class":5537}," MinioContainer",[3603,29354,29355],{"class":5576}," _minio",[3603,29357,5580],{"class":5541},[3603,29359,6124],{"class":5243},[3603,29361,29362],{"class":5541},"!;\n",[3603,29364,29365,29367,29369,29371,29373,29375],{"class":3605,"line":3643},[3603,29366,10968],{"class":5243},[3603,29368,10974],{"class":5537},[3603,29370,10977],{"class":5576},[3603,29372,5580],{"class":5541},[3603,29374,6124],{"class":5243},[3603,29376,29362],{"class":5541},[3603,29378,29379],{"class":3605,"line":3649},[3603,29380,3628],{"emptyLinePlaceholder":3627},[3603,29382,29383,29385,29387,29389,29392],{"class":3605,"line":3655},[3603,29384,11012],{"class":5243},[3603,29386,11051],{"class":5243},[3603,29388,11054],{"class":5537},[3603,29390,29391],{"class":5220}," InitializeAsync",[3603,29393,15281],{"class":5541},[3603,29395,29396],{"class":3605,"line":3661},[3603,29397,11086],{"class":5541},[3603,29399,29400,29403,29405,29407,29410],{"class":3605,"line":3667},[3603,29401,29402],{"class":5576},"        _minio",[3603,29404,5580],{"class":5541},[3603,29406,5583],{"class":5243},[3603,29408,29409],{"class":5537}," MinioBuilder",[3603,29411,15281],{"class":5541},[3603,29413,29414,29417,29420,29422,29425],{"class":3605,"line":3673},[3603,29415,29416],{"class":5541},"            .",[3603,29418,29419],{"class":5220},"WithUsername",[3603,29421,5707],{"class":5541},[3603,29423,29424],{"class":5224},"\"testuser\"",[3603,29426,6924],{"class":5541},[3603,29428,29429,29431,29434,29436,29439],{"class":3605,"line":3678},[3603,29430,29416],{"class":5541},[3603,29432,29433],{"class":5220},"WithPassword",[3603,29435,5707],{"class":5541},[3603,29437,29438],{"class":5224},"\"testpass12\"",[3603,29440,6924],{"class":5541},[3603,29442,29443,29445,29447],{"class":3605,"line":3684},[3603,29444,29416],{"class":5541},[3603,29446,23815],{"class":5220},[3603,29448,16490],{"class":5541},[3603,29450,29451],{"class":3605,"line":3690},[3603,29452,3628],{"emptyLinePlaceholder":3627},[3603,29454,29455,29457,29459,29461,29464],{"class":3605,"line":3696},[3603,29456,16058],{"class":5243},[3603,29458,29355],{"class":5576},[3603,29460,3453],{"class":5541},[3603,29462,29463],{"class":5220},"StartAsync",[3603,29465,16490],{"class":5541},[3603,29467,29468],{"class":3605,"line":3702},[3603,29469,3628],{"emptyLinePlaceholder":3627},[3603,29471,29472,29474,29476,29478,29480],{"class":3605,"line":3708},[3603,29473,15920],{"class":5576},[3603,29475,5580],{"class":5541},[3603,29477,5583],{"class":5243},[3603,29479,14412],{"class":5537},[3603,29481,14825],{"class":5541},[3603,29483,29484,29487,29489,29491,29494,29496,29499,29502,29504,29506,29509],{"class":3605,"line":4667},[3603,29485,29486],{"class":5243},"            new",[3603,29488,28720],{"class":5537},[3603,29490,5707],{"class":5541},[3603,29492,29493],{"class":5576},"_minio",[3603,29495,3453],{"class":5541},[3603,29497,29498],{"class":5220},"GetAccessKey",[3603,29500,29501],{"class":5541},"(), ",[3603,29503,29493],{"class":5576},[3603,29505,3453],{"class":5541},[3603,29507,29508],{"class":5220},"GetSecretKey",[3603,29510,29511],{"class":5541},"()),\n",[3603,29513,29514,29516],{"class":3605,"line":4673},[3603,29515,29486],{"class":5243},[3603,29517,14362],{"class":5537},[3603,29519,29520],{"class":3605,"line":4679},[3603,29521,16156],{"class":5541},[3603,29523,29524,29527,29529,29531,29533,29535,29537],{"class":3605,"line":4685},[3603,29525,29526],{"class":5576},"                ServiceURL",[3603,29528,5580],{"class":5541},[3603,29530,29493],{"class":5576},[3603,29532,3453],{"class":5541},[3603,29534,23651],{"class":5220},[3603,29536,29501],{"class":5541},[3603,29538,29539],{"class":5214},"\u002F\u002F http:\u002F\u002Flocalhost:PORT\n",[3603,29541,29542,29545,29547],{"class":3605,"line":4691},[3603,29543,29544],{"class":5576},"                ForcePathStyle",[3603,29546,5580],{"class":5541},[3603,29548,28363],{"class":5243},[3603,29550,29551],{"class":3605,"line":4696},[3603,29552,6045],{"class":5541},[3603,29554,29555],{"class":3605,"line":4701},[3603,29556,29557],{"class":5541},"        );\n",[3603,29559,29560],{"class":3605,"line":4707},[3603,29561,3628],{"emptyLinePlaceholder":3627},[3603,29563,29564],{"class":3605,"line":4713},[3603,29565,29566],{"class":5214},"        \u002F\u002F Створити тестовий bucket\n",[3603,29568,29569,29571,29573,29575,29578,29580,29583],{"class":3605,"line":4719},[3603,29570,16058],{"class":5243},[3603,29572,10977],{"class":5576},[3603,29574,3453],{"class":5541},[3603,29576,29577],{"class":5220},"PutBucketAsync",[3603,29579,5707],{"class":5541},[3603,29581,29582],{"class":5224},"\"test-bucket\"",[3603,29584,5713],{"class":5541},[3603,29586,29587],{"class":3605,"line":4724},[3603,29588,3664],{"class":5541},[3603,29590,29591],{"class":3605,"line":4730},[3603,29592,3628],{"emptyLinePlaceholder":3627},[3603,29594,29595,29597,29600],{"class":3605,"line":4736},[3603,29596,17315],{"class":5541},[3603,29598,29599],{"class":5537},"Fact",[3603,29601,13913],{"class":5541},[3603,29603,29604,29606,29608,29610,29613],{"class":3605,"line":4742},[3603,29605,11012],{"class":5243},[3603,29607,11051],{"class":5243},[3603,29609,11054],{"class":5537},[3603,29611,29612],{"class":5220}," UploadAndDownload_ShouldWork",[3603,29614,15281],{"class":5541},[3603,29616,29617],{"class":3605,"line":4748},[3603,29618,11086],{"class":5541},[3603,29620,29621,29623,29625,29627,29630,29633,29635,29638],{"class":3605,"line":4753},[3603,29622,11091],{"class":5243},[3603,29624,28940],{"class":5576},[3603,29626,5580],{"class":5541},[3603,29628,29629],{"class":5224},"\"Hello, MinIO!\"",[3603,29631,29632],{"class":5576},"u8",[3603,29634,3453],{"class":5541},[3603,29636,29637],{"class":5220},"ToArray",[3603,29639,16490],{"class":5541},[3603,29641,29642,29644,29646,29648,29650,29652,29655,29657,29659],{"class":3605,"line":4758},[3603,29643,16030],{"class":5533},[3603,29645,16033],{"class":5243},[3603,29647,17454],{"class":5576},[3603,29649,5580],{"class":5541},[3603,29651,5583],{"class":5243},[3603,29653,29654],{"class":5537}," MemoryStream",[3603,29656,5707],{"class":5541},[3603,29658,29007],{"class":5576},[3603,29660,5713],{"class":5541},[3603,29662,29663],{"class":3605,"line":6363},[3603,29664,3628],{"emptyLinePlaceholder":3627},[3603,29666,29667,29669,29671,29673,29675,29677,29679],{"class":3605,"line":6368},[3603,29668,16058],{"class":5243},[3603,29670,10977],{"class":5576},[3603,29672,3453],{"class":5541},[3603,29674,5704],{"class":5220},[3603,29676,5707],{"class":5541},[3603,29678,5583],{"class":5243},[3603,29680,5586],{"class":5537},[3603,29682,29683],{"class":3605,"line":6830},[3603,29684,5873],{"class":5541},[3603,29686,29687,29689,29691,29693],{"class":3605,"line":6835},[3603,29688,11109],{"class":5576},[3603,29690,5580],{"class":5541},[3603,29692,29582],{"class":5224},[3603,29694,5604],{"class":5541},[3603,29696,29697,29699,29701,29704],{"class":3605,"line":6841},[3603,29698,11121],{"class":5576},[3603,29700,5580],{"class":5541},[3603,29702,29703],{"class":5224},"\"test\u002Fhello.txt\"",[3603,29705,5604],{"class":5541},[3603,29707,29708,29710,29712,29714],{"class":3605,"line":6858},[3603,29709,11133],{"class":5576},[3603,29711,5580],{"class":5541},[3603,29713,17487],{"class":5576},[3603,29715,5604],{"class":5541},[3603,29717,29718,29720,29722],{"class":3605,"line":6871},[3603,29719,12915],{"class":5576},[3603,29721,5580],{"class":5541},[3603,29723,29724],{"class":5224},"\"text\u002Fplain\"\n",[3603,29726,29727],{"class":3605,"line":6881},[3603,29728,16205],{"class":5541},[3603,29730,29731],{"class":3605,"line":6894},[3603,29732,3628],{"emptyLinePlaceholder":3627},[3603,29734,29735,29737,29739,29741,29743,29745,29747,29749,29751,29753,29755,29757],{"class":3605,"line":6915},[3603,29736,11091],{"class":5243},[3603,29738,11190],{"class":5576},[3603,29740,5580],{"class":5541},[3603,29742,5696],{"class":5243},[3603,29744,10977],{"class":5576},[3603,29746,3453],{"class":5541},[3603,29748,11293],{"class":5220},[3603,29750,5707],{"class":5541},[3603,29752,29582],{"class":5224},[3603,29754,3470],{"class":5541},[3603,29756,29703],{"class":5224},[3603,29758,5713],{"class":5541},[3603,29760,29761,29763,29765,29768,29770,29772,29775,29777,29780,29782,29784],{"class":3605,"line":6927},[3603,29762,16030],{"class":5533},[3603,29764,16033],{"class":5243},[3603,29766,29767],{"class":5576}," reader",[3603,29769,5580],{"class":5541},[3603,29771,5583],{"class":5243},[3603,29773,29774],{"class":5537}," StreamReader",[3603,29776,5707],{"class":5541},[3603,29778,29779],{"class":5576},"response",[3603,29781,3453],{"class":5541},[3603,29783,11315],{"class":5576},[3603,29785,5713],{"class":5541},[3603,29787,29788,29790,29793,29795,29797,29799,29801,29804],{"class":3605,"line":6932},[3603,29789,11091],{"class":5243},[3603,29791,29792],{"class":5576}," result",[3603,29794,5580],{"class":5541},[3603,29796,5696],{"class":5243},[3603,29798,29767],{"class":5576},[3603,29800,3453],{"class":5541},[3603,29802,29803],{"class":5220},"ReadToEndAsync",[3603,29805,16490],{"class":5541},[3603,29807,29808],{"class":3605,"line":6946},[3603,29809,3628],{"emptyLinePlaceholder":3627},[3603,29811,29812,29815,29817,29820,29822,29824,29826,29829],{"class":3605,"line":6951},[3603,29813,29814],{"class":5576},"        Assert",[3603,29816,3453],{"class":5541},[3603,29818,29819],{"class":5220},"Equal",[3603,29821,5707],{"class":5541},[3603,29823,29629],{"class":5224},[3603,29825,3470],{"class":5541},[3603,29827,29828],{"class":5576},"result",[3603,29830,5713],{"class":5541},[3603,29832,29833],{"class":3605,"line":6963},[3603,29834,3664],{"class":5541},[3603,29836,29837],{"class":3605,"line":6976},[3603,29838,3628],{"emptyLinePlaceholder":3627},[3603,29840,29841,29843,29845,29847,29850,29853,29855,29857,29859,29862],{"class":3605,"line":6985},[3603,29842,11012],{"class":5243},[3603,29844,11051],{"class":5243},[3603,29846,11054],{"class":5537},[3603,29848,29849],{"class":5220}," DisposeAsync",[3603,29851,29852],{"class":5541},"() => ",[3603,29854,5696],{"class":5243},[3603,29856,29355],{"class":5576},[3603,29858,3453],{"class":5541},[3603,29860,29861],{"class":5220},"DisposeAsync",[3603,29863,16490],{"class":5541},[3603,29865,29866],{"class":3605,"line":6998},[3603,29867,3670],{"class":5541},[29869,29870,29871,29874],"tip",{},[3432,29872,29873],{},"Testcontainers.Minio"," автоматично завантажує MinIO Docker образ, запускає його на випадковому порту та зупиняє після тестів. Не потрібно вручну керувати жодним контейнером.",[3405,29876],{},[3412,29878,29880],{"id":29879},"minio-у-github-actions","MinIO у GitHub Actions",[3561,29882,29886],{"className":29883,"code":29884,"language":29885,"meta":3569,"style":3569},"language-yaml shiki shiki-themes light-plus dark-plus dark-plus","# .github\u002Fworkflows\u002Fci.yml\nname: CI\n\non: [push, pull_request]\n\njobs:\n    test:\n        runs-on: ubuntu-latest\n\n        services:\n            minio:\n                image: quay.io\u002Fminio\u002Fminio:latest\n                ports:\n                    - 9000:9000\n                env:\n                    MINIO_ROOT_USER: minioadmin\n                    MINIO_ROOT_PASSWORD: minioadmin\n                options: >-\n                    --health-cmd \"curl -f http:\u002F\u002Flocalhost:9000\u002Fminio\u002Fhealth\u002Flive\"\n                    --health-interval 5s\n                    --health-retries 5\n\n        steps:\n            - uses: actions\u002Fcheckout@v4\n\n            - name: Setup .NET\n              uses: actions\u002Fsetup-dotnet@v4\n              with:\n                  dotnet-version: '9.0.x'\n\n            - name: Create test bucket\n              run: |\n                  curl -O https:\u002F\u002Fdl.min.io\u002Fclient\u002Fmc\u002Frelease\u002Flinux-amd64\u002Fmc\n                  chmod +x mc\n                  .\u002Fmc alias set local http:\u002F\u002Flocalhost:9000 minioadmin minioadmin\n                  .\u002Fmc mb local\u002Ftest-bucket\n\n            - name: Run tests\n              run: dotnet test\n              env:\n                  AWS__ServiceURL: http:\u002F\u002Flocalhost:9000\n                  AWS__AccessKey: minioadmin\n                  AWS__SecretKey: minioadmin\n                  AWS__BucketName: test-bucket\n                  AWS__ForcePathStyle: 'true'\n","yaml",[3432,29887,29888,29893,29903,29907,29923,29927,29934,29941,29951,29955,29962,29968,29977,29984,29992,29999,30008,30017,30029,30034,30039,30044,30048,30055,30067,30071,30082,30092,30099,30109,30113,30124,30134,30139,30144,30149,30154,30158,30169,30178,30185,30195,30204,30213,30223],{"__ignoreMap":3569},[3603,29889,29890],{"class":3605,"line":3606},[3603,29891,29892],{"class":5214},"# .github\u002Fworkflows\u002Fci.yml\n",[3603,29894,29895,29898,29900],{"class":3605,"line":3612},[3603,29896,29897],{"class":15368},"name",[3603,29899,5881],{"class":5541},[3603,29901,29902],{"class":27475},"CI\n",[3603,29904,29905],{"class":3605,"line":3618},[3603,29906,3628],{"emptyLinePlaceholder":3627},[3603,29908,29909,29911,29913,29916,29918,29921],{"class":3605,"line":3624},[3603,29910,15229],{"class":5243},[3603,29912,7046],{"class":5541},[3603,29914,29915],{"class":27475},"push",[3603,29917,3470],{"class":5541},[3603,29919,29920],{"class":27475},"pull_request",[3603,29922,13913],{"class":5541},[3603,29924,29925],{"class":3605,"line":3631},[3603,29926,3628],{"emptyLinePlaceholder":3627},[3603,29928,29929,29932],{"class":3605,"line":3637},[3603,29930,29931],{"class":15368},"jobs",[3603,29933,27444],{"class":5541},[3603,29935,29936,29939],{"class":3605,"line":3643},[3603,29937,29938],{"class":15368},"    test",[3603,29940,27444],{"class":5541},[3603,29942,29943,29946,29948],{"class":3605,"line":3649},[3603,29944,29945],{"class":15368},"        runs-on",[3603,29947,5881],{"class":5541},[3603,29949,29950],{"class":27475},"ubuntu-latest\n",[3603,29952,29953],{"class":3605,"line":3655},[3603,29954,3628],{"emptyLinePlaceholder":3627},[3603,29956,29957,29960],{"class":3605,"line":3661},[3603,29958,29959],{"class":15368},"        services",[3603,29961,27444],{"class":5541},[3603,29963,29964,29966],{"class":3605,"line":3667},[3603,29965,27523],{"class":15368},[3603,29967,27444],{"class":5541},[3603,29969,29970,29973,29975],{"class":3605,"line":3673},[3603,29971,29972],{"class":15368},"                image",[3603,29974,5881],{"class":5541},[3603,29976,27556],{"class":27475},[3603,29978,29979,29982],{"class":3605,"line":3678},[3603,29980,29981],{"class":15368},"                ports",[3603,29983,27444],{"class":5541},[3603,29985,29986,29989],{"class":3605,"line":3684},[3603,29987,29988],{"class":5541},"                    - ",[3603,29990,29991],{"class":27475},"9000:9000\n",[3603,29993,29994,29997],{"class":3605,"line":3690},[3603,29995,29996],{"class":15368},"                env",[3603,29998,27444],{"class":5541},[3603,30000,30001,30004,30006],{"class":3605,"line":3696},[3603,30002,30003],{"class":15368},"                    MINIO_ROOT_USER",[3603,30005,5881],{"class":5541},[3603,30007,27602],{"class":27475},[3603,30009,30010,30013,30015],{"class":3605,"line":3702},[3603,30011,30012],{"class":15368},"                    MINIO_ROOT_PASSWORD",[3603,30014,5881],{"class":5541},[3603,30016,27602],{"class":27475},[3603,30018,30019,30022,30024,30026],{"class":3605,"line":3708},[3603,30020,30021],{"class":15368},"                options",[3603,30023,5881],{"class":5541},[3603,30025,18124],{"class":5533},[3603,30027,30028],{"class":5243},"-\n",[3603,30030,30031],{"class":3605,"line":4667},[3603,30032,30033],{"class":27475},"                    --health-cmd \"curl -f http:\u002F\u002Flocalhost:9000\u002Fminio\u002Fhealth\u002Flive\"\n",[3603,30035,30036],{"class":3605,"line":4673},[3603,30037,30038],{"class":27475},"                    --health-interval 5s\n",[3603,30040,30041],{"class":3605,"line":4679},[3603,30042,30043],{"class":27475},"                    --health-retries 5\n",[3603,30045,30046],{"class":3605,"line":4685},[3603,30047,3628],{"emptyLinePlaceholder":3627},[3603,30049,30050,30053],{"class":3605,"line":4691},[3603,30051,30052],{"class":15368},"        steps",[3603,30054,27444],{"class":5541},[3603,30056,30057,30059,30062,30064],{"class":3605,"line":4696},[3603,30058,27472],{"class":5541},[3603,30060,30061],{"class":15368},"uses",[3603,30063,5881],{"class":5541},[3603,30065,30066],{"class":27475},"actions\u002Fcheckout@v4\n",[3603,30068,30069],{"class":3605,"line":4701},[3603,30070,3628],{"emptyLinePlaceholder":3627},[3603,30072,30073,30075,30077,30079],{"class":3605,"line":4707},[3603,30074,27472],{"class":5541},[3603,30076,29897],{"class":15368},[3603,30078,5881],{"class":5541},[3603,30080,30081],{"class":27475},"Setup .NET\n",[3603,30083,30084,30087,30089],{"class":3605,"line":4713},[3603,30085,30086],{"class":15368},"              uses",[3603,30088,5881],{"class":5541},[3603,30090,30091],{"class":27475},"actions\u002Fsetup-dotnet@v4\n",[3603,30093,30094,30097],{"class":3605,"line":4719},[3603,30095,30096],{"class":15368},"              with",[3603,30098,27444],{"class":5541},[3603,30100,30101,30104,30106],{"class":3605,"line":4724},[3603,30102,30103],{"class":15368},"                  dotnet-version",[3603,30105,5881],{"class":5541},[3603,30107,30108],{"class":27475},"'9.0.x'\n",[3603,30110,30111],{"class":3605,"line":4730},[3603,30112,3628],{"emptyLinePlaceholder":3627},[3603,30114,30115,30117,30119,30121],{"class":3605,"line":4736},[3603,30116,27472],{"class":5541},[3603,30118,29897],{"class":15368},[3603,30120,5881],{"class":5541},[3603,30122,30123],{"class":27475},"Create test bucket\n",[3603,30125,30126,30129,30131],{"class":3605,"line":4742},[3603,30127,30128],{"class":15368},"              run",[3603,30130,5881],{"class":5541},[3603,30132,30133],{"class":5533},"|\n",[3603,30135,30136],{"class":3605,"line":4748},[3603,30137,30138],{"class":27475},"                  curl -O https:\u002F\u002Fdl.min.io\u002Fclient\u002Fmc\u002Frelease\u002Flinux-amd64\u002Fmc\n",[3603,30140,30141],{"class":3605,"line":4753},[3603,30142,30143],{"class":27475},"                  chmod +x mc\n",[3603,30145,30146],{"class":3605,"line":4758},[3603,30147,30148],{"class":27475},"                  .\u002Fmc alias set local http:\u002F\u002Flocalhost:9000 minioadmin minioadmin\n",[3603,30150,30151],{"class":3605,"line":6363},[3603,30152,30153],{"class":27475},"                  .\u002Fmc mb local\u002Ftest-bucket\n",[3603,30155,30156],{"class":3605,"line":6368},[3603,30157,3628],{"emptyLinePlaceholder":3627},[3603,30159,30160,30162,30164,30166],{"class":3605,"line":6830},[3603,30161,27472],{"class":5541},[3603,30163,29897],{"class":15368},[3603,30165,5881],{"class":5541},[3603,30167,30168],{"class":27475},"Run tests\n",[3603,30170,30171,30173,30175],{"class":3605,"line":6835},[3603,30172,30128],{"class":15368},[3603,30174,5881],{"class":5541},[3603,30176,30177],{"class":27475},"dotnet test\n",[3603,30179,30180,30183],{"class":3605,"line":6841},[3603,30181,30182],{"class":15368},"              env",[3603,30184,27444],{"class":5541},[3603,30186,30187,30190,30192],{"class":3605,"line":6858},[3603,30188,30189],{"class":15368},"                  AWS__ServiceURL",[3603,30191,5881],{"class":5541},[3603,30193,30194],{"class":27475},"http:\u002F\u002Flocalhost:9000\n",[3603,30196,30197,30200,30202],{"class":3605,"line":6871},[3603,30198,30199],{"class":15368},"                  AWS__AccessKey",[3603,30201,5881],{"class":5541},[3603,30203,27602],{"class":27475},[3603,30205,30206,30209,30211],{"class":3605,"line":6881},[3603,30207,30208],{"class":15368},"                  AWS__SecretKey",[3603,30210,5881],{"class":5541},[3603,30212,27602],{"class":27475},[3603,30214,30215,30218,30220],{"class":3605,"line":6894},[3603,30216,30217],{"class":15368},"                  AWS__BucketName",[3603,30219,5881],{"class":5541},[3603,30221,30222],{"class":27475},"test-bucket\n",[3603,30224,30225,30228,30230],{"class":3605,"line":6915},[3603,30226,30227],{"class":15368},"                  AWS__ForcePathStyle",[3603,30229,5881],{"class":5541},[3603,30231,30232],{"class":27475},"'true'\n",[4801,30234,30236,30246,30255,30262,30269,30278,30282,30286,30295,30299,30312],{"title":30235},"GitHub Actions — MinIO сервіс",[4805,30237,30239,4841,30243],{"className":30238},[3605],[3603,30240,30242],{"className":30241},[4811],"✓",[3356,30244,30245],{},"Set up job",[4805,30247,30249,4841,30252],{"className":30248},[3605],[3603,30250,30242],{"className":30251},[4811],[3356,30253,30254],{},"Initialize containers",[4805,30256,7040,30258],{"className":30257},[3605],[3603,30259,30261],{"className":30260},[7029],"Starting service container minio (quay.io\u002Fminio\u002Fminio:latest)",[4805,30263,7040,30265],{"className":30264},[3605],[3603,30266,30268],{"className":30267},[4811],"Service container minio healthy",[4805,30270,30272,4841,30275],{"className":30271},[3605],[3603,30273,30242],{"className":30274},[4811],[3356,30276,30277],{},"Create test bucket",[4805,30279,30281],{"className":30280},[3605],"  Added `local` successfully.",[4805,30283,30285],{"className":30284},[3605],"  Bucket created successfully `local\u002Ftest-bucket`.",[4805,30287,30289,4841,30292],{"className":30288},[3605],[3603,30290,30242],{"className":30291},[4811],[3356,30293,30294],{},"Run tests",[4805,30296,30298],{"className":30297},[3605],"  Build started...",[4805,30300,7040,30302,30306,30307,30311],{"className":30301},[3605],[3603,30303,30305],{"className":30304},[7044],"Passed!","  - Failed: 0, Passed: ",[3603,30308,30310],{"className":30309},[4811],"24",", Skipped: 0, Total: 24",[4805,30313,30315],{"className":30314},[3605],"  Test Run Successful.",[3405,30317],{},[3412,30319,30321],{"id":30320},"переключення-між-minio-та-aws-s3-у-проекті","Переключення між MinIO та AWS S3 у проекті",[3353,30323,30324],{},"Основний патерн: одна конфігурація керує тим, куди іде трафік — MinIO локально або AWS S3 у продакшні.",[30326,30327,30328,30334,30379,30391,30398,30568,30572,30624,30628],"steps",{},[3412,30329,30331,30332],{"id":30330},"визначте-конфігурацію-у-appsettingsjson","Визначте конфігурацію у ",[3432,30333,15549],{},[3561,30335,30337],{"className":5851,"code":30336,"language":5853,"meta":3569,"style":3569},"{\n    \"Storage\": {\n        \"Provider\": \"minio\",\n        \"BucketName\": \"uploads\"\n    }\n}\n",[3432,30338,30339,30343,30350,30362,30371,30375],{"__ignoreMap":3569},[3603,30340,30341],{"class":3605,"line":3606},[3603,30342,5591],{"class":5541},[3603,30344,30345,30348],{"class":3605,"line":3612},[3603,30346,30347],{"class":5864},"    \"Storage\"",[3603,30349,5906],{"class":5541},[3603,30351,30352,30355,30357,30360],{"class":3605,"line":3618},[3603,30353,30354],{"class":5864},"        \"Provider\"",[3603,30356,5881],{"class":5541},[3603,30358,30359],{"class":5224},"\"minio\"",[3603,30361,5604],{"class":5541},[3603,30363,30364,30366,30368],{"class":3605,"line":3624},[3603,30365,15709],{"class":5864},[3603,30367,5881],{"class":5541},[3603,30369,30370],{"class":5224},"\"uploads\"\n",[3603,30372,30373],{"class":3605,"line":3631},[3603,30374,3664],{"class":5541},[3603,30376,30377],{"class":3605,"line":3637},[3603,30378,3670],{"class":5541},[3353,30380,30381,5580,30384,30386,30387,30390],{},[3432,30382,30383],{},"Provider",[3432,30385,30359],{}," у Development, ",[3432,30388,30389],{},"\"s3\""," у Production.",[3412,30392,30394,30395,30397],{"id":30393},"зареєструйте-iamazons3-залежно-від-провайдера","Зареєструйте ",[3432,30396,11020],{}," залежно від провайдера",[3561,30399,30401],{"className":5519,"code":30400,"language":5521,"meta":3569,"style":3569},"var provider = builder.Configuration[\"Storage:Provider\"];\n\nif (provider == \"minio\")\n{\n    builder.Services.AddSingleton\u003CIAmazonS3>(_ =>\n        new AmazonS3Client(\n            new BasicAWSCredentials(\"minioadmin\", \"minioadmin\"),\n            new AmazonS3Config { ServiceURL = \"http:\u002F\u002Flocalhost:9000\", ForcePathStyle = true }\n        ));\n}\nelse\n{\n    \u002F\u002F Продакшн — IAM Role, credentials з IMDS (без ключів у коді)\n    builder.Services.AddAWSService\u003CIAmazonS3>();\n}\n",[3432,30402,30403,30426,30430,30445,30449,30472,30481,30497,30523,30528,30532,30537,30541,30546,30564],{"__ignoreMap":3569},[3603,30404,30405,30407,30410,30412,30414,30416,30418,30420,30423],{"class":3605,"line":3606},[3603,30406,5573],{"class":5243},[3603,30408,30409],{"class":5576}," provider",[3603,30411,5580],{"class":5541},[3603,30413,15605],{"class":5576},[3603,30415,3453],{"class":5541},[3603,30417,15841],{"class":5576},[3603,30419,15943],{"class":5541},[3603,30421,30422],{"class":5224},"\"Storage:Provider\"",[3603,30424,30425],{"class":5541},"];\n",[3603,30427,30428],{"class":3605,"line":3612},[3603,30429,3628],{"emptyLinePlaceholder":3627},[3603,30431,30432,30434,30436,30439,30441,30443],{"class":3605,"line":3618},[3603,30433,23912],{"class":5533},[3603,30435,10425],{"class":5541},[3603,30437,30438],{"class":5576},"provider",[3603,30440,17373],{"class":5541},[3603,30442,30359],{"class":5224},[3603,30444,6924],{"class":5541},[3603,30446,30447],{"class":3605,"line":3624},[3603,30448,5591],{"class":5541},[3603,30450,30451,30454,30456,30458,30460,30462,30464,30466,30468,30470],{"class":3605,"line":3631},[3603,30452,30453],{"class":5576},"    builder",[3603,30455,3453],{"class":5541},[3603,30457,15610],{"class":5576},[3603,30459,3453],{"class":5541},[3603,30461,28487],{"class":5220},[3603,30463,11250],{"class":5541},[3603,30465,11020],{"class":5537},[3603,30467,23623],{"class":5541},[3603,30469,3483],{"class":5576},[3603,30471,23628],{"class":5541},[3603,30473,30474,30477,30479],{"class":3605,"line":3637},[3603,30475,30476],{"class":5243},"        new",[3603,30478,14412],{"class":5537},[3603,30480,14825],{"class":5541},[3603,30482,30483,30485,30487,30489,30491,30493,30495],{"class":3605,"line":3643},[3603,30484,29486],{"class":5243},[3603,30486,28720],{"class":5537},[3603,30488,5707],{"class":5541},[3603,30490,28329],{"class":5224},[3603,30492,3470],{"class":5541},[3603,30494,28329],{"class":5224},[3603,30496,12741],{"class":5541},[3603,30498,30499,30501,30504,30506,30508,30510,30512,30514,30517,30519,30521],{"class":3605,"line":3649},[3603,30500,29486],{"class":5243},[3603,30502,30503],{"class":5537}," AmazonS3Config",[3603,30505,12973],{"class":5541},[3603,30507,28659],{"class":5576},[3603,30509,5580],{"class":5541},[3603,30511,28317],{"class":5224},[3603,30513,3470],{"class":5541},[3603,30515,30516],{"class":5576},"ForcePathStyle",[3603,30518,5580],{"class":5541},[3603,30520,6905],{"class":5243},[3603,30522,7502],{"class":5541},[3603,30524,30525],{"class":3605,"line":3655},[3603,30526,30527],{"class":5541},"        ));\n",[3603,30529,30530],{"class":3605,"line":3661},[3603,30531,3670],{"class":5541},[3603,30533,30534],{"class":3605,"line":3667},[3603,30535,30536],{"class":5533},"else\n",[3603,30538,30539],{"class":3605,"line":3673},[3603,30540,5591],{"class":5541},[3603,30542,30543],{"class":3605,"line":3678},[3603,30544,30545],{"class":5214},"    \u002F\u002F Продакшн — IAM Role, credentials з IMDS (без ключів у коді)\n",[3603,30547,30548,30550,30552,30554,30556,30558,30560,30562],{"class":3605,"line":3684},[3603,30549,30453],{"class":5576},[3603,30551,3453],{"class":5541},[3603,30553,15610],{"class":5576},[3603,30555,3453],{"class":5541},[3603,30557,15615],{"class":5220},[3603,30559,11250],{"class":5541},[3603,30561,11020],{"class":5537},[3603,30563,15622],{"class":5541},[3603,30565,30566],{"class":3605,"line":3690},[3603,30567,3670],{"class":5541},[3412,30569,30571],{"id":30570},"весь-інший-код-залишається-незмінним","Весь інший код залишається незмінним",[3561,30573,30575],{"className":5519,"code":30574,"language":5521,"meta":3569,"style":3569},"\u002F\u002F FileStorageService, UploadController, PresignedUrlController —\n\u002F\u002F всі інжектують IAmazonS3 і не знають, куди вони говорять\npublic class UploadController(IAmazonS3 s3, IConfiguration cfg)\n{\n    \u002F\u002F Той самий код що і з AWS S3\n}\n",[3432,30576,30577,30582,30587,30611,30615,30620],{"__ignoreMap":3569},[3603,30578,30579],{"class":3605,"line":3606},[3603,30580,30581],{"class":5214},"\u002F\u002F FileStorageService, UploadController, PresignedUrlController —\n",[3603,30583,30584],{"class":3605,"line":3612},[3603,30585,30586],{"class":5214},"\u002F\u002F всі інжектують IAmazonS3 і не знають, куди вони говорять\n",[3603,30588,30589,30591,30593,30596,30598,30600,30602,30604,30606,30609],{"class":3605,"line":3618},[3603,30590,10953],{"class":5243},[3603,30592,10956],{"class":5243},[3603,30594,30595],{"class":5537}," UploadController",[3603,30597,5707],{"class":5541},[3603,30599,11020],{"class":5537},[3603,30601,5225],{"class":5576},[3603,30603,3470],{"class":5541},[3603,30605,15907],{"class":5537},[3603,30607,30608],{"class":5576}," cfg",[3603,30610,6924],{"class":5541},[3603,30612,30613],{"class":3605,"line":3624},[3603,30614,5591],{"class":5541},[3603,30616,30617],{"class":3605,"line":3631},[3603,30618,30619],{"class":5214},"    \u002F\u002F Той самий код що і з AWS S3\n",[3603,30621,30622],{"class":3605,"line":3637},[3603,30623,3670],{"class":5541},[3412,30625,30627],{"id":30626},"перевірте-локально","Перевірте локально",[3561,30629,30631],{"className":5205,"code":30630,"language":5207,"meta":3569,"style":3569},"dotnet run --environment Development\n# → Storage: MinIO @ http:\u002F\u002Flocalhost:9000\n\ndotnet run --environment Production\n# → Storage: AWS S3 @ eu-central-1\n",[3432,30632,30633,30645,30650,30654,30665],{"__ignoreMap":3569},[3603,30634,30635,30637,30639,30642],{"class":3605,"line":3606},[3603,30636,15564],{"class":5220},[3603,30638,20737],{"class":5224},[3603,30640,30641],{"class":5243}," --environment",[3603,30643,30644],{"class":5224}," Development\n",[3603,30646,30647],{"class":3605,"line":3612},[3603,30648,30649],{"class":5214},"# → Storage: MinIO @ http:\u002F\u002Flocalhost:9000\n",[3603,30651,30652],{"class":3605,"line":3618},[3603,30653,3628],{"emptyLinePlaceholder":3627},[3603,30655,30656,30658,30660,30662],{"class":3605,"line":3624},[3603,30657,15564],{"class":5220},[3603,30659,20737],{"class":5224},[3603,30661,30641],{"class":5243},[3603,30663,30664],{"class":5224}," Production\n",[3603,30666,30667],{"class":3605,"line":3631},[3603,30668,30669],{"class":5214},"# → Storage: AWS S3 @ eu-central-1\n",[3405,30671],{},[3412,30673,30675],{"id":30674},"minio-для-self-hosted-продакшн","MinIO для self-hosted продакшн",[3353,30677,30678],{},"Якщо ваш проект вимагає зберігати дані на власних серверах (GDPR, compliance, низькі витрати на великих обсягах), MinIO можна запустити у продакшн-режимі.",[3353,30680,30681],{},[3356,30682,30683],{},"Distributed Mode (erasure coding):",[3561,30685,30687],{"className":5205,"code":30686,"language":5207,"meta":3569,"style":3569},"# 4 сервера × 4 диски = 16 дисків\n# MinIO зберігає дані з N\u002F2 парітетом — витримує відмову половини дисків\nminio server \\\n  http:\u002F\u002Fminio{1...4}\u002Fdata{1...4} \\\n  --console-address \":9001\"\n",[3432,30688,30689,30694,30699,30708,30715],{"__ignoreMap":3569},[3603,30690,30691],{"class":3605,"line":3606},[3603,30692,30693],{"class":5214},"# 4 сервера × 4 диски = 16 дисків\n",[3603,30695,30696],{"class":3605,"line":3612},[3603,30697,30698],{"class":5214},"# MinIO зберігає дані з N\u002F2 парітетом — витримує відмову половини дисків\n",[3603,30700,30701,30704,30706],{"class":3605,"line":3618},[3603,30702,30703],{"class":5220},"minio",[3603,30705,27313],{"class":5224},[3603,30707,5238],{"class":5237},[3603,30709,30710,30713],{"class":3605,"line":3624},[3603,30711,30712],{"class":5224},"  http:\u002F\u002Fminio{1...4}\u002Fdata{1...4}",[3603,30714,5238],{"class":5237},[3603,30716,30717,30720],{"class":3605,"line":3631},[3603,30718,30719],{"class":5243},"  --console-address",[3603,30721,27322],{"class":5224},[3353,30723,30724],{},[3356,30725,30726],{},"Kubernetes (MinIO Operator):",[3561,30728,30730],{"className":5205,"code":30729,"language":5207,"meta":3569,"style":3569},"# Встановити оператор\nkubectl apply -k github.com\u002Fminio\u002Foperator\n\n# Створити тенант (кластер MinIO)\nkubectl apply -f - \u003C\u003CEOF\napiVersion: minio.min.io\u002Fv2\nkind: Tenant\nmetadata:\n  name: minio-prod\nspec:\n  pools:\n    - servers: 4\n      volumesPerServer: 4\n      volumeClaimTemplate:\n        spec:\n          storageClassName: fast-ssd\n          resources:\n            requests:\n              storage: 1Ti\nEOF\n",[3432,30731,30732,30737,30751,30755,30760,30776,30781,30786,30791,30796,30801,30806,30811,30816,30821,30826,30831,30836,30841,30846],{"__ignoreMap":3569},[3603,30733,30734],{"class":3605,"line":3606},[3603,30735,30736],{"class":5214},"# Встановити оператор\n",[3603,30738,30739,30742,30745,30748],{"class":3605,"line":3612},[3603,30740,30741],{"class":5220},"kubectl",[3603,30743,30744],{"class":5224}," apply",[3603,30746,30747],{"class":5243}," -k",[3603,30749,30750],{"class":5224}," github.com\u002Fminio\u002Foperator\n",[3603,30752,30753],{"class":3605,"line":3618},[3603,30754,3628],{"emptyLinePlaceholder":3627},[3603,30756,30757],{"class":3605,"line":3624},[3603,30758,30759],{"class":5214},"# Створити тенант (кластер MinIO)\n",[3603,30761,30762,30764,30766,30769,30771,30774],{"class":3605,"line":3631},[3603,30763,30741],{"class":5220},[3603,30765,30744],{"class":5224},[3603,30767,30768],{"class":5243}," -f",[3603,30770,15158],{"class":5224},[3603,30772,30773],{"class":5541}," \u003C\u003C",[3603,30775,7945],{"class":5541},[3603,30777,30778],{"class":3605,"line":3637},[3603,30779,30780],{"class":5224},"apiVersion: minio.min.io\u002Fv2\n",[3603,30782,30783],{"class":3605,"line":3643},[3603,30784,30785],{"class":5224},"kind: Tenant\n",[3603,30787,30788],{"class":3605,"line":3649},[3603,30789,30790],{"class":5224},"metadata:\n",[3603,30792,30793],{"class":3605,"line":3655},[3603,30794,30795],{"class":5224},"  name: minio-prod\n",[3603,30797,30798],{"class":3605,"line":3661},[3603,30799,30800],{"class":5224},"spec:\n",[3603,30802,30803],{"class":3605,"line":3667},[3603,30804,30805],{"class":5224},"  pools:\n",[3603,30807,30808],{"class":3605,"line":3673},[3603,30809,30810],{"class":5224},"    - servers: 4\n",[3603,30812,30813],{"class":3605,"line":3678},[3603,30814,30815],{"class":5224},"      volumesPerServer: 4\n",[3603,30817,30818],{"class":3605,"line":3684},[3603,30819,30820],{"class":5224},"      volumeClaimTemplate:\n",[3603,30822,30823],{"class":3605,"line":3690},[3603,30824,30825],{"class":5224},"        spec:\n",[3603,30827,30828],{"class":3605,"line":3696},[3603,30829,30830],{"class":5224},"          storageClassName: fast-ssd\n",[3603,30832,30833],{"class":3605,"line":3702},[3603,30834,30835],{"class":5224},"          resources:\n",[3603,30837,30838],{"class":3605,"line":3708},[3603,30839,30840],{"class":5224},"            requests:\n",[3603,30842,30843],{"class":3605,"line":4667},[3603,30844,30845],{"class":5224},"              storage: 1Ti\n",[3603,30847,30848],{"class":3605,"line":4673},[3603,30849,7945],{"class":5541},[3879,30851,30852],{},"Self-hosted MinIO у продакшні вимагає: налаштованого TLS (не HTTP!), регулярних бекапів, моніторингу через MinIO Console або Prometheus, продуманої стратегії оновлень. MinIO активно розвивається, але відповідальність за uptime — на вас.",[3405,30854],{},[3412,30856,30858],{"id":30857},"порівняння-варіантів-для-різних-сценаріїв","Порівняння варіантів для різних сценаріїв",[3902,30860,30861,30869],{},[3905,30862,30863],{},[3908,30864,30865,30867],{},[3911,30866,14158],{},[3911,30868,10232],{},[3924,30870,30871,30882,30893,30901,30910,30917,30925],{},[3908,30872,30873,30876],{},[3929,30874,30875],{},"Локальна розробка одного розробника",[3929,30877,30878,30879,6430],{},"MinIO у Docker (",[3432,30880,30881],{},"docker run",[3908,30883,30884,30887],{},[3929,30885,30886],{},"Локальна розробка команди",[3929,30888,30889,30890,30892],{},"MinIO у ",[3432,30891,27414],{}," проекту",[3908,30894,30895,30898],{},[3929,30896,30897],{},"CI\u002FCD пайплайн",[3929,30899,30900],{},"MinIO як сервіс у GitHub Actions \u002F GitLab CI",[3908,30902,30903,30906],{},[3929,30904,30905],{},"Інтеграційні тести .NET",[3929,30907,30908],{},[3432,30909,29873],{},[3908,30911,30912,30915],{},[3929,30913,30914],{},"Продакшн, стандартні вимоги",[3929,30916,26891],{},[3908,30918,30919,30922],{},[3929,30920,30921],{},"Продакшн, GDPR \u002F on-premise",[3929,30923,30924],{},"MinIO Distributed або MinIO Kubernetes Operator",[3908,30926,30927,30930],{},[3929,30928,30929],{},"Міграція з AWS до self-hosted",[3929,30931,30932],{},"MinIO + той самий AWSSDK.S3 (тільки змінити endpoint)",[3405,30934],{},[3348,30936,30938],{"id":30937},"резюме","Резюме",[3369,30940,30941,30946,30951,30957,30962,30967,30976,30982,30988,30997,31003,31009,31015],{},[3372,30942,30943,30945],{},[3356,30944,3420],{}," — глобально унікальний контейнер, прив'язаний до регіону. Назва — лише lowercase + цифри + дефіс.",[3372,30947,30948,30950],{},[3356,30949,3493],{}," = дані + метадані + ключ. Ключ — рядок, а не реальна директорія.",[3372,30952,30953,30956],{},[3356,30954,30955],{},"Storage Classes:"," Standard (активні дані), Standard-IA (рідкісний доступ), Glacier (архів, 12–48 год відновлення), Intelligent-Tiering (автоматичний вибір).",[3372,30958,30959,30961],{},[3356,30960,20898],{}," захист від видалення та перезапису. Завжди налаштовуйте Lifecycle щоб контролювати витрати.",[3372,30963,30964,30966],{},[3356,30965,20888],{}," завжди вмикайте, якщо bucket не є публічним статичним сайтом.",[3372,30968,30969,30972,30973,3453],{},[3356,30970,30971],{},"Bucket Policy:"," JSON-правила на рівні bucket. Для публічного сайту — ",[3432,30974,30975],{},"\"Principal\": \"*\", \"Action\": \"s3:GetObject\"",[3372,30977,30978,30981],{},[3356,30979,30980],{},"Encryption:"," SSE-S3 (безкоштовно) для базового захисту, SSE-KMS для аудиту та compliance.",[3372,30983,30984,30987],{},[3356,30985,30986],{},"Presigned URLs:"," тимчасовий доступ до приватних файлів. GET (скачування) та PUT (завантаження з браузера).",[3372,30989,30990,30993,30994,30996],{},[3356,30991,30992],{},"Static Website Hosting:"," для React SPA — ",[3432,30995,13122],{}," як Error Document для коректного роутингу.",[3372,30998,30999,31002],{},[3356,31000,31001],{},"CORS:"," необхідний для direct upload з браузера та cross-origin запитів.",[3372,31004,31005,31008],{},[3356,31006,31007],{},"HLS\u002FDASH:"," S3 зберігає сегменти + manifest, CloudFront роздає їх з низькою latency по всьому світу.",[3372,31010,31011,31014],{},[3356,31012,31013],{},"Transfer Acceleration:"," для великих файлів від географічно розподілених користувачів.",[3372,31016,31017,4841,31020,31022,31023,31025,31026,31029],{},[3356,31018,31019],{},".NET SDK:",[3432,31021,15769],{}," для upload\u002Fdownload, ",[3432,31024,12771],{}," для Presigned URLs, ",[3432,31027,31028],{},"ListObjectsV2"," для переліку файлів.",[3405,31031],{},[3348,31033,31035],{"id":31034},"практичні-завдання","Практичні завдання",[3412,31037,31039],{"id":31038},"рівень-1-базовий","Рівень 1 (Базовий)",[3353,31041,31042,31045],{},[3356,31043,31044],{},"Завдання 1."," Порівняйте S3 Standard та S3 Glacier Deep Archive: ціна зберігання, час доступу, мінімальний термін зберігання. Для яких даних підходить кожен клас?",[3353,31047,31048,31051,31052,31054],{},[3356,31049,31050],{},"Завдання 2."," Чому для React SPA потрібно вказати ",[3432,31053,13122],{}," як Error Document, а не стандартну сторінку 404?",[3412,31056,31058],{"id":31057},"рівень-2-практичний","Рівень 2 (Практичний)",[3353,31060,31061,31064,31065,31067,31068,31070,31071,31073],{},[3356,31062,31063],{},"Завдання 3."," Задеплойте React SPA (або просто ",[3432,31066,13122],{}," з текстом «Hello S3») на S3 Static Hosting. Налаштуйте Lifecycle Policy: зберігати максимум 3 попередніх версії. Перевірте що при прямому переході на ",[3432,31069,13102],{}," (неіснуюча сторінка) отримуєте ",[3432,31072,13122],{},", а не помилку.",[3353,31075,31076,31079,31080,31083,31084,31087,31088,31091],{},[3356,31077,31078],{},"Завдання 4."," Реалізуйте .NET endpoint ",[3432,31081,31082],{},"POST \u002Fapi\u002Fupload",", який приймає файл, генерує унікальний ключ ",[3432,31085,31086],{},"uploads\u002F{userId}\u002F{timestamp}\u002F{filename}"," та завантажує в S3. Додайте endpoint ",[3432,31089,31090],{},"GET \u002Fapi\u002Fpresigned-url?key=..."," для отримання Presigned URL на 1 годину. Перевірте через Swagger.",[3412,31093,31095],{"id":31094},"рівень-3-архітектура","Рівень 3 (Архітектура)",[3353,31097,31098,31101],{},[3356,31099,31100],{},"Завдання 5."," Спроектуйте S3-архітектуру для відео-платформи: bucket для raw відео (завантажені користувачами), bucket для конвертованих HLS-сегментів, Lifecycle Policy для кожного bucket, CORS для фронтенду на окремому домені, Bucket Policy що дозволяє MediaConvert записувати у output bucket та лише читання через CloudFront. Опишіть покрокову Flow від завантаження відео до перегляду через плеєр.",[3405,31103],{},[3348,31105,31107],{"id":31106},"реальні-юзкейси-та-вартість","Реальні юзкейси та вартість",[3353,31109,31110,31111,3453],{},"Теорія — це добре, але студенти часто запитують: «скільки це реально коштує?». Розберемо кілька реальних сценаріїв з детальними розрахунками на основі актуальних AWS цін для регіону ",[3432,31112,14192],{},[3397,31114,31115,31116,31119,31120,3453],{},"Всі розрахунки нижче — ",[3356,31117,31118],{},"приблизні"," і слугують орієнтиром. Реальна вартість залежить від патерну доступу, вибору Storage Class, регіону та обсягу даних. Точні ціни перевіряйте на ",[14216,31121,31124],{"href":31122,"rel":31123},"https:\u002F\u002Fcalculator.aws\u002F",[14220],"AWS Pricing Calculator",[3405,31126],{},[3412,31128,31130],{"id":31129},"юзкейс-1-аніме-стрімінг-платформа","Юзкейс 1: Аніме-стрімінг платформа",[3353,31132,31133],{},"Побудуємо розрахунок для платформи з повною колекцією аніме контенту та помірною аудиторією. Це показовий приклад, бо поєднує величезне сховище з інтенсивним відео-трафіком.",[3353,31135,31136],{},[3356,31137,31138],{},"Обсяг контенту (станом на 2025 рік):",[3353,31140,31141,31142,31145,31146,31149,31150,3453],{},"За даними MyAnimeList \u002F AniList у світі існує близько ",[3356,31143,31144],{},"17 000 аніме-серіалів"," із середньою кількістю ",[3356,31147,31148],{},"22 епізоди",". Плюс ~",[3356,31151,31152],{},"2 800 аніме-фільмів",[3902,31154,31155,31165],{},[3905,31156,31157],{},[3908,31158,31159,31162],{},[3911,31160,31161],{},"Категорія",[3911,31163,31164],{},"Кількість",[3924,31166,31167,31175,31183],{},[3908,31168,31169,31172],{},[3929,31170,31171],{},"Серіали",[3929,31173,31174],{},"17 000",[3908,31176,31177,31180],{},[3929,31178,31179],{},"Серій × 22 епізоди",[3929,31181,31182],{},"374 000 епізодів",[3908,31184,31185,31188],{},[3929,31186,31187],{},"Фільми",[3929,31189,31190],{},"2 800",[3353,31192,31193],{},[3356,31194,31195],{},"Розміри після HLS конвертації (на одиницю контенту):",[3353,31197,31198,31199,31201],{},"Для HLS стрімінгу кожен відеофайл конвертується у кілька якостей і нарізається на 6-секундні ",[3432,31200,14441],{}," сегменти. Загальний розмір HLS файлів приблизно рівний розміру оригінального відео у відповідній якості.",[3902,31203,31204,31217],{},[3905,31205,31206],{},[3908,31207,31208,31211,31214],{},[3911,31209,31210],{},"Якість",[3911,31212,31213],{},"Епізод (24 хв)",[3911,31215,31216],{},"Фільм (90 хв)",[3924,31218,31219,31230,31241,31252,31263],{},[3908,31220,31221,31224,31227],{},[3929,31222,31223],{},"360p (~500 kbps)",[3929,31225,31226],{},"150 MB",[3929,31228,31229],{},"560 MB",[3908,31231,31232,31235,31238],{},[3929,31233,31234],{},"480p (~1.2 Mbps)",[3929,31236,31237],{},"350 MB",[3929,31239,31240],{},"1.3 GB",[3908,31242,31243,31246,31249],{},[3929,31244,31245],{},"720p (~2.4 Mbps)",[3929,31247,31248],{},"700 MB",[3929,31250,31251],{},"2.6 GB",[3908,31253,31254,31257,31260],{},[3929,31255,31256],{},"1080p (~4.8 Mbps)",[3929,31258,31259],{},"1.4 GB",[3929,31261,31262],{},"5.25 GB",[3908,31264,31265,31270,31274],{},[3929,31266,31267],{},[3356,31268,31269],{},"Разом (всі якості)",[3929,31271,31272],{},[3356,31273,31251],{},[3929,31275,31276],{},[3356,31277,31278],{},"9.75 GB",[3353,31280,31281],{},[3356,31282,31283],{},"Загальний обсяг сховища:",[3561,31285,31288],{"className":31286,"code":31287,"language":3566},[3564],"374 000 епізодів × 2.6 GB  = 972 400 GB = 950 TB\n  2 800 фільмів × 9.75 GB  =  27 300 GB = 27 TB\n                              ─────────────────────\n                              999 700 GB ≈ 976 TB  ≈ 1 Петабайт\n",[3432,31289,31287],{"__ignoreMap":3569},[3353,31291,31292,31293,31296],{},"Майже ",[3356,31294,31295],{},"1 петабайт",". Це реальна цифра — Netflix, Crunchyroll та аналоги зберігають десятки петабайт.",[3353,31298,31299],{},[3356,31300,31301],{},"Оптимізація Storage Class:",[3353,31303,31304],{},"Не весь контент дивляться однаково активно. Розподілимо:",[3902,31306,31307,31324],{},[3905,31308,31309],{},[3908,31310,31311,31313,31315,31318,31321],{},[3911,31312,10403],{},[3911,31314,3913],{},[3911,31316,31317],{},"% від загального",[3911,31319,31320],{},"Обсяг",[3911,31322,31323],{},"Вартість\u002Fміс",[3924,31325,31326,31342,31356,31372],{},[3908,31327,31328,31331,31333,31336,31339],{},[3929,31329,31330],{},"Топ-аніме (Naruto, AoT, One Piece...)",[3929,31332,3931],{},[3929,31334,31335],{},"40%",[3929,31337,31338],{},"390 TB",[3929,31340,31341],{},"$9 197",[3908,31343,31344,31347,31349,31351,31353],{},[3929,31345,31346],{},"Середній попит, сезон минулих 2 роки",[3929,31348,3952],{},[3929,31350,31335],{},[3929,31352,31338],{},[3929,31354,31355],{},"$4 998",[3908,31357,31358,31361,31363,31366,31369],{},[3929,31359,31360],{},"Ретро, рідко переглядають",[3929,31362,4521],{},[3929,31364,31365],{},"20%",[3929,31367,31368],{},"195 TB",[3929,31370,31371],{},"$800",[3908,31373,31374,31379,31381,31383,31386],{},[3929,31375,31376],{},[3356,31377,31378],{},"РАЗОМ сховище",[3929,31380],{},[3929,31382,19316],{},[3929,31384,31385],{},"~976 TB",[3929,31387,31388],{},[3356,31389,31390],{},"$14 995\u002Fміс",[3353,31392,31393],{},[3356,31394,31395],{},"Трафік (CloudFront):",[3353,31397,31398,31399,31402],{},"Припустимо ",[3356,31400,31401],{},"100 000 MAU"," (Monthly Active Users) — це помірна, але не маленька аудиторія. Порівняно: у Crunchyroll 10+ мільйонів передплатників.",[3561,31404,31407],{"className":31405,"code":31406,"language":3566},[3564],"100 000 користувачів × 20 епізодів\u002Fміс × 700 MB (720p)\n= 1 400 000 GB = 1 367 TB трафіку на місяць\n",[3432,31408,31406],{"__ignoreMap":3569},[3353,31410,31411],{},"CloudFront тарифікується по знижуючих рівнях (чим більше — тим дешевше):",[3902,31413,31414,31425],{},[3905,31415,31416],{},[3908,31417,31418,31420,31423],{},[3911,31419,31320],{},[3911,31421,31422],{},"Ціна\u002FGB",[3911,31424,4188],{},[3924,31426,31427,31438,31449,31460,31471,31482],{},[3908,31428,31429,31432,31435],{},[3929,31430,31431],{},"Перші 10 TB",[3929,31433,31434],{},"$0.085",[3929,31436,31437],{},"$850",[3908,31439,31440,31443,31446],{},[3929,31441,31442],{},"10–50 TB",[3929,31444,31445],{},"$0.080",[3929,31447,31448],{},"$3 200",[3908,31450,31451,31454,31457],{},[3929,31452,31453],{},"50–150 TB",[3929,31455,31456],{},"$0.060",[3929,31458,31459],{},"$6 000",[3908,31461,31462,31465,31468],{},[3929,31463,31464],{},"150–500 TB",[3929,31466,31467],{},"$0.040",[3929,31469,31470],{},"$14 000",[3908,31472,31473,31476,31479],{},[3929,31474,31475],{},"500–1367 TB",[3929,31477,31478],{},"$0.030",[3929,31480,31481],{},"$26 010",[3908,31483,31484,31489,31491],{},[3929,31485,31486],{},[3356,31487,31488],{},"РАЗОМ CloudFront",[3929,31490],{},[3929,31492,31493],{},[3356,31494,31495],{},"$50 060\u002Fміс",[3353,31497,31498],{},[3356,31499,31500],{},"Підсумок аніме-платформи:",[3902,31502,31503,31512],{},[3905,31504,31505],{},[3908,31506,31507,31510],{},[3911,31508,31509],{},"Стаття витрат",[3911,31511,31323],{},[3924,31513,31514,31522,31530,31538,31550],{},[3908,31515,31516,31519],{},[3929,31517,31518],{},"S3 Storage (~976 TB, змішані класи)",[3929,31520,31521],{},"$14 995",[3908,31523,31524,31527],{},[3929,31525,31526],{},"CloudFront трафік (~1 367 TB)",[3929,31528,31529],{},"$50 060",[3908,31531,31532,31535],{},[3929,31533,31534],{},"S3 API requests (GET\u002FPUT)",[3929,31536,31537],{},"$280",[3908,31539,31540,31545],{},[3929,31541,31542],{},[3356,31543,31544],{},"РАЗОМ",[3929,31546,31547],{},[3356,31548,31549],{},"~$65 000\u002Fміс",[3908,31551,31552,31555],{},[3929,31553,31554],{},"На рік",[3929,31556,31557],{},[3356,31558,31559],{},"~$780 000\u002Fрік",[3879,31561,31562,31565,31566,31570],{},[3356,31563,31564],{},"$65 000 на місяць"," при 100 000 MAU — це ",[31567,31568,31569],"del",{},"$0.65 на користувача на місяць. Передплата $7–10\u002Fміс покриває витрати з запасом на маржу. При масштабуванні до 1 млн MAU — трафік зростає лінійно (","$500K\u002Fміс), але CloudFront дає додаткові знижки при об'ємах 1 PB+. Реальні стрімінгові сервіси також укладають приватні угоди з AWS.",[3353,31572,31573],{},[3356,31574,31575],{},"Оптимізації для зниження витрат:",[3369,31577,31578,31587,31592,31598],{},[3372,31579,31580,31583,31584,31586],{},[3356,31581,31582],{},"Популярний контент"," кешувати агресивно (CloudFront TTL 24+ годин для ",[3432,31585,14441],{}," сегментів — вони незмінні)",[3372,31588,31589,31591],{},[3356,31590,4521],{}," для аніме 2000-х — більшість переглядів з кешу CloudFront після першого запиту",[3372,31593,31594,31597],{},[3356,31595,31596],{},"Spot Instances"," для MediaConvert задач конвертації нових епізодів (знижка 60–90%)",[3372,31599,31600,31603],{},[3356,31601,31602],{},"Reserved Capacity"," CloudFront для гарантованого великого обсягу",[3405,31605],{},[3412,31607,31609],{"id":31608},"юзкейс-2-react-spa-статичний-сайт","Юзкейс 2: React SPA \u002F статичний сайт",[3353,31611,31612],{},"Найпростіший юзкейс — хостинг фронтенду.",[3353,31614,31615,31618],{},[3356,31616,31617],{},"Параметри:"," 50 000 відвідувань\u002Fмісяць, build 50 MB, середнє завантаження 2 MB на сесію.",[3902,31620,31621,31633],{},[3905,31622,31623],{},[3908,31624,31625,31628,31631],{},[3911,31626,31627],{},"Стаття",[3911,31629,31630],{},"Розрахунок",[3911,31632,31323],{},[3924,31634,31635,31648,31661,31672],{},[3908,31636,31637,31640,31643],{},[3929,31638,31639],{},"S3 storage",[3929,31641,31642],{},"50 MB × $0.023",[3929,31644,31645],{},[3356,31646,31647],{},"$0.001",[3908,31649,31650,31653,31656],{},[3929,31651,31652],{},"CloudFront трафік",[3929,31654,31655],{},"50 000 × 2 MB = 100 GB",[3929,31657,31658],{},[3356,31659,31660],{},"$8.50",[3908,31662,31663,31666,31669],{},[3929,31664,31665],{},"CloudFront requests",[3929,31667,31668],{},"50 000 × 50 req = 2.5M",[3929,31670,31671],{},"$0.008",[3908,31673,31674,31678,31680],{},[3929,31675,31676],{},[3356,31677,31544],{},[3929,31679],{},[3929,31681,31682],{},[3356,31683,31684],{},"~$9\u002Fміс",[3353,31686,31687,31688,31691],{},"Для сайту з 500 000 відвідувань\u002Fміс (1 TB трафіку): ~$85\u002Fміс. ",[3356,31689,31690],{},"Хостинг на S3+CloudFront на порядки дешевший за VPS"," для статичного контенту.",[3405,31693],{},[3412,31695,31697],{"id":31696},"юзкейс-3-інтернет-магазин-фото-товарів","Юзкейс 3: Інтернет-магазин (фото товарів)",[3353,31699,31700,31702],{},[3356,31701,31617],{}," 50 000 SKU, 6 фото на товар, 3 розміри мініатюр (thumb, medium, large), 200 000 відвідувань\u002Fміс.",[3561,31704,31707],{"className":31705,"code":31706,"language":3566},[3564],"50 000 товарів × 6 фото × 3 розміри × 500 KB = 440 GB\n",[3432,31708,31706],{"__ignoreMap":3569},[3902,31710,31711,31719],{},[3905,31712,31713],{},[3908,31714,31715,31717],{},[3911,31716,31627],{},[3911,31718,31323],{},[3924,31720,31721,31729,31737,31745],{},[3908,31722,31723,31726],{},[3929,31724,31725],{},"S3 Standard (440 GB фото)",[3929,31727,31728],{},"$10",[3908,31730,31731,31734],{},[3929,31732,31733],{},"CloudFront (200k × 15 фото × 500KB = 1.5 TB)",[3929,31735,31736],{},"$125",[3908,31738,31739,31742],{},[3929,31740,31741],{},"S3 PUT requests (нові завантаження)",[3929,31743,31744],{},"$1",[3908,31746,31747,31751],{},[3929,31748,31749],{},[3356,31750,31544],{},[3929,31752,31753],{},[3356,31754,31755],{},"~$136\u002Fміс",[3353,31757,31758,31759,31762],{},"При 2 000 000 відвідувань\u002Fміс (15 TB трафіку): ",[31567,31760,31761],{},"$900\u002Fміс. Порівняйте з CDN від Cloudflare (","$200\u002Fміс) або Fastly (~$300\u002Fміс) — AWS не завжди найдешевший для pure CDN, але виграє за інтеграцію з іншими AWS сервісами.",[3405,31764],{},[3412,31766,31768],{"id":31767},"юзкейс-4-saas-застосунок-з-файловим-сховищем","Юзкейс 4: SaaS застосунок з файловим сховищем",[3353,31770,31771,31773],{},[3356,31772,31617],{}," 5 000 активних користувачів, середньо 2 GB файлів на акаунт (документи, зображення, pdf).",[3561,31775,31778],{"className":31776,"code":31777,"language":3566},[3564],"5 000 × 2 GB = 10 000 GB = 10 TB загального сховища\n",[3432,31779,31777],{"__ignoreMap":3569},[3353,31781,31782],{},"Розподіл: 30% активні файли (відкривають щотижня), 70% архів (рідко):",[3902,31784,31785,31795],{},[3905,31786,31787],{},[3908,31788,31789,31791,31793],{},[3911,31790,3913],{},[3911,31792,31320],{},[3911,31794,31323],{},[3924,31796,31797,31808,31819,31830],{},[3908,31798,31799,31802,31805],{},[3929,31800,31801],{},"Standard (30%)",[3929,31803,31804],{},"3 TB",[3929,31806,31807],{},"$69",[3908,31809,31810,31813,31816],{},[3929,31811,31812],{},"Standard-IA (70%)",[3929,31814,31815],{},"7 TB",[3929,31817,31818],{},"$87",[3908,31820,31821,31824,31827],{},[3929,31822,31823],{},"PUT\u002FGET\u002FLIST requests",[3929,31825,31826],{},"—",[3929,31828,31829],{},"$15",[3908,31831,31832,31836,31838],{},[3929,31833,31834],{},[3356,31835,31544],{},[3929,31837],{},[3929,31839,31840],{},[3356,31841,31842],{},"~$171\u002Fміс",[3353,31844,31845],{},"При зростанні до 50 000 користувачів: ~$1 700\u002Fміс. Lifecycle Policy автоматично переміщує файли що не відкривались 90 днів у Standard-IA.",[3405,31847],{},[3412,31849,31851],{"id":31850},"юзкейс-5-резервне-копіювання-баз-даних","Юзкейс 5: Резервне копіювання баз даних",[3353,31853,31854,31856],{},[3356,31855,31617],{}," production PostgreSQL 50 GB, щоденний повний backup, зберігання 30 днів.",[3561,31858,31861],{"className":31859,"code":31860,"language":3566},[3564],"50 GB × 30 = 1 500 GB у Glacier Flexible Retrieval\n",[3432,31862,31860],{"__ignoreMap":3569},[3902,31864,31865,31873],{},[3905,31866,31867],{},[3908,31868,31869,31871],{},[3911,31870,31627],{},[3911,31872,31323],{},[3924,31874,31875,31883,31891],{},[3908,31876,31877,31880],{},[3929,31878,31879],{},"Glacier Flexible (1 500 GB × $0.0036)",[3929,31881,31882],{},"$5.40",[3908,31884,31885,31888],{},[3929,31886,31887],{},"PUT requests (30 uploads × $0.05\u002F1000)",[3929,31889,31890],{},"$0.00015",[3908,31892,31893,31897],{},[3929,31894,31895],{},[3356,31896,31544],{},[3929,31898,31899],{},[3356,31900,31901],{},"~$5.40\u002Fміс",[3353,31903,31904,31907],{},[3356,31905,31906],{},"$5.40 на місяць"," для надійного backup 50 GB БД — це практично безкоштовно. Навіть якщо база зростає до 500 GB: ~$54\u002Fміс. Порівняйте з вартістю втрати даних.",[3353,31909,31910,31911,31914],{},"Lifecycle Policy: Standard (7 днів) → Glacier Flexible (30–90 днів) → видалення. Щоб відновити — ",[3432,31912,31913],{},"aws s3 restore-object"," ініціює відновлення за 3–12 годин.",[3405,31916],{},[3412,31918,31920],{"id":31919},"юзкейс-6-фотосток-медіа-галерея","Юзкейс 6: Фотосток \u002F медіа-галерея",[3353,31922,31923,31925],{},[3356,31924,31617],{}," 2 000 000 фотографій (raw 8 MB + 3 thumbnail розміри), 500 000 відвідувань\u002Fміс.",[3561,31927,31930],{"className":31928,"code":31929,"language":3566},[3564],"2 000 000 × (8 MB raw + 1.5 MB thumbs) = 2 000 000 × 9.5 MB = 19 000 GB ≈ 18.5 TB\n",[3432,31931,31929],{"__ignoreMap":3569},[3902,31933,31934,31944],{},[3905,31935,31936],{},[3908,31937,31938,31940,31942],{},[3911,31939,31627],{},[3911,31941,3913],{},[3911,31943,31323],{},[3924,31945,31946,31956,31966,31976],{},[3908,31947,31948,31951,31953],{},[3929,31949,31950],{},"Raw фото (рідко потрібні після завантаження)",[3929,31952,4502],{},[3929,31954,31955],{},"$290",[3908,31957,31958,31961,31963],{},[3929,31959,31960],{},"Thumbnails (активно дивляться)",[3929,31962,3931],{},[3929,31964,31965],{},"$110",[3908,31967,31968,31971,31973],{},[3929,31969,31970],{},"CloudFront (500k × 20 img × 500KB = 5TB)",[3929,31972,31826],{},[3929,31974,31975],{},"$425",[3908,31977,31978,31982,31984],{},[3929,31979,31980],{},[3356,31981,31544],{},[3929,31983],{},[3929,31985,31986],{},[3356,31987,31988],{},"~$825\u002Fміс",[3405,31990],{},[3412,31992,31994],{"id":31993},"порівняльна-таблиця-юзкейсів","Порівняльна таблиця юзкейсів",[3902,31996,31997,32020],{},[3905,31998,31999],{},[3908,32000,32001,32004,32007,32010,32013,32016],{},[3911,32002,32003],{},"Юзкейс",[3911,32005,32006],{},"Сховище",[3911,32008,32009],{},"Трафік\u002Fміс",[3911,32011,32012],{},"Вартість S3",[3911,32014,32015],{},"Вартість CF",[3911,32017,32018],{},[3356,32019,31544],{},[3924,32021,32022,32044,32064,32086,32107,32127],{},[3908,32023,32024,32027,32030,32033,32036,32039],{},[3929,32025,32026],{},"React SPA (50k MAU)",[3929,32028,32029],{},"50 MB",[3929,32031,32032],{},"100 GB",[3929,32034,32035],{},"~$0",[3929,32037,32038],{},"$9",[3929,32040,32041],{},[3356,32042,32043],{},"~$9",[3908,32045,32046,32049,32052,32055,32057,32059],{},[3929,32047,32048],{},"Інтернет-магазин",[3929,32050,32051],{},"440 GB",[3929,32053,32054],{},"1.5 TB",[3929,32056,31728],{},[3929,32058,31736],{},[3929,32060,32061],{},[3356,32062,32063],{},"~$135",[3908,32065,32066,32069,32072,32075,32078,32081],{},[3929,32067,32068],{},"SaaS файли (5k users)",[3929,32070,32071],{},"10 TB",[3929,32073,32074],{},"2 TB",[3929,32076,32077],{},"$156",[3929,32079,32080],{},"$170",[3929,32082,32083],{},[3356,32084,32085],{},"~$326",[3908,32087,32088,32091,32094,32097,32100,32102],{},[3929,32089,32090],{},"Backup БД (50 GB)",[3929,32092,32093],{},"1.5 TB (Glacier)",[3929,32095,32096],{},"мінімум",[3929,32098,32099],{},"$5",[3929,32101,31826],{},[3929,32103,32104],{},[3356,32105,32106],{},"~$5",[3908,32108,32109,32112,32115,32117,32120,32122],{},[3929,32110,32111],{},"Фотосток (2M фото)",[3929,32113,32114],{},"18.5 TB",[3929,32116,3452],{},[3929,32118,32119],{},"$400",[3929,32121,31975],{},[3929,32123,32124],{},[3356,32125,32126],{},"~$825",[3908,32128,32129,32134,32139,32144,32149,32154],{},[3929,32130,32131],{},[3356,32132,32133],{},"Аніме стрімінг (100k MAU)",[3929,32135,32136],{},[3356,32137,32138],{},"~1 PB",[3929,32140,32141],{},[3356,32142,32143],{},"~1.4 PB",[3929,32145,32146],{},[3356,32147,32148],{},"$15 000",[3929,32150,32151],{},[3356,32152,32153],{},"$50 000",[3929,32155,32156],{},[3356,32157,32158],{},"~$65 000",[3353,32160,32161,32164,32165,32168],{},[3356,32162,32163],{},"Головний висновок:"," S3 — надзвичайно вигідний для статичного контенту та рідкісного доступу (React SPA, backup). Основна вартість у медіа-сервісах — ",[3356,32166,32167],{},"CDN трафік",", а не само сховище. При проєктуванні системи зі стрімінгом відео — кешування на CloudFront є не опцією, а обов'язковою вимогою для контролю витрат.",[32170,32171,32172],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .spJ8K, html code.shiki .spJ8K{--shiki-light:#008000;--shiki-default:#6A9955;--shiki-dark:#6A9955}html pre.shiki code .s8Opu, html code.shiki .s8Opu{--shiki-light:#795E26;--shiki-default:#DCDCAA;--shiki-dark:#DCDCAA}html pre.shiki code .sbdoH, html code.shiki .sbdoH{--shiki-light:#A31515;--shiki-default:#CE9178;--shiki-dark:#CE9178}html pre.shiki code .sjcCO, html code.shiki .sjcCO{--shiki-light:#EE0000;--shiki-default:#D7BA7D;--shiki-dark:#D7BA7D}html pre.shiki code .su1O8, html code.shiki .su1O8{--shiki-light:#0000FF;--shiki-default:#569CD6;--shiki-dark:#569CD6}html pre.shiki code .s8xlr, html code.shiki .s8xlr{--shiki-light:#AF00DB;--shiki-default:#C586C0;--shiki-dark:#C586C0}html pre.shiki code .sN1BT, html code.shiki .sN1BT{--shiki-light:#267F99;--shiki-default:#4EC9B0;--shiki-dark:#4EC9B0}html pre.shiki code .sHH4Y, html code.shiki .sHH4Y{--shiki-light:#000000;--shiki-default:#D4D4D4;--shiki-dark:#D4D4D4}html pre.shiki code .siwwj, html code.shiki .siwwj{--shiki-light:#001080;--shiki-default:#9CDCFE;--shiki-dark:#9CDCFE}html pre.shiki code .sD7JJ, html code.shiki .sD7JJ{--shiki-light:#000000FF;--shiki-default:#D4D4D4;--shiki-dark:#D4D4D4}html pre.shiki code .sLwNe, html code.shiki .sLwNe{--shiki-light:#0451A5;--shiki-default:#9CDCFE;--shiki-dark:#9CDCFE}html pre.shiki code .sJj4R, html code.shiki .sJj4R{--shiki-light:#098658;--shiki-default:#B5CEA8;--shiki-dark:#B5CEA8}html pre.shiki code .s-QsJ, html code.shiki .s-QsJ{--shiki-light:#0070C1;--shiki-default:#4FC1FF;--shiki-dark:#4FC1FF}html pre.shiki code .s0P7L, html code.shiki .s0P7L{--shiki-light:#800000;--shiki-default:#808080;--shiki-dark:#808080}html pre.shiki code .sKtos, html code.shiki .sKtos{--shiki-light:#800000;--shiki-default:#569CD6;--shiki-dark:#569CD6}html pre.shiki code .sa4r_, html code.shiki .sa4r_{--shiki-light:#E50000;--shiki-default:#9CDCFE;--shiki-dark:#9CDCFE}html pre.shiki code .sqdDX, html code.shiki .sqdDX{--shiki-light:#800000;--shiki-default:#D7BA7D;--shiki-dark:#D7BA7D}html pre.shiki code .sDUd3, html code.shiki .sDUd3{--shiki-light:#0451A5;--shiki-default:#CE9178;--shiki-dark:#CE9178}html pre.shiki code .su9tN, html code.shiki .su9tN{--shiki-light:#0000FF;--shiki-default:#CE9178;--shiki-dark:#CE9178}",{"title":3569,"searchDepth":3612,"depth":3612,"links":32174},[32175,32176,32181,32191,32192,32193,32198,32199,32200,32201,32202,32211,32215,32227,32237,32255,32256,32261],{"id":3350,"depth":3612,"text":3351},{"id":3409,"depth":3612,"text":3410,"children":32177},[32178,32179,32180],{"id":3414,"depth":3618,"text":3415},{"id":3487,"depth":3618,"text":3488},{"id":3552,"depth":3618,"text":3553},{"id":3716,"depth":3612,"text":3717,"children":32182},[32183,32184,32185,32186,32187,32188,32189,32190],{"id":3726,"depth":3618,"text":3727},{"id":3826,"depth":3618,"text":3827},{"id":3983,"depth":3618,"text":3984},{"id":4095,"depth":3618,"text":4096},{"id":4229,"depth":3618,"text":4230},{"id":4409,"depth":3618,"text":4410},{"id":4795,"depth":3618,"text":4796},{"id":5194,"depth":3618,"text":5195},{"id":6079,"depth":3612,"text":6080},{"id":7171,"depth":3612,"text":7172},{"id":8211,"depth":3612,"text":8212,"children":32194},[32195,32196,32197],{"id":8537,"depth":3618,"text":8538},{"id":8947,"depth":3618,"text":8948},{"id":10041,"depth":3618,"text":10042},{"id":11988,"depth":3612,"text":11989},{"id":13061,"depth":3612,"text":13062},{"id":13459,"depth":3612,"text":13460},{"id":14023,"depth":3612,"text":14024},{"id":14424,"depth":3612,"text":14425,"children":32203},[32204,32205,32206,32207,32208,32209,32210],{"id":14431,"depth":3618,"text":14432},{"id":14475,"depth":3618,"text":14476},{"id":14670,"depth":3618,"text":14671},{"id":14744,"depth":3618,"text":14745},{"id":14771,"depth":3618,"text":14772},{"id":14965,"depth":3618,"text":14966},{"id":15435,"depth":3618,"text":15436},{"id":15513,"depth":3612,"text":15514,"children":32212},[32213,32214],{"id":15517,"depth":3618,"text":15518},{"id":15724,"depth":3618,"text":15725},{"id":17853,"depth":3612,"text":17854,"children":32216},[32217,32218,32219,32220,32221,32222,32223,32224,32225,32226],{"id":17862,"depth":3618,"text":17863},{"id":20719,"depth":3618,"text":20720},{"id":20840,"depth":3618,"text":20841},{"id":21038,"depth":3618,"text":21039},{"id":21171,"depth":3618,"text":21172},{"id":21294,"depth":3618,"text":21295},{"id":21424,"depth":3618,"text":21425},{"id":21680,"depth":3618,"text":21681},{"id":21850,"depth":3618,"text":21851},{"id":21985,"depth":3618,"text":21986},{"id":22134,"depth":3612,"text":22135,"children":32228},[32229,32230,32231,32232,32233,32234,32235,32236],{"id":22164,"depth":3618,"text":22165},{"id":22426,"depth":3618,"text":22427},{"id":22536,"depth":3618,"text":22537},{"id":22903,"depth":3618,"text":22904},{"id":23453,"depth":3618,"text":23454},{"id":25513,"depth":3618,"text":25514},{"id":26246,"depth":3618,"text":26247},{"id":26696,"depth":3618,"text":26697},{"id":26778,"depth":3612,"text":26779,"children":32238},[32239,32240,32241,32242,32243,32244,32245,32246,32247,32249,32251,32252,32253,32254],{"id":26878,"depth":3618,"text":26879},{"id":27056,"depth":3618,"text":27057},{"id":27224,"depth":3618,"text":27225},{"id":28205,"depth":3618,"text":28206},{"id":28274,"depth":3618,"text":28275},{"id":29300,"depth":3618,"text":29301},{"id":29879,"depth":3618,"text":29880},{"id":30320,"depth":3618,"text":30321},{"id":30330,"depth":3618,"text":32248},"Визначте конфігурацію у appsettings.json",{"id":30393,"depth":3618,"text":32250},"Зареєструйте IAmazonS3 залежно від провайдера",{"id":30570,"depth":3618,"text":30571},{"id":30626,"depth":3618,"text":30627},{"id":30674,"depth":3618,"text":30675},{"id":30857,"depth":3618,"text":30858},{"id":30937,"depth":3612,"text":30938},{"id":31034,"depth":3612,"text":31035,"children":32257},[32258,32259,32260],{"id":31038,"depth":3618,"text":31039},{"id":31057,"depth":3618,"text":31058},{"id":31094,"depth":3618,"text":31095},{"id":31106,"depth":3612,"text":31107,"children":32262},[32263,32264,32265,32266,32267,32268,32269],{"id":31129,"depth":3618,"text":31130},{"id":31608,"depth":3618,"text":31609},{"id":31696,"depth":3618,"text":31697},{"id":31767,"depth":3618,"text":31768},{"id":31850,"depth":3618,"text":31851},{"id":31919,"depth":3618,"text":31920},{"id":31993,"depth":3618,"text":31994},"Повний посібник з Amazon S3 для .NET і React розробників. Buckets, Storage Classes, Versioning, Lifecycle Policies, безпека, статичний хостинг, CORS, Presigned URLs, SDK for .NET та медіа-стрімінг HLS\u002FDASH.","md",null,{},{"title":3246,"description":32270},"5Op_erjYLOneeymLyB1GgLc_uxRV_CLmfANPQX2an50",[32277,32279],{"title":3242,"path":3243,"stem":3244,"description":32278,"children":-1},"Глибоке дослідження Application Load Balancer, Network Load Balancer, Target Groups та Auto Scaling Groups для .NET застосунків — від фундаментальних принципів балансування навантаження до повного налаштування HTTPS, health checks та стратегій автоматичного масштабування.",{"title":3250,"path":3251,"stem":3252,"description":32280,"children":-1},"Детальне пояснення що таке CDN і навіщо він потрібен. CloudFront Distributions, Origins, Edge Locations, Cache Behaviors, OAC, CloudFront Functions, Invalidations. Повна лабораторна робота з React SPA на S3 + CloudFront + HTTPS з підключенням безкоштовного домену pp.ua.",1782371305155]