Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions forgejo/Makefile
Original file line number Diff line number Diff line change
@@ -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
81 changes: 81 additions & 0 deletions forgejo/README.md
Original file line number Diff line number Diff line change
@@ -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 <snapshot-id>
```

or pass an explicit target directory:

```sh
plakar restore -to forgejo://local -o target_dir=/tmp/forgejo-restore <snapshot-id>
```

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.
142 changes: 142 additions & 0 deletions forgejo/config.go
Original file line number Diff line number Diff line change
@@ -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"
}
}
Loading