feat(sync): add opt-in remote deletion for gcal and gtasks (#85)

Problem: push/sync permanently deleted remote Google Calendar events and
Google Tasks entries whenever a local task was marked deleted, done, or
de-due'd. There was no opt-out, so a misfire could silently cause
irreversible data loss on the remote side.

Solution: add a `remote_delete` boolean to the config (default `false`).
A unified flag at `sync.remote_delete` sets the base; per-backend
overrides at `sync.gcal.remote_delete` / `sync.gtasks.remote_delete`
take precedence when non-nil. When disabled, `_extra` remote IDs are
cleared silently (unlinking) so stale IDs don't accumulate.
This commit is contained in:
Barrett Ruth 2026-03-06 13:12:53 -05:00
parent 25ad5a6d88
commit 9de53f2bb3
3 changed files with 78 additions and 26 deletions

View file

@ -129,6 +129,31 @@ local function delete_event(access_token, calendar_id, event_id)
return err
end
---@return boolean
local function allow_remote_delete()
local cfg = config.get()
local sync = cfg.sync or {}
local per = (sync.gcal or {}) --[[@as pending.GcalConfig]]
if per.remote_delete ~= nil then
return per.remote_delete == true
end
return sync.remote_delete == true
end
---@param task pending.Task
---@param extra table
---@param now_ts string
local function unlink_remote(task, extra, now_ts)
extra['_gcal_event_id'] = nil
extra['_gcal_calendar_id'] = nil
if next(extra) == nil then
task._extra = nil
else
task._extra = extra
end
task.modified = now_ts
end
---@param callback fun(access_token: string): nil
local function with_token(callback)
oauth.async(function()
@ -150,6 +175,7 @@ function M.push()
end
local s = require('pending').store()
local now_ts = os.date('!%Y-%m-%dT%H:%M:%SZ') --[[@as string]]
local created, updated, deleted = 0, 0, 0
for _, task in ipairs(s:tasks()) do
@ -166,19 +192,20 @@ function M.push()
)
if should_delete then
local del_err =
delete_event(access_token, cal_id --[[@as string]], event_id --[[@as string]])
if del_err then
log.warn('gcal delete failed: ' .. del_err)
else
extra['_gcal_event_id'] = nil
extra['_gcal_calendar_id'] = nil
if next(extra) == nil then
task._extra = nil
if allow_remote_delete() then
local del_err =
delete_event(access_token, cal_id --[[@as string]], event_id --[[@as string]])
if del_err then
log.warn('gcal delete failed: ' .. del_err)
else
task._extra = extra
unlink_remote(task, extra, now_ts)
deleted = deleted + 1
end
task.modified = os.date('!%Y-%m-%dT%H:%M:%SZ') --[[@as string]]
else
log.debug(
'gcal: remote delete skipped (remote_delete disabled) — unlinked task #' .. task.id
)
unlink_remote(task, extra, now_ts)
deleted = deleted + 1
end
elseif task.status == 'pending' and task.due then
@ -204,7 +231,7 @@ function M.push()
end
task._extra['_gcal_event_id'] = new_id
task._extra['_gcal_calendar_id'] = lid
task.modified = os.date('!%Y-%m-%dT%H:%M:%SZ') --[[@as string]]
task.modified = now_ts
created = created + 1
end
end