diff --git a/forgejo/Makefile b/forgejo/Makefile new file mode 100644 index 0000000..6288a9c --- /dev/null +++ b/forgejo/Makefile @@ -0,0 +1,16 @@ +GO=go +EXT= + +all: build + +build: + ${GO} build -v -o forgejoImporter${EXT} ./plugin/importer + ${GO} build -v -o forgejoExporter${EXT} ./plugin/exporter + +test: + ${GO} test ./... + +clean: + rm -f forgejoImporter forgejoExporter forgejo_*.ptar + +.PHONY: all build test clean diff --git a/forgejo/README.md b/forgejo/README.md new file mode 100644 index 0000000..4236644 --- /dev/null +++ b/forgejo/README.md @@ -0,0 +1,81 @@ +# Forgejo integration + +[Forgejo](https://forgejo.org/) is a self-hosted software forge. This +integration backs up a Forgejo instance by invoking Forgejo's native +`forgejo dump` command and storing the generated archive in a Plakar snapshot. + +The restore connector extracts the archived dump contents to a target +directory. Forgejo does not provide a single `forgejo restore` command for full +instance dumps, so the extracted directory is intended to be used with +Forgejo's documented restore flow for the selected database and storage +backend. + +## Requirements + +- The `forgejo` binary must be available on the host where the backup runs, or + `forgejo_bin` must point to it. +- The backup process must run as an operating-system user that can read + Forgejo's configured data, repositories, attachments, and database. +- For consistent production backups, follow Forgejo's operational guidance for + your database and storage backend before running `forgejo dump`. + +## Backup + +```sh +plakar backup forgejo://local +``` + +Common options: + +```sh +plakar backup forgejo://local \ + -o forgejo_bin=/usr/local/bin/forgejo \ + -o work_path=/var/lib/forgejo \ + -o config_path=/etc/forgejo/app.ini \ + -o custom_path=/var/lib/forgejo/custom +``` + +The importer defaults to a gzip-compressed tar stream and stores it as +`/forgejo-dump.tar.gz` inside the Plakar snapshot: + +```sh +forgejo dump --file - --type tar.gz --quiet +``` + +Additional backup options: + +| Option | Description | +| --- | --- | +| `forgejo_bin` | Path to the Forgejo binary. Defaults to `forgejo`. | +| `work_path` | Value passed to `--work-path`. | +| `custom_path` | Value passed to `--custom-path`. | +| `config_path` | Value passed to `--config`. | +| `temp_dir` | Value passed to `--tempdir`. | +| `database` | Value passed to `--database` when Forgejo cannot infer the database syntax. | +| `dump_type` | Forgejo dump output type. Defaults to `tar.gz`. Supported restore extraction types are `tar`, `tar.gz`, `tgz`, and `zip`. | +| `skip_repository` | Pass `--skip-repository`. | +| `skip_log` | Pass `--skip-log`. | +| `skip_custom_dir` | Pass `--skip-custom-dir`. | +| `skip_lfs_data` | Pass `--skip-lfs-data`. | +| `skip_attachment_data` | Pass `--skip-attachment-data`. | +| `skip_package_data` | Pass `--skip-package-data`. | +| `skip_index` | Pass `--skip-index`. | +| `skip_repo_archives` | Pass `--skip-repo-archives`. | + +## Restore + +Restore the snapshot to a directory: + +```sh +plakar restore -to forgejo:///tmp/forgejo-restore +``` + +or pass an explicit target directory: + +```sh +plakar restore -to forgejo://local -o target_dir=/tmp/forgejo-restore +``` + +The exporter creates the target directory if it does not exist and extracts the +Forgejo dump archive into it. It rejects archive entries that would escape the +target directory. diff --git a/forgejo/config.go b/forgejo/config.go new file mode 100644 index 0000000..1a2aa7b --- /dev/null +++ b/forgejo/config.go @@ -0,0 +1,142 @@ +package forgejo + +import ( + "fmt" + "net/url" + "strconv" + "strings" +) + +const defaultDumpType = "tar.gz" + +type config struct { + location string + + forgejoBin string + workPath string + customPath string + configPath string + tempDir string + database string + dumpType string + + targetDir string + + skipRepository bool + skipLog bool + skipCustomDir bool + skipLFSData bool + skipAttachmentData bool + skipPackageData bool + skipIndex bool + skipRepoArchives bool +} + +func parseImporterConfig(values map[string]string) (config, error) { + cfg := config{ + location: values["location"], + forgejoBin: "forgejo", + dumpType: defaultDumpType, + } + if cfg.location == "" { + cfg.location = "forgejo://local" + } + if v := values["forgejo_bin"]; v != "" { + cfg.forgejoBin = v + } + cfg.workPath = values["work_path"] + cfg.customPath = values["custom_path"] + cfg.configPath = values["config_path"] + cfg.tempDir = values["temp_dir"] + cfg.database = values["database"] + if v := values["dump_type"]; v != "" { + v = strings.ToLower(v) + if !supportedDumpType(v) { + return cfg, fmt.Errorf("unsupported dump_type %q", v) + } + cfg.dumpType = v + } + + boolOptions := map[string]*bool{ + "skip_repository": &cfg.skipRepository, + "skip_log": &cfg.skipLog, + "skip_custom_dir": &cfg.skipCustomDir, + "skip_lfs_data": &cfg.skipLFSData, + "skip_attachment_data": &cfg.skipAttachmentData, + "skip_package_data": &cfg.skipPackageData, + "skip_index": &cfg.skipIndex, + "skip_repo_archives": &cfg.skipRepoArchives, + } + for key, dest := range boolOptions { + if err := parseBoolOption(values, key, dest); err != nil { + return cfg, err + } + } + return cfg, nil +} + +func parseExporterConfig(values map[string]string) (config, error) { + cfg := config{ + location: values["location"], + targetDir: values["target_dir"], + } + if cfg.location == "" { + cfg.location = "forgejo://local" + } + if cfg.targetDir == "" { + cfg.targetDir = targetDirFromLocation(cfg.location) + } + if cfg.targetDir == "" { + return cfg, fmt.Errorf("target_dir is required or location must be forgejo:///path") + } + return cfg, nil +} + +func parseBoolOption(values map[string]string, key string, dest *bool) error { + raw := values[key] + if raw == "" { + return nil + } + parsed, err := strconv.ParseBool(raw) + if err != nil { + return fmt.Errorf("invalid value for %s: %w", key, err) + } + *dest = parsed + return nil +} + +func targetDirFromLocation(location string) string { + parsed, err := url.Parse(location) + if err != nil || parsed.Scheme != "forgejo" { + return "" + } + if parsed.Path != "" && parsed.Path != "/" { + return parsed.Path + } + if parsed.Opaque != "" { + return parsed.Opaque + } + return "" +} + +func supportedDumpType(value string) bool { + switch strings.ToLower(value) { + case "tar", "tar.gz", "tgz", "zip": + return true + default: + return false + } +} + +func archiveName(dumpType string) string { + switch strings.ToLower(dumpType) { + case "tar": + return "forgejo-dump.tar" + case "zip": + return "forgejo-dump.zip" + case "tgz": + return "forgejo-dump.tgz" + default: + return "forgejo-dump.tar.gz" + } +} diff --git a/forgejo/exporter.go b/forgejo/exporter.go new file mode 100644 index 0000000..6c44a8c --- /dev/null +++ b/forgejo/exporter.go @@ -0,0 +1,216 @@ +package forgejo + +import ( + "archive/tar" + "archive/zip" + "bytes" + "compress/gzip" + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/PlakarKorp/kloset/connectors" + iexporter "github.com/PlakarKorp/kloset/connectors/exporter" + "github.com/PlakarKorp/kloset/location" +) + +func init() { + iexporter.Register("forgejo", 0, NewExporter) +} + +type Exporter struct { + cfg config +} + +func NewExporter(_ context.Context, _ *connectors.Options, _ string, values map[string]string) (iexporter.Exporter, error) { + cfg, err := parseExporterConfig(values) + if err != nil { + return nil, err + } + return &Exporter{cfg: cfg}, nil +} + +func (e *Exporter) Origin() string { return e.cfg.targetDir } +func (e *Exporter) Type() string { return "forgejo" } +func (e *Exporter) Root() string { return e.cfg.targetDir } +func (e *Exporter) Flags() location.Flags { return location.FLAG_LOCALFS } +func (e *Exporter) Ping(_ context.Context) error { + return os.MkdirAll(e.cfg.targetDir, 0755) +} +func (e *Exporter) Close(_ context.Context) error { return nil } + +func (e *Exporter) Export(ctx context.Context, records <-chan *connectors.Record, results chan<- *connectors.Result) error { + defer close(results) + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case record, ok := <-records: + if !ok { + return nil + } + if record.Err != nil { + results <- record.Error(record.Err) + continue + } + if record.FileInfo.Lmode.IsDir() { + results <- record.Ok() + continue + } + if err := e.restore(record); err != nil { + results <- record.Error(err) + continue + } + results <- record.Ok() + } + } +} + +func (e *Exporter) restore(record *connectors.Record) error { + if record.Reader == nil { + return fmt.Errorf("record %s has no reader", record.Pathname) + } + + name := filepath.Base(record.Pathname) + switch { + case strings.HasSuffix(name, ".tar.gz") || strings.HasSuffix(name, ".tgz"): + return extractTarGz(record.Reader, e.cfg.targetDir) + case strings.HasSuffix(name, ".tar"): + return extractTar(record.Reader, e.cfg.targetDir) + case strings.HasSuffix(name, ".zip"): + return extractZip(record.Reader, e.cfg.targetDir) + default: + return fmt.Errorf("unexpected Forgejo backup record %q", record.Pathname) + } +} + +func extractTarGz(reader io.Reader, targetDir string) error { + gz, err := gzip.NewReader(reader) + if err != nil { + return fmt.Errorf("opening gzip stream: %w", err) + } + defer gz.Close() + return extractTar(gz, targetDir) +} + +func extractTar(reader io.Reader, targetDir string) error { + if err := os.MkdirAll(targetDir, 0755); err != nil { + return err + } + + tr := tar.NewReader(reader) + for { + header, err := tr.Next() + if err == io.EOF { + return nil + } + if err != nil { + return fmt.Errorf("reading tar entry: %w", err) + } + target, err := safeJoin(targetDir, header.Name) + if err != nil { + return err + } + + switch header.Typeflag { + case tar.TypeDir: + if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil { + return err + } + case tar.TypeReg, tar.TypeRegA: + if err := writeFile(target, os.FileMode(header.Mode), tr); err != nil { + return err + } + default: + return fmt.Errorf("unsupported tar entry %q type %d", header.Name, header.Typeflag) + } + } +} + +func extractZip(reader io.Reader, targetDir string) error { + data, err := io.ReadAll(reader) + if err != nil { + return fmt.Errorf("reading zip archive: %w", err) + } + zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) + if err != nil { + return fmt.Errorf("opening zip archive: %w", err) + } + if err := os.MkdirAll(targetDir, 0755); err != nil { + return err + } + + for _, file := range zr.File { + target, err := safeJoin(targetDir, file.Name) + if err != nil { + return err + } + info := file.FileInfo() + if info.IsDir() { + if err := os.MkdirAll(target, info.Mode()); err != nil { + return err + } + continue + } + if !info.Mode().IsRegular() { + return fmt.Errorf("unsupported zip entry %q mode %s", file.Name, info.Mode()) + } + src, err := file.Open() + if err != nil { + return fmt.Errorf("opening zip entry %q: %w", file.Name, err) + } + err = writeFile(target, info.Mode(), src) + closeErr := src.Close() + if err != nil { + return err + } + if closeErr != nil { + return closeErr + } + } + return nil +} + +func writeFile(target string, mode os.FileMode, reader io.Reader) error { + if mode == 0 { + mode = 0644 + } + if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { + return err + } + file, err := os.OpenFile(target, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, mode) + if err != nil { + return err + } + _, copyErr := io.Copy(file, reader) + closeErr := file.Close() + if copyErr != nil { + return copyErr + } + return closeErr +} + +func safeJoin(baseDir, name string) (string, error) { + cleanName := filepath.Clean(name) + if cleanName == "." || filepath.IsAbs(cleanName) || strings.HasPrefix(cleanName, ".."+string(os.PathSeparator)) || cleanName == ".." { + return "", fmt.Errorf("unsafe archive path %q", name) + } + + base, err := filepath.Abs(baseDir) + if err != nil { + return "", err + } + target := filepath.Join(base, cleanName) + rel, err := filepath.Rel(base, target) + if err != nil { + return "", err + } + if rel == "." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) || rel == ".." { + return "", fmt.Errorf("unsafe archive path %q", name) + } + return target, nil +} diff --git a/forgejo/exporter/schema.json b/forgejo/exporter/schema.json new file mode 100644 index 0000000..1416c70 --- /dev/null +++ b/forgejo/exporter/schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Plakar Forgejo Exporter Connector Config", + "type": "object", + "additionalProperties": false, + "required": ["location"], + "properties": { + "location": { + "type": "string", + "minLength": 1, + "description": "Forgejo restore location. Use forgejo:///path/to/restore or target_dir." + }, + "target_dir": { + "type": "string", + "description": "Directory where Forgejo dump contents are extracted. Overrides the location path." + } + } +} diff --git a/forgejo/forgejo_test.go b/forgejo/forgejo_test.go new file mode 100644 index 0000000..ba67265 --- /dev/null +++ b/forgejo/forgejo_test.go @@ -0,0 +1,250 @@ +package forgejo + +import ( + "archive/tar" + "archive/zip" + "bytes" + "compress/gzip" + "context" + "io" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/PlakarKorp/kloset/connectors" +) + +func TestImporterDumpArgs(t *testing.T) { + cfg, err := parseImporterConfig(map[string]string{ + "location": "forgejo://local", + "forgejo_bin": "/usr/bin/forgejo", + "work_path": "/var/lib/forgejo", + "custom_path": "/var/lib/forgejo/custom", + "config_path": "/etc/forgejo/app.ini", + "temp_dir": "/tmp", + "database": "postgres", + "dump_type": "tar.gz", + "skip_repository": "true", + "skip_log": "true", + "skip_custom_dir": "true", + "skip_lfs_data": "true", + "skip_attachment_data": "true", + "skip_package_data": "true", + "skip_index": "true", + "skip_repo_archives": "true", + }) + if err != nil { + t.Fatal(err) + } + importer := &Importer{cfg: cfg} + got := importer.dumpArgs() + want := []string{ + "dump", + "--file", "-", + "--type", "tar.gz", + "--quiet", + "--work-path", "/var/lib/forgejo", + "--custom-path", "/var/lib/forgejo/custom", + "--config", "/etc/forgejo/app.ini", + "--tempdir", "/tmp", + "--database", "postgres", + "--skip-repository", + "--skip-log", + "--skip-custom-dir", + "--skip-lfs-data", + "--skip-attachment-data", + "--skip-package-data", + "--skip-index", + "--skip-repo-archives", + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("dump args mismatch\ngot: %#v\nwant: %#v", got, want) + } +} + +func TestImporterNormalizesDumpType(t *testing.T) { + cfg, err := parseImporterConfig(map[string]string{ + "dump_type": "TGZ", + }) + if err != nil { + t.Fatal(err) + } + if cfg.dumpType != "tgz" { + t.Fatalf("unexpected dump type: %q", cfg.dumpType) + } +} + +func TestImporterImportReportsClosedResults(t *testing.T) { + importer := &Importer{cfg: config{dumpType: defaultDumpType}} + records := make(chan *connectors.Record, 1) + results := make(chan *connectors.Result) + close(results) + + err := importer.Import(context.Background(), records, results) + if err == nil { + t.Fatal("expected an error when results closes without acknowledgement") + } +} + +func TestImporterStartDumpRunsForgejoCommand(t *testing.T) { + tmp := t.TempDir() + argsFile := filepath.Join(tmp, "args.txt") + forgejoBin := filepath.Join(tmp, "forgejo") + script := `#!/bin/sh +if [ "$1" = "--version" ]; then + echo "Forgejo version 1.0" + exit 0 +fi +printf '%s\n' "$@" > "$FORGEJO_ARGS_FILE" +printf 'archive-data' +` + if err := os.WriteFile(forgejoBin, []byte(script), 0755); err != nil { + t.Fatal(err) + } + + cfg, err := parseImporterConfig(map[string]string{ + "forgejo_bin": forgejoBin, + "dump_type": "zip", + "work_path": "/var/lib/forgejo", + }) + if err != nil { + t.Fatal(err) + } + importer := &Importer{cfg: cfg} + t.Setenv("FORGEJO_ARGS_FILE", argsFile) + + reader, err := importer.startDump(context.Background()) + if err != nil { + t.Fatal(err) + } + data, err := io.ReadAll(reader) + if err != nil { + t.Fatal(err) + } + if err := reader.Close(); err != nil { + t.Fatal(err) + } + if string(data) != "archive-data" { + t.Fatalf("unexpected dump output: %q", data) + } + + got, err := os.ReadFile(argsFile) + if err != nil { + t.Fatal(err) + } + want := "dump\n--file\n-\n--type\nzip\n--quiet\n--work-path\n/var/lib/forgejo\n" + if string(got) != want { + t.Fatalf("unexpected forgejo args:\ngot:\n%s\nwant:\n%s", got, want) + } +} + +func TestImporterPingReportsForgejoOutput(t *testing.T) { + tmp := t.TempDir() + forgejoBin := filepath.Join(tmp, "forgejo") + script := `#!/bin/sh +echo "bad config" >&2 +exit 42 +` + if err := os.WriteFile(forgejoBin, []byte(script), 0755); err != nil { + t.Fatal(err) + } + + importer := &Importer{cfg: config{forgejoBin: forgejoBin}} + err := importer.Ping(context.Background()) + if err == nil { + t.Fatal("expected ping error") + } + if !strings.Contains(err.Error(), "bad config") { + t.Fatalf("expected stderr in error, got %q", err) + } +} + +func TestExporterTargetDirFromLocation(t *testing.T) { + cfg, err := parseExporterConfig(map[string]string{ + "location": "forgejo:///tmp/forgejo-restore", + }) + if err != nil { + t.Fatal(err) + } + if cfg.targetDir != "/tmp/forgejo-restore" { + t.Fatalf("unexpected target dir: %q", cfg.targetDir) + } +} + +func TestExtractTarGz(t *testing.T) { + target := t.TempDir() + var archive bytes.Buffer + + gz := gzip.NewWriter(&archive) + tw := tar.NewWriter(gz) + if err := tw.WriteHeader(&tar.Header{Name: "custom/conf/app.ini", Mode: 0644, Size: int64(len("APP_NAME = Forgejo\n"))}); err != nil { + t.Fatal(err) + } + if _, err := tw.Write([]byte("APP_NAME = Forgejo\n")); err != nil { + t.Fatal(err) + } + if err := tw.Close(); err != nil { + t.Fatal(err) + } + if err := gz.Close(); err != nil { + t.Fatal(err) + } + + if err := extractTarGz(bytes.NewReader(archive.Bytes()), target); err != nil { + t.Fatal(err) + } + got, err := os.ReadFile(filepath.Join(target, "custom", "conf", "app.ini")) + if err != nil { + t.Fatal(err) + } + if string(got) != "APP_NAME = Forgejo\n" { + t.Fatalf("unexpected file contents: %q", got) + } +} + +func TestExtractTarRejectsTraversal(t *testing.T) { + var archive bytes.Buffer + tw := tar.NewWriter(&archive) + if err := tw.WriteHeader(&tar.Header{Name: "../escape", Mode: 0644, Size: 1}); err != nil { + t.Fatal(err) + } + if _, err := tw.Write([]byte("x")); err != nil { + t.Fatal(err) + } + if err := tw.Close(); err != nil { + t.Fatal(err) + } + + if err := extractTar(bytes.NewReader(archive.Bytes()), t.TempDir()); err == nil { + t.Fatal("expected traversal path to be rejected") + } +} + +func TestExtractZip(t *testing.T) { + target := t.TempDir() + var archive bytes.Buffer + zw := zip.NewWriter(&archive) + writer, err := zw.Create("repositories/example.git/HEAD") + if err != nil { + t.Fatal(err) + } + if _, err := writer.Write([]byte("ref: refs/heads/main\n")); err != nil { + t.Fatal(err) + } + if err := zw.Close(); err != nil { + t.Fatal(err) + } + + if err := extractZip(bytes.NewReader(archive.Bytes()), target); err != nil { + t.Fatal(err) + } + got, err := os.ReadFile(filepath.Join(target, "repositories", "example.git", "HEAD")) + if err != nil { + t.Fatal(err) + } + if string(got) != "ref: refs/heads/main\n" { + t.Fatalf("unexpected file contents: %q", got) + } +} diff --git a/forgejo/go.mod b/forgejo/go.mod new file mode 100644 index 0000000..27aea95 --- /dev/null +++ b/forgejo/go.mod @@ -0,0 +1,39 @@ +module github.com/PlakarKorp/integration-forgejo + +go 1.25.5 + +require ( + github.com/PlakarKorp/go-kloset-sdk v1.1.0-beta.1 + github.com/PlakarKorp/kloset v1.1.0-beta.2 +) + +require ( + github.com/PlakarKorp/integration-grpc v1.1.0-beta.3 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/nickball/go-aes-key-wrap v0.0.0-20170929221519-1c3aa3e4dfc5 // indirect + github.com/pierrec/lz4/v4 v4.1.25 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/tink-crypto/tink-go/v2 v2.6.0 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/zeebo/blake3 v0.2.4 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect + google.golang.org/grpc v1.78.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + modernc.org/libc v1.67.6 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.44.3 // indirect +) diff --git a/forgejo/go.sum b/forgejo/go.sum new file mode 100644 index 0000000..4f65c7c --- /dev/null +++ b/forgejo/go.sum @@ -0,0 +1,187 @@ +github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= +github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/NickBall/go-aes-key-wrap v0.0.0-20170929221519-1c3aa3e4dfc5 h1:5BIUS5hwyLM298mOf8e8TEgD3cCYqc86uaJdQCYZo/o= +github.com/NickBall/go-aes-key-wrap v0.0.0-20170929221519-1c3aa3e4dfc5/go.mod h1:w5D10RxC0NmPYxmQ438CC1S07zaC1zpvuNW7s5sUk2Q= +github.com/PlakarKorp/go-cdc-chunkers v1.0.3 h1:6ozBFcNMHvGe6IsbPcAZUUEAExCgcNx3aa8xiCA6+Qw= +github.com/PlakarKorp/go-cdc-chunkers v1.0.3/go.mod h1:y7ag92JABKPBDoSOPwedssQ5NIOgjRm4Mu6yTBpmUMY= +github.com/PlakarKorp/go-kloset-sdk v1.1.0-beta.1 h1:gPetIUfg///RiaML7CRINCcdXo55NHvaQIbpIoIBWGk= +github.com/PlakarKorp/go-kloset-sdk v1.1.0-beta.1/go.mod h1:ni69BgWur3+rHb7cOg/8JOKEMFh8J7tEPxTTSUhGNjE= +github.com/PlakarKorp/integration-grpc v1.1.0-beta.3 h1:u0n6Uyz7wqHOMoMYbfBLrcSrfAGqzcNQtcgkgYfU5TQ= +github.com/PlakarKorp/integration-grpc v1.1.0-beta.3/go.mod h1:lrkbUc9iT0jHiZ5wmB19wAhjDTMRO/VDhMJEt5trFlY= +github.com/PlakarKorp/kloset v1.1.0-beta.2 h1:YUMCCguPW6buz3h17eYYX+c0QMVE9c8WqkwTI6ueLwA= +github.com/PlakarKorp/kloset v1.1.0-beta.2/go.mod h1:LVCuJUI+ojPMyGeB32ydlT8xMl4NqVbvbnGwwFBaBMM= +github.com/RaduBerinde/axisds v0.1.0 h1:YItk/RmU5nvlsv/awo2Fjx97Mfpt4JfgtEVAGPrLdz8= +github.com/RaduBerinde/axisds v0.1.0/go.mod h1:UHGJonU9z4YYGKJxSaC6/TNcLOBptpmM5m2Cksbnw0Y= +github.com/RaduBerinde/btreemap v0.0.0-20250419174037-3d62b7205d54 h1:bsU8Tzxr/PNz75ayvCnxKZWEYdLMPDkUgticP4a4Bvk= +github.com/RaduBerinde/btreemap v0.0.0-20250419174037-3d62b7205d54/go.mod h1:0tr7FllbE9gJkHq7CVeeDDFAFKQVy5RnCSSNBOvdqbc= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/crlib v0.0.0-20250110162118-b7c9be99e911 h1:X+r2Lb1qj0APqrxM8NhBD3X3JDM1Fe+u+WAvhvKuLdM= +github.com/cockroachdb/crlib v0.0.0-20250110162118-b7c9be99e911/go.mod h1:Gq51ZeKaFCXk6QwuGM0w1dnaOqc/F5zKT2zA9D6Xeac= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 h1:ASDL+UJcILMqgNeV5jiqR4j+sTuvQNHdf2chuKj1M5k= +github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506/go.mod h1:Mw7HqKr2kdtu6aYGn3tPmAftiP3QPX63LdK/zcariIo= +github.com/cockroachdb/pebble/v2 v2.1.4 h1:j9wPgMDbkErFdAKYFGhsoCcvzcjR+6zrJ4jhKtJ6bOk= +github.com/cockroachdb/pebble/v2 v2.1.4/go.mod h1:Reo1RTniv1UjVTAu/Fv74y5i3kJ5gmVrPhO9UtFiKn8= +github.com/cockroachdb/redact v1.1.6 h1:zXJBwDZ84xJNlHl1rMyCojqyIxv+7YUpQiJLQ7n4314= +github.com/cockroachdb/redact v1.1.6/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/swiss v0.0.0-20251224182025-b0f6560f979b h1:VXvSNzmr8hMj8XTuY0PT9Ane9qZGul/p67vGYwl9BFI= +github.com/cockroachdb/swiss v0.0.0-20251224182025-b0f6560f979b/go.mod h1:yBRu/cnL4ks9bgy4vAASdjIW+/xMlFwuHKqtmh3GZQg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/getsentry/sentry-go v0.31.1 h1:ELVc0h7gwyhnXHDouXkhqTFSO5oslsRDk0++eyE0KJ4= +github.com/getsentry/sentry-go v0.31.1/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= +github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/minio/minlz v1.0.1-0.20250507153514-87eb42fe8882 h1:0lgqHvJWHLGW5TuObJrfyEi6+ASTKDBWikGvPqy9Yiw= +github.com/minio/minlz v1.0.1-0.20250507153514-87eb42fe8882/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/nickball/go-aes-key-wrap v0.0.0-20170929221519-1c3aa3e4dfc5 h1:eQr2od6dyd9gCLYHgMX2TlAYQtMUpxK7S0nsZXyH0L8= +github.com/nickball/go-aes-key-wrap v0.0.0-20170929221519-1c3aa3e4dfc5/go.mod h1:1VYCE0dvZM9Y2q8kcAHdXZB6YwfrCUQDeSJ2DuIiA4k= +github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0= +github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= +github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= +github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM= +github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tink-crypto/tink-go/v2 v2.6.0 h1:+KHNBHhWH33Vn+igZWcsgdEPUxKwBMEe0QC60t388v4= +github.com/tink-crypto/tink-go/v2 v2.6.0/go.mod h1:2WbBA6pfNsAfBwDCggboaHeB2X29wkU8XHtGwh2YIk8= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= +github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= +github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= +github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= +github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= +modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= +modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= +modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY= +modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/forgejo/importer.go b/forgejo/importer.go new file mode 100644 index 0000000..bfaebfe --- /dev/null +++ b/forgejo/importer.go @@ -0,0 +1,163 @@ +package forgejo + +import ( + "bytes" + "context" + "fmt" + "io" + "os/exec" + "strings" + "time" + + "github.com/PlakarKorp/kloset/connectors" + iimporter "github.com/PlakarKorp/kloset/connectors/importer" + "github.com/PlakarKorp/kloset/location" + "github.com/PlakarKorp/kloset/objects" +) + +func init() { + iimporter.Register("forgejo", 0, NewImporter) +} + +type Importer struct { + cfg config +} + +func NewImporter(_ context.Context, _ *connectors.Options, _ string, values map[string]string) (iimporter.Importer, error) { + cfg, err := parseImporterConfig(values) + if err != nil { + return nil, err + } + return &Importer{cfg: cfg}, nil +} + +func (i *Importer) Origin() string { return i.cfg.location } +func (i *Importer) Type() string { return "forgejo" } +func (i *Importer) Root() string { return "/" } +func (i *Importer) Flags() location.Flags { return location.FLAG_STREAM } +func (i *Importer) Close(_ context.Context) error { + return nil +} + +func (i *Importer) Ping(ctx context.Context) error { + cmd := exec.CommandContext(ctx, i.cfg.forgejoBin, "--version") + if out, err := cmd.CombinedOutput(); err != nil { + if msg := strings.TrimSpace(string(out)); msg != "" { + return fmt.Errorf("%w: %s", err, msg) + } + return err + } + return nil +} + +func (i *Importer) Import(ctx context.Context, records chan<- *connectors.Record, results <-chan *connectors.Result) error { + defer close(records) + + name := archiveName(i.cfg.dumpType) + fileinfo := objects.FileInfo{ + Lname: name, + Lsize: -1, + Lmode: 0444, + LmodTime: time.Now().UTC(), + } + + select { + case <-ctx.Done(): + return ctx.Err() + case records <- connectors.NewRecord("/"+name, "", fileinfo, nil, func() (io.ReadCloser, error) { + return i.startDump(ctx) + }): + } + + select { + case <-ctx.Done(): + return ctx.Err() + case result, ok := <-results: + if !ok || result == nil { + return fmt.Errorf("forgejo dump record was not acknowledged") + } + return result.Err + } +} + +func (i *Importer) dumpArgs() []string { + args := []string{ + "dump", + "--file", "-", + "--type", i.cfg.dumpType, + "--quiet", + } + if i.cfg.workPath != "" { + args = append(args, "--work-path", i.cfg.workPath) + } + if i.cfg.customPath != "" { + args = append(args, "--custom-path", i.cfg.customPath) + } + if i.cfg.configPath != "" { + args = append(args, "--config", i.cfg.configPath) + } + if i.cfg.tempDir != "" { + args = append(args, "--tempdir", i.cfg.tempDir) + } + if i.cfg.database != "" { + args = append(args, "--database", i.cfg.database) + } + if i.cfg.skipRepository { + args = append(args, "--skip-repository") + } + if i.cfg.skipLog { + args = append(args, "--skip-log") + } + if i.cfg.skipCustomDir { + args = append(args, "--skip-custom-dir") + } + if i.cfg.skipLFSData { + args = append(args, "--skip-lfs-data") + } + if i.cfg.skipAttachmentData { + args = append(args, "--skip-attachment-data") + } + if i.cfg.skipPackageData { + args = append(args, "--skip-package-data") + } + if i.cfg.skipIndex { + args = append(args, "--skip-index") + } + if i.cfg.skipRepoArchives { + args = append(args, "--skip-repo-archives") + } + return args +} + +type commandReadCloser struct { + io.ReadCloser + cmd *exec.Cmd + stderr *bytes.Buffer +} + +func (r *commandReadCloser) Close() error { + _ = r.ReadCloser.Close() + err := r.cmd.Wait() + if err != nil { + if msg := strings.TrimSpace(r.stderr.String()); msg != "" { + return fmt.Errorf("%w: %s", err, msg) + } + return err + } + return nil +} + +func (i *Importer) startDump(ctx context.Context) (io.ReadCloser, error) { + cmd := exec.CommandContext(ctx, i.cfg.forgejoBin, i.dumpArgs()...) + var stderr bytes.Buffer + cmd.Stderr = &stderr + + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, fmt.Errorf("creating forgejo dump stdout pipe: %w", err) + } + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("starting forgejo dump: %w", err) + } + return &commandReadCloser{ReadCloser: stdout, cmd: cmd, stderr: &stderr}, nil +} diff --git a/forgejo/importer/schema.json b/forgejo/importer/schema.json new file mode 100644 index 0000000..689aa7d --- /dev/null +++ b/forgejo/importer/schema.json @@ -0,0 +1,75 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Plakar Forgejo Importer Connector Config", + "type": "object", + "additionalProperties": false, + "required": ["location"], + "properties": { + "location": { + "type": "string", + "minLength": 1, + "description": "Forgejo source location, for example forgejo://local" + }, + "forgejo_bin": { + "type": "string", + "description": "Path to the Forgejo binary. Defaults to forgejo." + }, + "work_path": { + "type": "string", + "description": "Forgejo work path passed to --work-path." + }, + "custom_path": { + "type": "string", + "description": "Forgejo custom path passed to --custom-path." + }, + "config_path": { + "type": "string", + "description": "Forgejo app.ini path passed to --config." + }, + "temp_dir": { + "type": "string", + "description": "Temporary directory passed to --tempdir." + }, + "database": { + "type": "string", + "description": "Database SQL syntax passed to --database, for example sqlite3, mysql, or postgres." + }, + "dump_type": { + "type": "string", + "enum": ["tar", "tar.gz", "tgz", "zip"], + "description": "Forgejo dump output type. Defaults to tar.gz." + }, + "skip_repository": { + "type": "boolean", + "description": "Pass --skip-repository to forgejo dump." + }, + "skip_log": { + "type": "boolean", + "description": "Pass --skip-log to forgejo dump." + }, + "skip_custom_dir": { + "type": "boolean", + "description": "Pass --skip-custom-dir to forgejo dump." + }, + "skip_lfs_data": { + "type": "boolean", + "description": "Pass --skip-lfs-data to forgejo dump." + }, + "skip_attachment_data": { + "type": "boolean", + "description": "Pass --skip-attachment-data to forgejo dump." + }, + "skip_package_data": { + "type": "boolean", + "description": "Pass --skip-package-data to forgejo dump." + }, + "skip_index": { + "type": "boolean", + "description": "Pass --skip-index to forgejo dump." + }, + "skip_repo_archives": { + "type": "boolean", + "description": "Pass --skip-repo-archives to forgejo dump." + } + } +} diff --git a/forgejo/manifest.yaml b/forgejo/manifest.yaml new file mode 100644 index 0000000..3e40bd3 --- /dev/null +++ b/forgejo/manifest.yaml @@ -0,0 +1,20 @@ +name: forgejo +display_name: Forgejo +description: Back up Forgejo instances using the native forgejo dump command and restore dump contents to disk. +homepage: https://github.com/PlakarKorp/integration-forgejo +license: ISC +api_version: v1.1.0 +connectors: + - type: importer + executable: forgejoImporter + protocols: [forgejo] + validator: ./importer/schema.json + class: scm + subclass: forgejo + - type: exporter + executable: forgejoExporter + protocols: [forgejo] + validator: ./exporter/schema.json + flags: [localfs] + class: scm + subclass: forgejo diff --git a/forgejo/plugin/exporter/main.go b/forgejo/plugin/exporter/main.go new file mode 100644 index 0000000..fb8d1c8 --- /dev/null +++ b/forgejo/plugin/exporter/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "os" + + sdk "github.com/PlakarKorp/go-kloset-sdk" + forgejo "github.com/PlakarKorp/integration-forgejo" +) + +func main() { + sdk.EntrypointExporter(os.Args, forgejo.NewExporter) +} diff --git a/forgejo/plugin/importer/main.go b/forgejo/plugin/importer/main.go new file mode 100644 index 0000000..2b624ba --- /dev/null +++ b/forgejo/plugin/importer/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "os" + + sdk "github.com/PlakarKorp/go-kloset-sdk" + forgejo "github.com/PlakarKorp/integration-forgejo" +) + +func main() { + sdk.EntrypointImporter(os.Args, forgejo.NewImporter) +}