From 4d0d09e89169381c7426c3baa59b55d27417c7c1 Mon Sep 17 00:00:00 2001 From: Silas Della Contrada Date: Fri, 25 Jul 2025 23:21:40 +0200 Subject: [PATCH 1/3] feat: start working on a state-machine for pulls --- pkg/client/pull/state/state.go | 52 ++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 pkg/client/pull/state/state.go diff --git a/pkg/client/pull/state/state.go b/pkg/client/pull/state/state.go new file mode 100644 index 0000000..b8ced94 --- /dev/null +++ b/pkg/client/pull/state/state.go @@ -0,0 +1,52 @@ +package state + +import ( + "github.com/distribution/reference" + "github.com/opencontainers/go-digest" +) + +type Pull interface { + Ref() reference.Named + Digest() *digest.Digest // can be nil when the pull is not yet complete + Layers() []Layer + Layer(id string) Layer +} + +type Layer interface { + Id() string + Status() string +} + +type pullBase struct { + ref reference.Named + layers []Layer +} + +func (p *pullBase) Ref() reference.Named { + return p.ref +} + +func (p *pullBase) Digest() *digest.Digest { + return nil +} + +func (p *pullBase) Layers() []Layer { + return p.layers +} + +func (p *pullBase) Layer(id string) Layer { + for _, l := range p.layers { + if l.Id() == id { + return l + } + } + return nil +} + +type layerBase struct { + id string +} + +func (l *layerBase) Id() string { + return l.id +} From 609808788d3dd9c881dfbc9590e9393611f80dc8 Mon Sep 17 00:00:00 2001 From: Silas Della Contrada Date: Sat, 26 Jul 2025 17:21:06 +0200 Subject: [PATCH 2/3] feat: implement pull-state for properly keeping track of layer progress and pull events --- examples/pull/main.go | 20 ++- pkg/client/pull.go | 32 ++++- pkg/client/pull/events/error.go | 14 +++ pkg/client/pull/events/event.go | 7 ++ pkg/client/pull/events/final.go | 4 +- pkg/client/pull/state.go | 43 +++++++ pkg/client/pull/state/layer.go | 216 ++++++++++++++++++++++++++++++++ pkg/client/pull/state/pull.go | 122 ++++++++++++++++++ pkg/client/pull/state/state.go | 10 +- 9 files changed, 455 insertions(+), 13 deletions(-) create mode 100644 pkg/client/pull/events/error.go create mode 100644 pkg/client/pull/state.go create mode 100644 pkg/client/pull/state/layer.go create mode 100644 pkg/client/pull/state/pull.go diff --git a/examples/pull/main.go b/examples/pull/main.go index e586f82..a0f5bfa 100644 --- a/examples/pull/main.go +++ b/examples/pull/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "fmt" "github.com/distribution/reference" client2 "github.com/docker/docker/client" "github.com/silenium-dev/docker-wrapper/pkg/client" @@ -17,21 +18,30 @@ func main() { cli, err := client.NewWithOpts( client.WithAuthProvider(authProvider), client.FromEnv, - client.WithDockerOpts(client2.WithTimeout(time.Second*10)), + client.WithDockerOpts(client2.WithTimeout(time.Hour*1)), ) if err != nil { panic(err) } //ref, err := reference.ParseDockerRef("quay.io/prometheus/node-exporter@sha256:a25fbdaa3e4d03e0d735fd03f231e9a48332ecf40ca209b2f103b1f970d1cde0") - ref, err := reference.ParseDockerRef("confluentinc/cp-kafka:latest") + ref, err := reference.ParseDockerRef("localstack/localstack:latest") if err != nil { panic(err) } - eventChan, err := cli.Pull(context.Background(), ref) + stateChan, err := cli.PullWithState(context.Background(), ref) if err != nil { panic(err) } - for event := range eventChan { - println(event.String()) + for state := range stateChan { + print("\033[2J") + fmt.Printf("%s\n", state.Status()) + for idx, l := range state.Layers() { + fmt.Printf("%02d [%s]: %s\n", idx, l.Id(), l.Status()) + } } + digest, err := cli.Pull(context.Background(), ref) + if err != nil { + panic(err) + } + fmt.Printf("Digest: %s\n", digest) } diff --git a/pkg/client/pull.go b/pkg/client/pull.go index c704fa9..eb3b798 100644 --- a/pkg/client/pull.go +++ b/pkg/client/pull.go @@ -2,14 +2,25 @@ package client import ( "context" + "fmt" "github.com/distribution/reference" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/registry" + "github.com/opencontainers/go-digest" "github.com/silenium-dev/docker-wrapper/pkg/client/pull" "github.com/silenium-dev/docker-wrapper/pkg/client/pull/events" + "github.com/silenium-dev/docker-wrapper/pkg/client/pull/state" ) -func (c *Client) Pull(ctx context.Context, ref reference.Named) (chan events.PullEvent, error) { +func (c *Client) PullWithState(ctx context.Context, ref reference.Named) (chan state.Pull, error) { + eventChan, err := c.PullWithEvents(ctx, ref) + if err != nil { + return nil, err + } + return pull.StateFromStream(ctx, ref, eventChan), nil +} + +func (c *Client) PullWithEvents(ctx context.Context, ref reference.Named) (chan events.PullEvent, error) { var encodedAuth string var err error if c.authProvider != nil { @@ -24,3 +35,22 @@ func (c *Client) Pull(ctx context.Context, ref reference.Named) (chan events.Pul } return pull.ParseStream(ctx, reader), nil } + +func (c *Client) Pull(ctx context.Context, ref reference.Named) (digest.Digest, error) { + eventChan, err := c.PullWithEvents(ctx, ref) + if err != nil { + return "", err + } + + var digestEvent *events.Digest + for event := range eventChan { + if _, ok := event.(*events.Digest); digestEvent == nil && ok { + digestEvent = event.(*events.Digest) + } + } + if digestEvent == nil { + return "", fmt.Errorf("no digest event received") + } + + return digestEvent.Digest, nil +} diff --git a/pkg/client/pull/events/error.go b/pkg/client/pull/events/error.go new file mode 100644 index 0000000..d624346 --- /dev/null +++ b/pkg/client/pull/events/error.go @@ -0,0 +1,14 @@ +package events + +type LayerError struct { + LayerBase + PullError +} + +type PullError struct { + Error string +} + +func (e *PullError) String() string { + return e.Error +} diff --git a/pkg/client/pull/events/event.go b/pkg/client/pull/events/event.go index 03fb695..1c47a0d 100644 --- a/pkg/client/pull/events/event.go +++ b/pkg/client/pull/events/event.go @@ -13,6 +13,7 @@ type PullEvent interface { } type LayerEvent interface { + PullEvent LayerId() string } @@ -47,6 +48,7 @@ func Parse(event base.PullProgressEvent) (PullEvent, error) { Hide: event.ProgressDetail.HideCounts, } progressBase := ProgressBase{layer, progress} + errorEvent := PullError{Error: event.Error} switch event.Status { case AlreadyExistsStatus: @@ -76,6 +78,11 @@ func Parse(event base.PullProgressEvent) (PullEvent, error) { case PullCompleteStatus: return &PullComplete{layer}, nil default: + if event.Error != "" && event.ID != "" { + return &LayerError{layer, errorEvent}, nil + } else if event.Error != "" { + return &errorEvent, nil + } if strings.HasPrefix(event.Status, "Pulling from") { return &PullStarted{}, nil } diff --git a/pkg/client/pull/events/final.go b/pkg/client/pull/events/final.go index 05db8fb..fcdf138 100644 --- a/pkg/client/pull/events/final.go +++ b/pkg/client/pull/events/final.go @@ -1,6 +1,8 @@ package events -import "fmt" +import ( + "fmt" +) type DownloadedNewerImage struct { Final diff --git a/pkg/client/pull/state.go b/pkg/client/pull/state.go new file mode 100644 index 0000000..08be553 --- /dev/null +++ b/pkg/client/pull/state.go @@ -0,0 +1,43 @@ +package pull + +import ( + "context" + "github.com/distribution/reference" + "github.com/silenium-dev/docker-wrapper/pkg/client/pull/events" + "github.com/silenium-dev/docker-wrapper/pkg/client/pull/state" +) + +func StateFromStream(ctx context.Context, ref reference.Named, ch chan events.PullEvent) chan state.Pull { + out := make(chan state.Pull) + + go processEvents(ctx, ref, ch, out) + + return out +} + +func processEvents(ctx context.Context, ref reference.Named, ch chan events.PullEvent, out chan state.Pull) { + defer close(out) + var current state.Pull + var err error + for { + select { + case <-ctx.Done(): + return + case event, ok := <-ch: + if !ok { + return + } + var next state.Pull + if current == nil { + next, err = state.NewPullState(ref, event) + } else { + next, err = current.Next(event) + } + if err != nil { + panic(err) + } + current = next + out <- current + } + } +} diff --git a/pkg/client/pull/state/layer.go b/pkg/client/pull/state/layer.go new file mode 100644 index 0000000..359101f --- /dev/null +++ b/pkg/client/pull/state/layer.go @@ -0,0 +1,216 @@ +package state + +import ( + "fmt" + "github.com/silenium-dev/docker-wrapper/pkg/client/pull/events" + "time" +) + +func NewLayer(event events.LayerEvent) (Layer, error) { + switch event := event.(type) { + case *events.PullingFSLayer: + return &LayerPullingFSLayer{layerBase{event.LayerId()}}, nil + case *events.AlreadyExists: + return &LayerAlreadyExists{layerBase{event.LayerId()}}, nil + case *events.LayerError: + return &LayerErrored{layerBase{event.LayerId()}, event.Error}, nil + } + return nil, fmt.Errorf("invalid initial event (%T)", event) +} + +type LayerErrored struct { + layerBase + error string +} + +func (l *LayerErrored) Status() string { + return fmt.Sprintf("Error: %s", l.error) +} + +func (l *LayerErrored) Next(events.LayerEvent) (Layer, error) { + return nil, fmt.Errorf("layer errored: %s", l.error) +} + +type LayerPullingFSLayer struct { + layerBase +} + +func (l *LayerPullingFSLayer) Status() string { + return "Pulling fs layer" +} + +func (l *LayerPullingFSLayer) Next(event events.LayerEvent) (Layer, error) { + switch event := event.(type) { + case *events.Waiting: + return &LayerWaiting{l.layerBase}, nil + case *events.Downloading: + return &LayerDownloading{l.layerBase, event.Progress()}, nil + case *events.AlreadyExists: + return &LayerAlreadyExists{l.layerBase}, nil + case *events.LayerError: + return &LayerErrored{layerBase{event.LayerId()}, event.Error}, nil + } + return nil, fmt.Errorf("invalid transition (pulling-fs-layer + %T)", event) +} + +type LayerWaiting struct { + layerBase +} + +func (l *LayerWaiting) Status() string { + return "Waiting" +} + +func (l *LayerWaiting) Next(event events.LayerEvent) (Layer, error) { + switch event := event.(type) { + case *events.AlreadyExists: + return &LayerAlreadyExists{l.layerBase}, nil + case *events.Downloading: + return &LayerDownloading{l.layerBase, event.Progress()}, nil + case *events.DownloadComplete: + return &LayerDownloadComplete{l.layerBase}, nil + case *events.LayerError: + return &LayerErrored{layerBase{event.LayerId()}, event.Error}, nil + } + return nil, fmt.Errorf("invalid transition (waiting + %T)", event) +} + +type LayerDownloading struct { + layerBase + progress events.Progress +} + +func (l *LayerDownloading) Status() string { + return fmt.Sprintf("Downloading (%s)", l.progress.String()) +} + +func (l *LayerDownloading) Progress() events.Progress { + return l.progress +} + +func (l *LayerDownloading) Next(event events.LayerEvent) (Layer, error) { + switch event := event.(type) { + case *events.Downloading: + return &LayerDownloading{l.layerBase, event.Progress()}, nil + case *events.DownloadComplete: + return &LayerDownloadComplete{l.layerBase}, nil + case *events.VerifyingChecksum: + return &LayerVerifyingChecksum{l.layerBase}, nil + case *events.Extracting: + return parseLayerExtracting(l.layerBase, event), nil + case *events.LayerError: + return &LayerErrored{layerBase{event.LayerId()}, event.Error}, nil + } + return nil, fmt.Errorf("invalid transition (downloading + %T)", event) +} + +type LayerVerifyingChecksum struct { + layerBase +} + +func (l *LayerVerifyingChecksum) Status() string { + return "Verifying Checksum" +} + +func (l *LayerVerifyingChecksum) Next(event events.LayerEvent) (Layer, error) { + switch event := event.(type) { + case *events.DownloadComplete: + return &LayerDownloadComplete{l.layerBase}, nil + case *events.Extracting: + return parseLayerExtracting(l.layerBase, event), nil + case *events.LayerError: + return &LayerErrored{layerBase{event.LayerId()}, event.Error}, nil + } + return nil, fmt.Errorf("invalid transition (verifying-checksum + %T)", event) +} + +type LayerDownloadComplete struct { + layerBase +} + +func (l *LayerDownloadComplete) Status() string { + return "Download complete" +} + +func (l *LayerDownloadComplete) Next(event events.LayerEvent) (Layer, error) { + switch event := event.(type) { + case *events.Extracting: + return parseLayerExtracting(l.layerBase, event), nil + case *events.LayerError: + return &LayerErrored{layerBase{event.LayerId()}, event.Error}, nil + } + return nil, fmt.Errorf("invalid transition (download-complete + %T)", event) +} + +type LayerExtracting struct { + layerBase + duration *time.Duration + progress *events.Progress +} + +func (l *LayerExtracting) Status() string { + switch { + case l.duration != nil: + return fmt.Sprintf("Extracting (%s)", l.duration.String()) + case l.progress != nil: + return fmt.Sprintf("Extracting (%s)", l.progress.String()) + default: + return "Extracting" + } +} + +func (l *LayerExtracting) Progress() *events.Progress { + return l.progress +} + +func (l *LayerExtracting) Duration() *time.Duration { + return l.duration +} + +func (l *LayerExtracting) Next(event events.LayerEvent) (Layer, error) { + switch event := event.(type) { + case *events.Extracting: + return parseLayerExtracting(l.layerBase, event), nil + case *events.PullComplete: + return &LayerPullComplete{l.layerBase}, nil + case *events.LayerError: + return &LayerErrored{layerBase{event.LayerId()}, event.Error}, nil + } + return nil, fmt.Errorf("invalid transition (extracting + %T)", event) +} + +func parseLayerExtracting(base layerBase, event *events.Extracting) Layer { + progress := event.Progress() + duration := event.Duration + if progress.Total == 0 { + return &LayerExtracting{base, &duration, nil} + } else { + return &LayerExtracting{base, nil, &progress} + } +} + +// Final states + +type LayerAlreadyExists struct { + layerBase +} + +func (l *LayerAlreadyExists) Status() string { + return "Already exists" +} + +func (l *LayerAlreadyExists) Next(events.LayerEvent) (Layer, error) { + return nil, fmt.Errorf("already completed") +} + +type LayerPullComplete struct { + layerBase +} + +func (l *LayerPullComplete) Status() string { + return "Pull complete" +} + +func (l *LayerPullComplete) Next(events.LayerEvent) (Layer, error) { + return nil, fmt.Errorf("already completed") +} diff --git a/pkg/client/pull/state/pull.go b/pkg/client/pull/state/pull.go new file mode 100644 index 0000000..20325a5 --- /dev/null +++ b/pkg/client/pull/state/pull.go @@ -0,0 +1,122 @@ +package state + +import ( + "fmt" + "github.com/distribution/reference" + "github.com/opencontainers/go-digest" + "github.com/silenium-dev/docker-wrapper/pkg/client/pull/events" +) + +type PullInProgress struct { + pullBase + digest *digest.Digest +} + +func (p *PullInProgress) Status() string { + if p.digest == nil { + return "Pulling" + } + return "Finishing" +} + +func NewPullState(ref reference.Named, event events.PullEvent) (Pull, error) { + switch event.(type) { + case *events.PullStarted: + return &PullInProgress{ + pullBase: pullBase{ + ref: ref, + }, + }, nil + } + return nil, fmt.Errorf("invalid initial event (%T)", event) +} + +func (p *PullInProgress) Next(event events.PullEvent) (Pull, error) { + layers := p.layers + if le, ok := event.(events.LayerEvent); ok { + layers = make([]Layer, len(p.layers)) + copy(layers, p.layers) + found := false + for i, l := range p.layers { + if l.Id() == le.LayerId() { + found = true + newL, err := l.Next(le) + if err != nil { + return nil, err + } + layers[i] = newL + break + } + } + if !found { + layer, err := NewLayer(le) + if err != nil { + return nil, err + } + layers = append(layers, layer) + } + } + + var result Pull + switch event := event.(type) { + case events.LayerEvent: + result = &PullInProgress{ + pullBase: pullBase{ + ref: p.ref, + layers: layers, + }, + digest: p.digest, + } + case *events.PullStarted: + result = &PullInProgress{ + pullBase: p.pullBase, + digest: p.digest, + } + case *events.Digest: + result = &PullInProgress{ + p.pullBase, + &event.Digest, + } + case *events.PullError: + result = &PullErrored{ + pullBase: p.pullBase, + error: event.Error, + } + case events.FinalEvent: + if p.digest == nil { + return nil, fmt.Errorf("cannot complete pull: no digest event received") + } + result = &PullComplete{ + pullBase: p.pullBase, + digest: *p.digest, + } + } + + return result, nil +} + +type PullErrored struct { + pullBase + error string +} + +func (p *PullErrored) Status() string { + return fmt.Sprintf("Error: %s", p.error) +} + +func (p *PullErrored) Next(events.PullEvent) (Pull, error) { + return nil, fmt.Errorf("pull errored: %s", p.error) +} + +type PullComplete struct { + pullBase + digest digest.Digest +} + +func (p *PullComplete) Status() string { + return fmt.Sprintf("Complete (Digest: %s)", p.digest.String()) +} + +func (p *PullComplete) Next(event events.PullEvent) (Pull, error) { + return nil, fmt.Errorf("pull already complete (event: %T)", event) +} diff --git a/pkg/client/pull/state/state.go b/pkg/client/pull/state/state.go index b8ced94..b9cfd2e 100644 --- a/pkg/client/pull/state/state.go +++ b/pkg/client/pull/state/state.go @@ -2,19 +2,21 @@ package state import ( "github.com/distribution/reference" - "github.com/opencontainers/go-digest" + "github.com/silenium-dev/docker-wrapper/pkg/client/pull/events" ) type Pull interface { Ref() reference.Named - Digest() *digest.Digest // can be nil when the pull is not yet complete Layers() []Layer Layer(id string) Layer + Next(event events.PullEvent) (Pull, error) + Status() string } type Layer interface { Id() string Status() string + Next(event events.LayerEvent) (Layer, error) } type pullBase struct { @@ -26,10 +28,6 @@ func (p *pullBase) Ref() reference.Named { return p.ref } -func (p *pullBase) Digest() *digest.Digest { - return nil -} - func (p *pullBase) Layers() []Layer { return p.layers } From 05073b8e289eeb41753f7392dcfcd20b4f565810 Mon Sep 17 00:00:00 2001 From: Silas Date: Sat, 26 Jul 2025 17:24:27 +0200 Subject: [PATCH 3/3] docs: add license --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4d160d7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 silenium-dev + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License.