// Copyright (c) 2026 Duplicati Inc. All rights reserved. using System.Collections.Concurrent; 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 OnenoteApiImpl OnenoteApi => new OnenoteApiImpl(_apiHelper); internal class OnenoteApiImpl(APIHelper provider) { internal Task CreateUserNotebookAsync(string userIdOrUpn, string displayName, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var url = $"{baseUrl}/v1.0/users/{user}/onenote/notebooks"; var content = new StringContent(JsonSerializer.Serialize(new { displayName }), Encoding.UTF8, "application/json"); return provider.PostGraphItemAsync(url, content, ct); } internal Task CreateGroupNotebookAsync(string groupId, string displayName, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var group = Uri.EscapeDataString(groupId); var url = $"{baseUrl}/v1.0/groups/{group}/onenote/notebooks"; var content = new StringContent(JsonSerializer.Serialize(new { displayName }), Encoding.UTF8, "application/json"); return provider.PostGraphItemAsync(url, content, ct); } internal Task CreateSectionAsync(string parentId, string displayName, bool isNotebookParent, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var parent = Uri.EscapeDataString(parentId); var url = isNotebookParent ? $"{baseUrl}/v1.0/onenote/notebooks/{parent}/sections" : $"{baseUrl}/v1.0/onenote/sectionGroups/{parent}/sections"; var content = new StringContent(JsonSerializer.Serialize(new { displayName }), Encoding.UTF8, "application/json"); return provider.PostGraphItemAsync(url, content, ct); } internal Task CreateSectionGroupAsync(string parentId, string displayName, bool isNotebookParent, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var parent = Uri.EscapeDataString(parentId); var url = isNotebookParent ? $"{baseUrl}/v1.0/onenote/notebooks/{parent}/sectionGroups" : $"{baseUrl}/v1.0/onenote/sectionGroups/{parent}/sectionGroups"; var content = new StringContent(JsonSerializer.Serialize(new { displayName }), Encoding.UTF8, "application/json"); return provider.PostGraphItemAsync(url, content, ct); } internal Task CreatePageAsync(string sectionId, Stream contentStream, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var section = Uri.EscapeDataString(sectionId); var url = $"{baseUrl}/v1.0/onenote/sections/{section}/pages"; return provider.PostGraphItemStreamAsync(url, contentStream, "text/html", ct); } internal IAsyncEnumerable GetUserNotebooks(string userIdOrUpn, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var user = Uri.EscapeDataString(userIdOrUpn); var url = $"{baseUrl}/v1.0/users/{user}/onenote/notebooks"; return provider.GetAllGraphItemsAsync(url, ct); } internal IAsyncEnumerable GetGroupNotebooks(string groupId, CancellationToken ct) { var baseUrl = provider.GraphBaseUrl.TrimEnd('/'); var group = Uri.EscapeDataString(groupId); var url = $"{baseUrl}/v1.0/groups/{group}/onenote/notebooks"; return provider.GetAllGraphItemsAsync(url, ct); } } private NotebookRestoreHelper? _notebookRestoreHelper = null; internal NotebookRestoreHelper NotebookRestore => _notebookRestoreHelper ??= new NotebookRestoreHelper(this); internal class NotebookRestoreHelper(RestoreProvider Provider) { private string? _targetUserId = null; private string? _targetGroupId = null; private bool _hasLoadedTarget = false; private ConcurrentDictionary _targetNotebookIds = new(); public async Task GetTargetNotebookId(string originalPath, string displayName, CancellationToken cancel) { if (_targetNotebookIds.TryGetValue(originalPath, out var notebookId)) return notebookId; if (!_hasLoadedTarget) { _hasLoadedTarget = true; var target = Provider.RestoreTarget; if (target == null) throw new InvalidOperationException("Restore target is not set"); if (target.Type == SourceItemType.User) { _targetUserId = target.Metadata.GetValueOrDefault("o365:Id"); } else if (target.Type == SourceItemType.Group || target.Type == SourceItemType.GroupTeams) { _targetGroupId = target.Metadata.GetValueOrDefault("o365:Id"); } else if (target.Type == SourceItemType.Notebook) { var targetId = target.Metadata.GetValueOrDefault("o365:Id"); if (!string.IsNullOrWhiteSpace(targetId)) { return targetId; } } } if (!string.IsNullOrWhiteSpace(_targetUserId)) { // Check if notebook exists if (!Provider._ignoreExisting) { await EnsureExistingNotebooksLoaded(_targetUserId, null, cancel); if (_existingNotebooks.TryGetValue(displayName, out var existingId)) { _targetNotebookIds[originalPath] = existingId; return existingId; } } // Create new var newNotebook = await Provider.OnenoteApi.CreateUserNotebookAsync(_targetUserId, displayName, cancel); _targetNotebookIds[originalPath] = newNotebook.Id; return newNotebook.Id; } else if (!string.IsNullOrWhiteSpace(_targetGroupId)) { // Check if notebook exists if (!Provider._ignoreExisting) { await EnsureExistingNotebooksLoaded(null, _targetGroupId, cancel); if (_existingNotebooks.TryGetValue(displayName, out var existingId)) { _targetNotebookIds[originalPath] = existingId; return existingId; } } // Create new var newNotebook = await Provider.OnenoteApi.CreateGroupNotebookAsync(_targetGroupId, displayName, cancel); _targetNotebookIds[originalPath] = newNotebook.Id; return newNotebook.Id; } return null; } private Dictionary _existingNotebooks = new(StringComparer.OrdinalIgnoreCase); private bool _hasLoadedExistingNotebooks = false; private async Task EnsureExistingNotebooksLoaded(string? userId, string? groupId, CancellationToken cancel) { if (_hasLoadedExistingNotebooks) return; _hasLoadedExistingNotebooks = true; IAsyncEnumerable? notebooks = null; if (userId != null) notebooks = Provider.OnenoteApi.GetUserNotebooks(userId, cancel); else if (groupId != null) notebooks = Provider.OnenoteApi.GetGroupNotebooks(groupId, cancel); if (notebooks != null) { await foreach (var notebook in notebooks) { if (!string.IsNullOrWhiteSpace(notebook.DisplayName) && !string.IsNullOrWhiteSpace(notebook.Id)) { _existingNotebooks.TryAdd(notebook.DisplayName, notebook.Id); } } } } } private async Task RestoreNotebooks(CancellationToken cancel) { if (RestoreTarget == null) throw new InvalidOperationException("Restore target entry is not set"); var notebooks = GetMetadataByType(SourceItemType.Notebook); if (notebooks.Count == 0) return; foreach (var notebook in notebooks) { if (cancel.IsCancellationRequested) break; try { var originalPath = notebook.Key; var metadata = notebook.Value; var displayName = metadata.GetValueOrDefault("o365:Name") ?? metadata.GetValueOrDefault("o365:DisplayName") ?? "Restored Notebook"; var notebookId = await NotebookRestore.GetTargetNotebookId(originalPath, displayName, cancel); if (notebookId != null) { _restoredNotebookMap[originalPath] = notebookId; _metadata.TryRemove(originalPath, out _); } else { Log.WriteWarningMessage(LOGTAG, "RestoreNotebooksMissingTarget", null, $"Could not determine target for notebook {notebook.Key}"); } } catch (Exception ex) { Log.WriteErrorMessage(LOGTAG, "RestoreNotebooksFailed", ex, $"Failed to restore notebook {notebook.Key}"); } } } private async Task RestoreNotebookSectionGroups(CancellationToken cancel) { var sectionGroups = GetMetadataByType(SourceItemType.NotebookSectionGroup); if (sectionGroups.Count == 0) return; // Sort by path length to ensure parents are created first var sortedGroups = sectionGroups.OrderBy(k => k.Key.Split(Path.DirectorySeparatorChar).Length).ToList(); foreach (var group in sortedGroups) { if (cancel.IsCancellationRequested) break; try { var originalPath = group.Key; var metadata = group.Value; var displayName = metadata.GetValueOrDefault("o365:Name") ?? metadata.GetValueOrDefault("o365:DisplayName") ?? "Restored Section Group"; // Find parent var parentPath = Util.AppendDirSeparator(Path.GetDirectoryName(originalPath.TrimEnd(Path.DirectorySeparatorChar)) ?? ""); string? parentId = null; bool isNotebookParent = false; if (parentPath != null) { if (_restoredNotebookMap.TryGetValue(parentPath, out var notebookId)) { parentId = notebookId; isNotebookParent = true; } else if (_restoredSectionGroupMap.TryGetValue(parentPath, out var groupId)) { parentId = groupId; isNotebookParent = false; } } if (parentId == null) { Log.WriteWarningMessage(LOGTAG, "RestoreNotebookSectionGroupsMissingParent", null, $"Could not find parent for section group {originalPath}, skipping."); continue; } var newGroup = await OnenoteApi.CreateSectionGroupAsync(parentId, displayName, isNotebookParent, cancel); _restoredSectionGroupMap[originalPath] = newGroup.Id; _metadata.TryRemove(originalPath, out _); } catch (Exception ex) { Log.WriteErrorMessage(LOGTAG, "RestoreNotebookSectionGroupsFailed", ex, $"Failed to restore section group {group.Key}"); } } } private async Task RestoreNotebookSections(CancellationToken cancel) { var sections = GetMetadataByType(SourceItemType.NotebookSection); if (sections.Count == 0) return; foreach (var section in sections) { if (cancel.IsCancellationRequested) break; try { var originalPath = section.Key; var metadata = section.Value; var displayName = metadata.GetValueOrDefault("o365:Name") ?? metadata.GetValueOrDefault("o365:DisplayName") ?? "Restored Section"; // Find parent var parentPath = Util.AppendDirSeparator(Path.GetDirectoryName(originalPath.TrimEnd(Path.DirectorySeparatorChar)) ?? ""); string? parentId = null; bool isNotebookParent = false; if (parentPath != null) { if (_restoredNotebookMap.TryGetValue(parentPath, out var notebookId)) { parentId = notebookId; isNotebookParent = true; } else if (_restoredSectionGroupMap.TryGetValue(parentPath, out var groupId)) { parentId = groupId; isNotebookParent = false; } } if (parentId == null) { Log.WriteWarningMessage(LOGTAG, "RestoreNotebookSectionsMissingParent", null, $"Could not find parent for section {originalPath}, skipping."); continue; } var newSection = await OnenoteApi.CreateSectionAsync(parentId, displayName, isNotebookParent, cancel); _restoredSectionMap[originalPath] = newSection.Id; _metadata.TryRemove(originalPath, out _); } catch (Exception ex) { Log.WriteErrorMessage(LOGTAG, "RestoreNotebookSectionsFailed", ex, $"Failed to restore section {section.Key}"); } } } private async Task RestoreNotebookPages(CancellationToken cancel) { // Iterate over temporary files to find pages // Pages are .html files whose parent is a restored section var files = _temporaryFiles.Keys.ToList(); foreach (var filePath in files) { if (cancel.IsCancellationRequested) break; if (!filePath.EndsWith(".html", StringComparison.OrdinalIgnoreCase)) continue; var parentPath = Util.AppendDirSeparator(Path.GetDirectoryName(filePath.TrimEnd(Path.DirectorySeparatorChar)) ?? ""); if (parentPath == null) continue; if (_restoredSectionMap.TryGetValue(parentPath, out var sectionId)) { try { var contentEntry = _temporaryFiles[filePath]; using (var contentStream = SystemIO.IO_OS.FileOpenRead(contentEntry)) { await OnenoteApi.CreatePageAsync(sectionId, contentStream, cancel); } _temporaryFiles.TryRemove(filePath, out var file); file?.Dispose(); _metadata.TryRemove(filePath, out _); } catch (Exception ex) { Log.WriteErrorMessage(LOGTAG, "RestoreNotebookPagesFailed", ex, $"Failed to restore page {filePath}"); } } } } }