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:
parent
25ad5a6d88
commit
9de53f2bb3
3 changed files with 78 additions and 26 deletions
|
|
@ -7,16 +7,19 @@
|
||||||
---@field category string
|
---@field category string
|
||||||
|
|
||||||
---@class pending.GcalConfig
|
---@class pending.GcalConfig
|
||||||
|
---@field remote_delete? boolean
|
||||||
---@field credentials_path? string
|
---@field credentials_path? string
|
||||||
---@field client_id? string
|
---@field client_id? string
|
||||||
---@field client_secret? string
|
---@field client_secret? string
|
||||||
|
|
||||||
---@class pending.GtasksConfig
|
---@class pending.GtasksConfig
|
||||||
|
---@field remote_delete? boolean
|
||||||
---@field credentials_path? string
|
---@field credentials_path? string
|
||||||
---@field client_id? string
|
---@field client_id? string
|
||||||
---@field client_secret? string
|
---@field client_secret? string
|
||||||
|
|
||||||
---@class pending.SyncConfig
|
---@class pending.SyncConfig
|
||||||
|
---@field remote_delete? boolean
|
||||||
---@field gcal? pending.GcalConfig
|
---@field gcal? pending.GcalConfig
|
||||||
---@field gtasks? pending.GtasksConfig
|
---@field gtasks? pending.GtasksConfig
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,31 @@ local function delete_event(access_token, calendar_id, event_id)
|
||||||
return err
|
return err
|
||||||
end
|
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
|
---@param callback fun(access_token: string): nil
|
||||||
local function with_token(callback)
|
local function with_token(callback)
|
||||||
oauth.async(function()
|
oauth.async(function()
|
||||||
|
|
@ -150,6 +175,7 @@ function M.push()
|
||||||
end
|
end
|
||||||
|
|
||||||
local s = require('pending').store()
|
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
|
local created, updated, deleted = 0, 0, 0
|
||||||
|
|
||||||
for _, task in ipairs(s:tasks()) do
|
for _, task in ipairs(s:tasks()) do
|
||||||
|
|
@ -166,19 +192,20 @@ function M.push()
|
||||||
)
|
)
|
||||||
|
|
||||||
if should_delete then
|
if should_delete then
|
||||||
|
if allow_remote_delete() then
|
||||||
local del_err =
|
local del_err =
|
||||||
delete_event(access_token, cal_id --[[@as string]], event_id --[[@as string]])
|
delete_event(access_token, cal_id --[[@as string]], event_id --[[@as string]])
|
||||||
if del_err then
|
if del_err then
|
||||||
log.warn('gcal delete failed: ' .. del_err)
|
log.warn('gcal delete failed: ' .. del_err)
|
||||||
else
|
else
|
||||||
extra['_gcal_event_id'] = nil
|
unlink_remote(task, extra, now_ts)
|
||||||
extra['_gcal_calendar_id'] = nil
|
deleted = deleted + 1
|
||||||
if next(extra) == nil then
|
|
||||||
task._extra = nil
|
|
||||||
else
|
|
||||||
task._extra = extra
|
|
||||||
end
|
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
|
deleted = deleted + 1
|
||||||
end
|
end
|
||||||
elseif task.status == 'pending' and task.due then
|
elseif task.status == 'pending' and task.due then
|
||||||
|
|
@ -204,7 +231,7 @@ function M.push()
|
||||||
end
|
end
|
||||||
task._extra['_gcal_event_id'] = new_id
|
task._extra['_gcal_event_id'] = new_id
|
||||||
task._extra['_gcal_calendar_id'] = lid
|
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
|
created = created + 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,29 @@ local function parse_notes(notes)
|
||||||
return priority, recur, recur_mode
|
return priority, recur, recur_mode
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return boolean
|
||||||
|
local function allow_remote_delete()
|
||||||
|
local cfg = config.get()
|
||||||
|
local sync = cfg.sync or {}
|
||||||
|
local per = (sync.gtasks or {}) --[[@as pending.GtasksConfig]]
|
||||||
|
if per.remote_delete ~= nil then
|
||||||
|
return per.remote_delete == true
|
||||||
|
end
|
||||||
|
return sync.remote_delete == true
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param task pending.Task
|
||||||
|
---@param now_ts string
|
||||||
|
local function unlink_remote(task, now_ts)
|
||||||
|
task._extra['_gtasks_task_id'] = nil
|
||||||
|
task._extra['_gtasks_list_id'] = nil
|
||||||
|
task._extra['_gtasks_synced_at'] = nil
|
||||||
|
if next(task._extra) == nil then
|
||||||
|
task._extra = nil
|
||||||
|
end
|
||||||
|
task.modified = now_ts
|
||||||
|
end
|
||||||
|
|
||||||
---@param task pending.Task
|
---@param task pending.Task
|
||||||
---@return table
|
---@return table
|
||||||
local function task_to_gtask(task)
|
local function task_to_gtask(task)
|
||||||
|
|
@ -240,21 +263,20 @@ local function push_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
|
||||||
local list_id = extra['_gtasks_list_id'] --[[@as string?]]
|
local list_id = extra['_gtasks_list_id'] --[[@as string?]]
|
||||||
|
|
||||||
if task.status == 'deleted' and gtid and list_id then
|
if task.status == 'deleted' and gtid and list_id then
|
||||||
|
if allow_remote_delete() then
|
||||||
local err = delete_gtask(access_token, list_id, gtid)
|
local err = delete_gtask(access_token, list_id, gtid)
|
||||||
if err then
|
if err then
|
||||||
log.warn('gtasks delete failed: ' .. err)
|
log.warn('gtasks delete failed: ' .. err)
|
||||||
failed = failed + 1
|
failed = failed + 1
|
||||||
else
|
else
|
||||||
if not task._extra then
|
unlink_remote(task, now_ts)
|
||||||
task._extra = {}
|
deleted = deleted + 1
|
||||||
end
|
end
|
||||||
task._extra['_gtasks_task_id'] = nil
|
else
|
||||||
task._extra['_gtasks_list_id'] = nil
|
log.debug(
|
||||||
task._extra['_gtasks_synced_at'] = nil
|
'gtasks: remote delete skipped (remote_delete disabled) — unlinked task #' .. task.id
|
||||||
if next(task._extra) == nil then
|
)
|
||||||
task._extra = nil
|
unlink_remote(task, now_ts)
|
||||||
end
|
|
||||||
task.modified = now_ts
|
|
||||||
deleted = deleted + 1
|
deleted = deleted + 1
|
||||||
end
|
end
|
||||||
elseif task.status ~= 'deleted' then
|
elseif task.status ~= 'deleted' then
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue