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 }