Files
duplicati/proprietary/Office365/RestoreProvider/RestoreProvider.Tasks.cs
T

328 lines
14 KiB
C#

// Copyright (c) 2026 Duplicati Inc. All rights reserved.
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using Duplicati.Library.Common.IO;
using Duplicati.Library.Logging;
using Duplicati.Proprietary.Office365.SourceItems;
namespace Duplicati.Proprietary.Office365;
public partial class RestoreProvider
{
internal TodoApiImpl TodoApi => new TodoApiImpl(_apiHelper);
internal class TodoApiImpl(APIHelper provider)
{
internal Task<GraphTodoTaskList> CreateTaskListAsync(string userIdOrUpn, string displayName, CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var user = Uri.EscapeDataString(userIdOrUpn);
var url = $"{baseUrl}/v1.0/users/{user}/todo/lists";
var content = new StringContent(JsonSerializer.Serialize(new { displayName }), Encoding.UTF8, "application/json");
return provider.PostGraphItemAsync<GraphTodoTaskList>(url, content, ct);
}
internal Task<GraphTodoTask> CreateTaskAsync(string userIdOrUpn, string taskListId, GraphTodoTask task, CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var user = Uri.EscapeDataString(userIdOrUpn);
var list = Uri.EscapeDataString(taskListId);
var url = $"{baseUrl}/v1.0/users/{user}/todo/lists/{list}/tasks";
// Ensure ID is null to avoid errors
task.Id = "";
var content = new StringContent(JsonSerializer.Serialize(task), Encoding.UTF8, "application/json");
return provider.PostGraphItemAsync<GraphTodoTask>(url, content, ct);
}
internal Task<GraphTodoChecklistItem> CreateChecklistItemAsync(string userIdOrUpn, string taskListId, string taskId, string displayName, bool isChecked, CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var user = Uri.EscapeDataString(userIdOrUpn);
var list = Uri.EscapeDataString(taskListId);
var task = Uri.EscapeDataString(taskId);
var url = $"{baseUrl}/v1.0/users/{user}/todo/lists/{list}/tasks/{task}/checklistItems";
var content = new StringContent(JsonSerializer.Serialize(new { displayName, isChecked }), Encoding.UTF8, "application/json");
return provider.PostGraphItemAsync<GraphTodoChecklistItem>(url, content, ct);
}
internal Task<GraphTodoLinkedResource> CreateLinkedResourceAsync(string userIdOrUpn, string taskListId, string taskId, GraphTodoLinkedResource resource, CancellationToken ct)
{
var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
var user = Uri.EscapeDataString(userIdOrUpn);
var list = Uri.EscapeDataString(taskListId);
var task = Uri.EscapeDataString(taskId);
var url = $"{baseUrl}/v1.0/users/{user}/todo/lists/{list}/tasks/{task}/linkedResources";
// Ensure ID is null
resource.Id = "";
var content = new StringContent(JsonSerializer.Serialize(resource), Encoding.UTF8, "application/json");
return provider.PostGraphItemAsync<GraphTodoLinkedResource>(url, content, ct);
}
}
private async Task RestoreTaskLists(CancellationToken cancel)
{
if (RestoreTarget == null)
throw new InvalidOperationException("Restore target entry is not set");
var taskLists = GetMetadataByType(SourceItemType.TaskList);
if (taskLists.Count == 0)
return;
string? targetUserId = null;
if (RestoreTarget.Type == SourceItemType.User)
{
targetUserId = RestoreTarget.Metadata.GetValueOrDefault("o365:Id");
}
if (string.IsNullOrWhiteSpace(targetUserId))
{
Log.WriteWarningMessage(LOGTAG, "RestoreTaskListsMissingUser", null, "Could not determine target user for task list restore.");
return;
}
foreach (var taskList in taskLists)
{
if (cancel.IsCancellationRequested)
break;
try
{
var originalPath = taskList.Key;
var metadata = taskList.Value;
var displayName = metadata.GetValueOrDefault("o365:Name") ?? metadata.GetValueOrDefault("o365:DisplayName") ?? "Restored Task List";
var newList = await TodoApi.CreateTaskListAsync(targetUserId, displayName, cancel);
_restoredTaskListMap[originalPath] = newList.Id;
_metadata.TryRemove(originalPath, out _);
}
catch (Exception ex)
{
Log.WriteErrorMessage(LOGTAG, "RestoreTaskListsFailed", ex, $"Failed to restore task list {taskList.Key}");
}
}
}
private async Task RestoreTaskListTasks(CancellationToken cancel)
{
var tasks = GetMetadataByType(SourceItemType.TaskListTask);
if (tasks.Count == 0)
return;
string? targetUserId = null;
if (RestoreTarget?.Type == SourceItemType.User)
{
targetUserId = RestoreTarget.Metadata.GetValueOrDefault("o365:Id");
}
if (string.IsNullOrWhiteSpace(targetUserId))
{
Log.WriteWarningMessage(LOGTAG, "RestoreTaskListTasksMissingUser", null, "Could not determine target user for task restore.");
return;
}
foreach (var task in tasks)
{
if (cancel.IsCancellationRequested)
break;
try
{
var originalPath = task.Key;
// Find parent list
var parentPath = Util.AppendDirSeparator(Path.GetDirectoryName(originalPath.TrimEnd(Path.DirectorySeparatorChar)) ?? "");
if (parentPath == null || !_restoredTaskListMap.TryGetValue(parentPath, out var listId))
{
Log.WriteWarningMessage(LOGTAG, "RestoreTaskListTasksMissingParent", null, $"Could not find parent list for task {originalPath}, skipping.");
continue;
}
var contentEntry = _temporaryFiles.GetValueOrDefault(originalPath);
if (contentEntry == null)
{
Log.WriteWarningMessage(LOGTAG, "RestoreTaskListTasksMissingContent", null, $"Missing content for task {originalPath}, skipping.");
continue;
}
GraphTodoTask? taskObj;
using (var contentStream = SystemIO.IO_OS.FileOpenRead(contentEntry))
{
taskObj = await JsonSerializer.DeserializeAsync<GraphTodoTask>(contentStream, cancellationToken: cancel);
}
if (taskObj == null)
{
Log.WriteWarningMessage(LOGTAG, "RestoreTaskListTasksInvalidContent", null, $"Invalid content for task {originalPath}, skipping.");
continue;
}
var newTask = await TodoApi.CreateTaskAsync(targetUserId, listId, taskObj, cancel);
_restoredTaskMap[originalPath] = newTask.Id;
_metadata.TryRemove(originalPath, out _);
_temporaryFiles.TryRemove(originalPath, out var file);
file?.Dispose();
}
catch (Exception ex)
{
Log.WriteErrorMessage(LOGTAG, "RestoreTaskListTasksFailed", ex, $"Failed to restore task {task.Key}");
}
}
}
private async Task RestoreTaskChecklistItems(CancellationToken cancel)
{
var items = GetMetadataByType(SourceItemType.TaskListChecklistItem);
if (items.Count == 0)
return;
string? targetUserId = null;
if (RestoreTarget?.Type == SourceItemType.User)
{
targetUserId = RestoreTarget.Metadata.GetValueOrDefault("o365:Id");
}
if (string.IsNullOrWhiteSpace(targetUserId))
return;
foreach (var item in items)
{
if (cancel.IsCancellationRequested)
break;
try
{
var originalPath = item.Key;
var metadata = item.Value;
var displayName = metadata.GetValueOrDefault("o365:Name") ?? "Checklist Item";
var isChecked = metadata.GetValueOrDefault("o365:IsChecked") == "True";
// Find parent task
// Path: .../Lists/ListId/Tasks/TaskId/ChecklistItems/ItemId
// Parent: .../Lists/ListId/Tasks/TaskId/ChecklistItems
// GrandParent: .../Lists/ListId/Tasks/TaskId
var parentDir = Path.GetDirectoryName(originalPath.TrimEnd(Path.DirectorySeparatorChar)); // ChecklistItems
if (parentDir == null) continue;
var taskPath = Util.AppendDirSeparator(Path.GetDirectoryName(parentDir) ?? ""); // TaskId
if (taskPath == null || !_restoredTaskMap.TryGetValue(taskPath, out var taskId))
{
Log.WriteWarningMessage(LOGTAG, "RestoreTaskChecklistItemsMissingParent", null, $"Could not find parent task for checklist item {originalPath}, skipping.");
continue;
}
// We also need listId.
// Task path: .../Lists/ListId/Tasks/TaskId
// List path: .../Lists/ListId
var tasksDir = Path.GetDirectoryName(taskPath.TrimEnd(Path.DirectorySeparatorChar)); // Tasks
if (tasksDir == null) continue;
var listPath = Util.AppendDirSeparator(Path.GetDirectoryName(tasksDir) ?? ""); // ListId
if (listPath == null || !_restoredTaskListMap.TryGetValue(listPath, out var listId))
{
Log.WriteWarningMessage(LOGTAG, "RestoreTaskChecklistItemsMissingList", null, $"Could not find parent list for checklist item {originalPath}, skipping.");
continue;
}
await TodoApi.CreateChecklistItemAsync(targetUserId, listId, taskId, displayName, isChecked, cancel);
_metadata.TryRemove(originalPath, out _);
}
catch (Exception ex)
{
Log.WriteErrorMessage(LOGTAG, "RestoreTaskChecklistItemsFailed", ex, $"Failed to restore checklist item {item.Key}");
}
}
}
private async Task RestoreTaskLinkedResources(CancellationToken cancel)
{
var resources = GetMetadataByType(SourceItemType.TaskListLinkedResource);
if (resources.Count == 0)
return;
string? targetUserId = null;
if (RestoreTarget?.Type == SourceItemType.User)
{
targetUserId = RestoreTarget.Metadata.GetValueOrDefault("o365:Id");
}
if (string.IsNullOrWhiteSpace(targetUserId))
return;
foreach (var resource in resources)
{
if (cancel.IsCancellationRequested)
break;
try
{
var originalPath = resource.Key;
// Find parent task
var parentDir = Path.GetDirectoryName(originalPath.TrimEnd(Path.DirectorySeparatorChar)); // LinkedResources
if (parentDir == null) continue;
var taskPath = Util.AppendDirSeparator(Path.GetDirectoryName(parentDir) ?? ""); // TaskId
if (taskPath == null || !_restoredTaskMap.TryGetValue(taskPath, out var taskId))
{
Log.WriteWarningMessage(LOGTAG, "RestoreTaskLinkedResourcesMissingParent", null, $"Could not find parent task for linked resource {originalPath}, skipping.");
continue;
}
// We also need listId.
var tasksDir = Path.GetDirectoryName(taskPath.TrimEnd(Path.DirectorySeparatorChar)); // Tasks
if (tasksDir == null) continue;
var listPath = Util.AppendDirSeparator(Path.GetDirectoryName(tasksDir) ?? ""); // ListId
if (listPath == null || !_restoredTaskListMap.TryGetValue(listPath, out var listId))
{
Log.WriteWarningMessage(LOGTAG, "RestoreTaskLinkedResourcesMissingList", null, $"Could not find parent list for linked resource {originalPath}, skipping.");
continue;
}
var contentEntry = _temporaryFiles.GetValueOrDefault(originalPath);
if (contentEntry == null)
{
Log.WriteWarningMessage(LOGTAG, "RestoreTaskLinkedResourcesMissingContent", null, $"Missing content for linked resource {originalPath}, skipping.");
continue;
}
GraphTodoLinkedResource? resourceObj;
using (var contentStream = SystemIO.IO_OS.FileOpenRead(contentEntry))
{
resourceObj = await JsonSerializer.DeserializeAsync<GraphTodoLinkedResource>(contentStream, cancellationToken: cancel);
}
if (resourceObj == null)
{
Log.WriteWarningMessage(LOGTAG, "RestoreTaskLinkedResourcesInvalidContent", null, $"Invalid content for linked resource {originalPath}, skipping.");
continue;
}
await TodoApi.CreateLinkedResourceAsync(targetUserId, listId, taskId, resourceObj, cancel);
_metadata.TryRemove(originalPath, out _);
_temporaryFiles.TryRemove(originalPath, out var file);
file?.Dispose();
}
catch (Exception ex)
{
Log.WriteErrorMessage(LOGTAG, "RestoreTaskLinkedResourcesFailed", ex, $"Failed to restore linked resource {resource.Key}");
}
}
}
}