diff --git a/README.md b/README.md
index 9021f5c..d8e2c1e 100644
--- a/README.md
+++ b/README.md
@@ -70,6 +70,8 @@ synosharp apply spec.json --confirm # …only this mutates
The reconciler does **existence reconciliation** (create-if-missing /
delete-if-`present:false` / skip-if-present) and **never prunes** unmanaged
-resources. **Next:** field-level drift (desc/ACLs), then NFS exports via
+resources. Share deletes **keep data by default** (DSM shares are btrfs subvolumes);
+set `ShareSpec.DeleteData = true` for a destructive `synoshare --del TRUE`.
+**Next:** field-level drift (desc/ACLs), then NFS exports via
`synowebapi` — last, highest-risk, prove on Virtual DSM (needs an x86/KVM host).
See the [write-path plan](https://github.com/chrison-dev/Homelab/blob/main/docs/plans/057-synosharp-write-path.md).
diff --git a/src/SynoSharp/Provisioning/Specs.cs b/src/SynoSharp/Provisioning/Specs.cs
index 4c15feb..e4c0a48 100644
--- a/src/SynoSharp/Provisioning/Specs.cs
+++ b/src/SynoSharp/Provisioning/Specs.cs
@@ -10,6 +10,14 @@ public sealed record ShareSpec
public required string Path { get; init; }
public bool Present { get; init; } = true;
+
+ ///
+ /// When deleting ( false), also remove the share's data.
+ /// DSM shares are btrfs subvolumes, so the default keep-data delete leaves the
+ /// subvolume behind (plain rm can't remove it) — set true for a full
+ /// synoshare --del TRUE. Destructive; off by default.
+ ///
+ public bool DeleteData { get; init; }
}
/// Desired state for a local user. false = ensure absent.
diff --git a/src/SynoSharp/Provisioning/SynologyReconciler.cs b/src/SynoSharp/Provisioning/SynologyReconciler.cs
index e3ea89e..b2b4c48 100644
--- a/src/SynoSharp/Provisioning/SynologyReconciler.cs
+++ b/src/SynoSharp/Provisioning/SynologyReconciler.cs
@@ -133,7 +133,8 @@ private static PlannedAction PlanShare(ShareSpec s, bool exists)
}
if (!s.Present && exists)
{
- return PlannedAction.Delete("share", s.Name, SynoShareTool.DeleteCommand(s.Name), "present → delete");
+ var reason = s.DeleteData ? "present → delete (incl. data)" : "present → delete (keep data)";
+ return PlannedAction.Delete("share", s.Name, SynoShareTool.DeleteCommand(s.Name, s.DeleteData), reason);
}
return PlannedAction.Skip("share", s.Name, s.Present ? "already present" : "already absent");
}
diff --git a/tests/SynoSharp.Tests/ReconcilerTests.cs b/tests/SynoSharp.Tests/ReconcilerTests.cs
index 544b77b..2158339 100644
--- a/tests/SynoSharp.Tests/ReconcilerTests.cs
+++ b/tests/SynoSharp.Tests/ReconcilerTests.cs
@@ -73,6 +73,38 @@ public async Task Plan_deletes_when_present_false()
Assert.Equal("synogroup --del http", delete.Command!.Render());
}
+ [Fact]
+ public async Task Plan_share_delete_keeps_data_by_default()
+ {
+ var reconciler = ReconcilerWith(out _);
+ var desired = new SynologyDesiredState
+ {
+ Shares = [new ShareSpec { Name = "Volume-1", Path = "/volume1/Volume-1", Present = false }],
+ };
+
+ var plan = await reconciler.PlanAsync(desired);
+
+ var delete = Assert.Single(plan.Mutations);
+ Assert.Equal("synoshare --del FALSE Volume-1", delete.Command!.Render());
+ Assert.Contains("keep data", delete.Reason);
+ }
+
+ [Fact]
+ public async Task Plan_share_delete_with_DeleteData_removes_data()
+ {
+ var reconciler = ReconcilerWith(out _);
+ var desired = new SynologyDesiredState
+ {
+ Shares = [new ShareSpec { Name = "Volume-1", Path = "/volume1/Volume-1", Present = false, DeleteData = true }],
+ };
+
+ var plan = await reconciler.PlanAsync(desired);
+
+ var delete = Assert.Single(plan.Mutations);
+ Assert.Equal("synoshare --del TRUE Volume-1", delete.Command!.Render());
+ Assert.Contains("incl. data", delete.Reason);
+ }
+
[Fact]
public async Task Plan_blocks_user_create_without_password()
{