129 lines
3.8 KiB
Go
129 lines
3.8 KiB
Go
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
|
|
}
|