feat: establish NekoNest Cloud control and relay
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user