feat: establish NekoNest Cloud control and relay

This commit is contained in:
2026-08-12 23:25:43 +08:00
commit f27606b709
222 changed files with 71456 additions and 0 deletions
+128
View File
@@ -0,0 +1,128 @@
package tenantpurge
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/klarkxy/nekonest-cloud/relay/internal/tenantfs"
)
type Result struct {
DataDeleted bool
BackupsDeleted bool
EvidenceSHA256 string
}
func exactChild(parent, child string) (string, string, error) {
parentAbsolute, err := filepath.Abs(strings.TrimSpace(parent))
if err != nil || strings.TrimSpace(parent) == "" {
return "", "", errors.New("invalid purge parent")
}
childAbsolute, err := filepath.Abs(strings.TrimSpace(child))
if err != nil || filepath.Dir(childAbsolute) != parentAbsolute {
return "", "", errors.New("purge target escaped its parent")
}
return parentAbsolute, childAbsolute, nil
}
func validateTree(ctx context.Context, root string) error {
return filepath.WalkDir(root, func(path string, entry os.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if err := ctx.Err(); err != nil {
return err
}
if entry.Type()&os.ModeSymlink != 0 {
return fmt.Errorf("purge target contains a symbolic link: %s", path)
}
info, err := entry.Info()
if err != nil {
return err
}
if !info.IsDir() && !info.Mode().IsRegular() {
return fmt.Errorf("purge target contains a non-regular artifact: %s", path)
}
return nil
})
}
func removeTree(ctx context.Context, parent, target string) (bool, error) {
parent, target, err := exactChild(parent, target)
if err != nil {
return false, err
}
parentInfo, err := os.Lstat(parent)
if os.IsNotExist(err) {
return false, nil
}
if err != nil || parentInfo.Mode()&os.ModeSymlink != 0 || !parentInfo.IsDir() {
return false, errors.New("purge parent is not a real directory")
}
info, err := os.Lstat(target)
if os.IsNotExist(err) {
return false, nil
}
if err != nil || info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
return false, errors.New("purge target is not a real directory")
}
if err := validateTree(ctx, target); err != nil {
return false, err
}
if err := os.RemoveAll(target); err != nil {
return false, err
}
if _, err := os.Lstat(target); !os.IsNotExist(err) {
if err == nil {
return false, errors.New("purge target still exists")
}
return false, err
}
return true, nil
}
func evidence(tenantID string, generation int64) string {
digest := sha256.Sum256([]byte(
"nekonest-cloud/tenant-logical-purge/v1\x00" + tenantID + "\x00" + strconv.FormatInt(generation, 10),
))
return hex.EncodeToString(digest[:])
}
// Purge performs an idempotent application-layer deletion of one tenant's
// live Relay directory and every backup directory. It refuses links and
// special files rather than following them. Storage-provider block erasure is
// outside this primitive and must be covered by the infrastructure policy.
func Purge(ctx context.Context, dataRoot, backupRoot, tenantID string, generation int64) (Result, error) {
if generation < 1 {
return Result{}, errors.New("invalid purge generation")
}
paths, err := tenantfs.Derive(dataRoot, tenantID)
if err != nil {
return Result{}, err
}
dataParent := filepath.Dir(paths.Root)
backupRootAbsolute, err := filepath.Abs(strings.TrimSpace(backupRoot))
if err != nil || strings.TrimSpace(backupRoot) == "" {
return Result{}, errors.New("invalid backup root")
}
backupTenantRoot := filepath.Join(backupRootAbsolute, filepath.Base(paths.Root))
dataDeleted, err := removeTree(ctx, dataParent, paths.Root)
if err != nil {
return Result{}, fmt.Errorf("delete tenant Relay data: %w", err)
}
backupsDeleted, err := removeTree(ctx, backupRootAbsolute, backupTenantRoot)
if err != nil {
return Result{}, fmt.Errorf("delete tenant Relay backups: %w", err)
}
return Result{
DataDeleted: dataDeleted, BackupsDeleted: backupsDeleted,
EvidenceSHA256: evidence(tenantID, generation),
}, nil
}
+77
View File
@@ -0,0 +1,77 @@
package tenantpurge
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/klarkxy/nekonest-cloud/relay/internal/tenantfs"
)
const testTenantID = "tenant_0123456789abcdef0123456789abcdef"
func TestPurgeDeletesLiveDataAndEveryBackupIdempotently(t *testing.T) {
dataRoot := t.TempDir()
backupRoot := t.TempDir()
paths, err := tenantfs.Resolve(dataRoot, testTenantID)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(paths.Database, []byte("sqlite"), 0o600); err != nil {
t.Fatal(err)
}
backupTenant := filepath.Join(backupRoot, filepath.Base(paths.Root))
for _, name := range []string{"g1", "g2"} {
path := filepath.Join(backupTenant, name)
if err := os.MkdirAll(path, 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(path, "manifest.json"), []byte("{}"), 0o600); err != nil {
t.Fatal(err)
}
}
first, err := Purge(context.Background(), dataRoot, backupRoot, testTenantID, 3)
if err != nil {
t.Fatal(err)
}
if !first.DataDeleted || !first.BackupsDeleted || len(first.EvidenceSHA256) != 64 {
t.Fatalf("purge result = %#v", first)
}
if _, err := os.Lstat(paths.Root); !os.IsNotExist(err) {
t.Fatalf("live tenant data survived: %v", err)
}
if _, err := os.Lstat(backupTenant); !os.IsNotExist(err) {
t.Fatalf("tenant backups survived: %v", err)
}
second, err := Purge(context.Background(), dataRoot, backupRoot, testTenantID, 3)
if err != nil {
t.Fatal(err)
}
if second.DataDeleted || second.BackupsDeleted || second.EvidenceSHA256 != first.EvidenceSHA256 {
t.Fatalf("idempotent retry = %#v", second)
}
}
func TestPurgeRefusesSymbolicLinksWithoutTouchingTarget(t *testing.T) {
dataRoot := t.TempDir()
backupRoot := t.TempDir()
outside := t.TempDir()
outsideFile := filepath.Join(outside, "keep.txt")
if err := os.WriteFile(outsideFile, []byte("keep"), 0o600); err != nil {
t.Fatal(err)
}
paths, err := tenantfs.Resolve(dataRoot, testTenantID)
if err != nil {
t.Fatal(err)
}
if err := os.Symlink(outsideFile, filepath.Join(paths.Root, "escape")); err != nil {
t.Skipf("symbolic links unavailable: %v", err)
}
if _, err := Purge(context.Background(), dataRoot, backupRoot, testTenantID, 1); err == nil {
t.Fatal("purge followed or accepted a symbolic link")
}
if data, err := os.ReadFile(outsideFile); err != nil || string(data) != "keep" {
t.Fatalf("outside target changed: data=%q err=%v", data, err)
}
}