From 90c6f4fa5436efd9fed21c44e80da0da7a0d652b Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Mon, 16 Feb 2026 12:43:54 +0100 Subject: [PATCH] Fixed task restore --- .../RestoreProvider/RestoreProvider.Tasks.cs | 72 +++++++++++++------ .../SourceItems/TaskListSourceEntry.cs | 7 +- 2 files changed, 54 insertions(+), 25 deletions(-) diff --git a/proprietary/GoogleWorkspace/RestoreProvider/RestoreProvider.Tasks.cs b/proprietary/GoogleWorkspace/RestoreProvider/RestoreProvider.Tasks.cs index abb81f19d..e1ba6f9ab 100644 --- a/proprietary/GoogleWorkspace/RestoreProvider/RestoreProvider.Tasks.cs +++ b/proprietary/GoogleWorkspace/RestoreProvider/RestoreProvider.Tasks.cs @@ -111,6 +111,30 @@ partial class RestoreProvider var createdTask = await tasksService.Tasks.Insert(task, taskListId).ExecuteAsync(cancel); return createdTask.Id; } + + public async Task GetOrCreateRestoredTaskList(string userId, CancellationToken cancel) + { + const string RESTORED_TASKLIST_NAME = "Restored"; + + var tasksService = Provider._apiHelper.GetTasksService(userId); + + // Check if task list already exists + var existingLists = await tasksService.Tasklists.List().ExecuteAsync(cancel); + var existingList = existingLists.Items?.FirstOrDefault(l => + l.Title?.Equals(RESTORED_TASKLIST_NAME, StringComparison.OrdinalIgnoreCase) == true); + + if (existingList != null) + return existingList.Id; + + // Create new task list + var newTaskList = new TaskList + { + Title = RESTORED_TASKLIST_NAME + }; + + var createdList = await tasksService.Tasklists.Insert(newTaskList).ExecuteAsync(cancel); + return createdList.Id; + } } private async System.Threading.Tasks.Task RestoreTaskLists(CancellationToken cancel) @@ -134,25 +158,13 @@ partial class RestoreProvider try { var originalPath = taskList.Key; - var contentEntry = _temporaryFiles.GetValueOrDefault(originalPath); + var metadata = taskList.Value; - if (contentEntry == null) + // Create TaskList from metadata + var taskListData = new TaskList { - Log.WriteWarningMessage(LOGTAG, "RestoreTaskListsMissingContent", null, $"Missing content for task list {originalPath}, skipping."); - continue; - } - - TaskList? taskListData; - using (var contentStream = SystemIO.IO_OS.FileOpenRead(contentEntry)) - { - taskListData = await JsonSerializer.DeserializeAsync(contentStream, cancellationToken: cancel); - } - - if (taskListData == null) - { - Log.WriteWarningMessage(LOGTAG, "RestoreTaskListsInvalidContent", null, $"Invalid content for task list {originalPath}, skipping."); - continue; - } + Title = metadata.GetValueOrDefault("gsuite:Name") ?? "Unnamed Task List" + }; var newListId = await TaskRestore.CreateTaskList(userId, taskListData, cancel); if (newListId != null) @@ -161,8 +173,6 @@ partial class RestoreProvider } _metadata.TryRemove(originalPath, out _); - _temporaryFiles.TryRemove(originalPath, out var contentFile); - contentFile?.Dispose(); } catch (Exception ex) { @@ -184,6 +194,9 @@ partial class RestoreProvider if (string.IsNullOrWhiteSpace(userId)) return; + // Get or create the "Restored" task list for orphaned tasks + string? restoredTaskListId = null; + foreach (var task in tasks) { if (cancel.IsCancellationRequested) @@ -200,12 +213,25 @@ partial class RestoreProvider continue; } - // Find parent task list + // Find parent task list - tasks are stored under their tasklist's path var parentPath = Util.AppendDirSeparator(Path.GetDirectoryName(originalPath.TrimEnd(Path.DirectorySeparatorChar)) ?? ""); - if (parentPath == null || !_restoredTaskListMap.TryGetValue(parentPath, out var listId)) + + // Try to find the parent task list in our restored map + string? listId = null; + if (!string.IsNullOrEmpty(parentPath) && _restoredTaskListMap.TryGetValue(parentPath, out var mappedListId)) { - Log.WriteWarningMessage(LOGTAG, "RestoreTasksMissingParent", null, $"Could not find parent list for task {originalPath}, skipping."); - continue; + listId = mappedListId; + } + + // If no parent list found, use/create the "Restored" task list + if (listId == null) + { + if (restoredTaskListId == null) + { + restoredTaskListId = await TaskRestore.GetOrCreateRestoredTaskList(userId, cancel); + } + listId = restoredTaskListId; + Log.WriteInformationMessage(LOGTAG, "RestoreTasksToRestoredList", $"Task {originalPath} has no parent list, adding to 'Restored' task list."); } Google.Apis.Tasks.v1.Data.Task? taskData; diff --git a/proprietary/GoogleWorkspace/SourceItems/TaskListSourceEntry.cs b/proprietary/GoogleWorkspace/SourceItems/TaskListSourceEntry.cs index 643ba5902..f52199851 100644 --- a/proprietary/GoogleWorkspace/SourceItems/TaskListSourceEntry.cs +++ b/proprietary/GoogleWorkspace/SourceItems/TaskListSourceEntry.cs @@ -10,7 +10,7 @@ using Task = System.Threading.Tasks.Task; namespace Duplicati.Proprietary.GoogleWorkspace.SourceItems; internal class TaskListSourceEntry(string parentPath, TaskList taskList, TasksService tasksService) - : MetaEntryBase(Util.AppendDirSeparator(SystemIO.IO_OS.PathCombine(parentPath, taskList.Title)), null, null) + : MetaEntryBase(Util.AppendDirSeparator(SystemIO.IO_OS.PathCombine(parentPath, taskList.Id)), null, null) { public override async IAsyncEnumerable Enumerate([EnumeratorCancellation] CancellationToken cancellationToken) { @@ -43,7 +43,10 @@ internal class TaskListSourceEntry(string parentPath, TaskList taskList, TasksSe { "gsuite:Type", SourceItemType.TaskList.ToString() }, { "gsuite:Name", taskList.Title }, { "gsuite:Id", taskList.Id }, - { "gsuite:Updated", taskList.Updated } + { "gsuite:Updated", taskList.Updated }, + { "gsuite:ETag", taskList.ETag }, + { "gsuite:Kind", taskList.Kind }, + { "gsuite:SelfLink", taskList.SelfLink } } .Where(kv => !string.IsNullOrEmpty(kv.Value)) .ToDictionary(kv => kv.Key, kv => kv.Value));