From f61978586a3ce83acee5cb9aa5faac5b09a089ac Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sun, 8 Mar 2026 20:39:25 -0400 Subject: [PATCH] Refactored item file attachments support. --- NEXT_RELEASE.md | 1 + .../OnePasswordManagerCommandTests.cs | 31 ++++ OnePassword.NET.Tests/TestItems.cs | 71 +++++++- OnePassword.NET/IOnePasswordManager.Items.cs | 34 ++++ OnePassword.NET/Items/File.cs | 45 ----- OnePassword.NET/Items/FileAttachment.cs | 56 ++++++ OnePassword.NET/Items/ItemBase.cs | 8 +- OnePassword.NET/OnePasswordManager.Items.cs | 163 +++++++++++++++++- README.md | 29 ++++ docfx/docs/quick-start.md | 29 ++++ 10 files changed, 409 insertions(+), 58 deletions(-) delete mode 100644 OnePassword.NET/Items/File.cs create mode 100644 OnePassword.NET/Items/FileAttachment.cs diff --git a/NEXT_RELEASE.md b/NEXT_RELEASE.md index bfdd6bf..f157690 100644 --- a/NEXT_RELEASE.md +++ b/NEXT_RELEASE.md @@ -10,6 +10,7 @@ - Redesigned item sharing so unrestricted links are supported and the created share URL is returned to the caller. - Trimmed the CLI version string returned on Windows so `Version` no longer includes a trailing newline. - Clarified in the docs that `GetItems(...)` returns summary items and `GetItem(...)` should be used before working with hydrated fields. +- Added `FileAttachments` support so item attachments can be created, listed, removed, and read through the wrapper. - Added regression coverage for archive behavior, item sharing, version handling, and adding a new field to an existing built-in item. ## Migration diff --git a/OnePassword.NET.Tests/OnePasswordManagerCommandTests.cs b/OnePassword.NET.Tests/OnePasswordManagerCommandTests.cs index 3a86f41..e96fd98 100644 --- a/OnePassword.NET.Tests/OnePasswordManagerCommandTests.cs +++ b/OnePassword.NET.Tests/OnePasswordManagerCommandTests.cs @@ -66,6 +66,37 @@ public void ArchiveItemStringOverloadUsesArchiveCommand() Assert.That(fakeCli.LastArguments, Does.StartWith("item delete item-id --vault vault-id --archive")); } + [Test] + public void GetFileAttachmentReferenceUsesIds() + { + using var fakeCli = new FakeCli(); + var manager = fakeCli.CreateManager(); + + var reference = manager.GetFileAttachmentReference("file-id", "item-id", "vault-id"); + + Assert.That(reference, Is.EqualTo("op://vault-id/item-id/file-id?attr=content")); + } + + [Test] + public void SaveFileAttachmentContentUsesGeneratedReference() + { + using var fakeCli = new FakeCli(); + var manager = fakeCli.CreateManager(); + var outputPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + try + { + manager.SaveFileAttachmentContent("file-id", "item-id", "vault-id", outputPath); + + Assert.That(fakeCli.LastArguments, Does.StartWith($"read op://vault-id/item-id/file-id?attr=content --no-newline --force --out-file \"{outputPath}\"")); + } + finally + { + if (File.Exists(outputPath)) + File.Delete(outputPath); + } + } + [Test] public void ShareItemWithoutEmailsOmitsEmailsFlag() { diff --git a/OnePassword.NET.Tests/TestItems.cs b/OnePassword.NET.Tests/TestItems.cs index cf84273..76bdfc2 100644 --- a/OnePassword.NET.Tests/TestItems.cs +++ b/OnePassword.NET.Tests/TestItems.cs @@ -9,6 +9,8 @@ public class TestItems : TestsBase { private const string InitialTitle = "Created Item"; private const string InitialUsername = "Created Username"; + private const string InitialAttachmentName = "Initial Attachment"; + private const string InitialAttachmentContent = "Initial attachment content"; private const string EditSection = "Edit Section"; private const string EditField = "Edit Field"; private const string DeleteSection = "Delete Section"; @@ -23,8 +25,13 @@ public class TestItems : TestsBase private const string FinalValue = "Test Value"; private const string AddedField = "Added Field"; private const string AddedValue = "Added Value"; + private const string AddedAttachmentName = "Added Attachment"; + private const string AddedAttachmentContent = "Added attachment content"; private const string Tag1 = "Tag1"; private const string Tag2 = "Tag2"; + private readonly string _addedAttachmentFilePath = Path.Combine(WorkingDirectory, "AddedAttachment.txt"); + private readonly string _addedAttachmentOutputFilePath = Path.Combine(WorkingDirectory, "AddedAttachment.out.txt"); + private readonly string _initialAttachmentFilePath = Path.Combine(WorkingDirectory, "InitialAttachment.txt"); [Test] [Order(1)] @@ -40,6 +47,8 @@ public void CreateItem() template.Fields.First(x => x.Label == "username").Value = InitialUsername; template.Tags.Add(Tag1); template.Tags.Add(Tag2); + File.WriteAllText(_initialAttachmentFilePath, InitialAttachmentContent); + template.FileAttachments.Add(new FileAttachment(_initialAttachmentFilePath, InitialAttachmentName)); var editSection = new Section(EditSection); template.Sections.Add(editSection); @@ -60,6 +69,9 @@ public void CreateItem() Assert.That(_initialItem.Fields.First(x => x.Section?.Label == EditSection && x.Label == EditField).Type, Is.EqualTo(InitialType)); Assert.That(_initialItem.Fields.First(x => x.Section?.Label == EditSection && x.Label == EditField).Value, Is.EqualTo(InitialValue)); Assert.That(_initialItem.Fields.First(x => x.Section?.Label == DeleteSection && x.Label == DeleteField).Value, Is.EqualTo(DeleteValue)); + Assert.That(_initialItem.FileAttachments.Any(x => x.Name == InitialAttachmentName), Is.True); + Assert.That(_initialItem.FileAttachments.First(x => x.Name == InitialAttachmentName).ContentPath, Is.Not.Empty); + Assert.That(_initialItem.FileAttachments.First(x => x.Name == InitialAttachmentName).Size, Is.GreaterThan(0)); Assert.That(_initialItem.Tags, Does.Contain(Tag1)); Assert.That(_initialItem.Tags, Does.Contain(Tag2)); }); @@ -93,6 +105,7 @@ public void EditItem() Assert.That(item.Fields.First(x => x.Section?.Label == EditSection && x.Label == EditField).Type, Is.EqualTo(FinalType)); Assert.That(item.Fields.First(x => x.Section?.Label == EditSection && x.Label == EditField).Value, Is.EqualTo(FinalValue)); Assert.That(item.Fields.FirstOrDefault(x => x.Section?.Label == DeleteSection && x.Label == DeleteField), Is.Null); + Assert.That(item.FileAttachments.Any(x => x.Name == InitialAttachmentName), Is.True); Assert.That(item.Tags, Does.Contain(Tag1)); Assert.That(item.Tags, Does.Not.Contain(Tag2)); }); @@ -119,6 +132,56 @@ public void EditItemAddsNewField() [Test] [Order(4)] + public void EditItemAddsFileAttachment() + { + if (!RunLiveTests) + Assert.Ignore(); + + Run(RunType.Test, () => + { + File.WriteAllText(_addedAttachmentFilePath, AddedAttachmentContent); + + var item = OnePassword.GetItem(_initialItem, TestVault); + item.FileAttachments.Add(new FileAttachment(_addedAttachmentFilePath, AddedAttachmentName)); + + var editedItem = OnePassword.EditItem(item, TestVault); + + Assert.Multiple(() => + { + Assert.That(editedItem.FileAttachments.Any(x => x.Name == InitialAttachmentName), Is.True); + Assert.That(editedItem.FileAttachments.Any(x => x.Name == AddedAttachmentName), Is.True); + }); + + _initialItem = editedItem; + }); + } + + [Test] + [Order(5)] + public void EditItemRemovesFileAttachment() + { + if (!RunLiveTests) + Assert.Ignore(); + + Run(RunType.Test, () => + { + var item = OnePassword.GetItem(_initialItem, TestVault); + item.FileAttachments.Remove(item.FileAttachments.First(x => x.Name == InitialAttachmentName)); + + var editedItem = OnePassword.EditItem(item, TestVault); + + Assert.Multiple(() => + { + Assert.That(editedItem.FileAttachments.Any(x => x.Name == InitialAttachmentName), Is.False); + Assert.That(editedItem.FileAttachments.Any(x => x.Name == AddedAttachmentName), Is.True); + }); + + _initialItem = editedItem; + }); + } + + [Test] + [Order(6)] public void GetItems() { if (!RunLiveTests) @@ -143,7 +206,7 @@ public void GetItems() } [Test] - [Order(5)] + [Order(7)] public void GetItem() { if (!RunLiveTests) @@ -162,7 +225,13 @@ public void GetItem() Assert.That(item.Fields.First(x => x.Section?.Label == EditSection && x.Label == EditField).Value, Is.EqualTo(FinalValue)); Assert.That(item.Fields.First(x => x.Label == AddedField).Value, Is.EqualTo(AddedValue)); Assert.That(item.Fields.FirstOrDefault(x => x.Section?.Label == DeleteSection && x.Label == DeleteField), Is.Null); + Assert.That(item.FileAttachments.Any(x => x.Name == InitialAttachmentName), Is.False); + Assert.That(item.FileAttachments.Any(x => x.Name == AddedAttachmentName), Is.True); }); + + var attachment = item.FileAttachments.First(x => x.Name == AddedAttachmentName); + OnePassword.SaveFileAttachmentContent(attachment, item, TestVault, _addedAttachmentOutputFilePath); + Assert.That(File.ReadAllText(_addedAttachmentOutputFilePath), Is.EqualTo(AddedAttachmentContent)); }); } } diff --git a/OnePassword.NET/IOnePasswordManager.Items.cs b/OnePassword.NET/IOnePasswordManager.Items.cs index 7068407..8a02097 100644 --- a/OnePassword.NET/IOnePasswordManager.Items.cs +++ b/OnePassword.NET/IOnePasswordManager.Items.cs @@ -107,6 +107,40 @@ public ImmutableList SearchForItems(string? vaultId = null, bool? includeA /// Thrown when there is an invalid argument. public Item EditItem(Item item, string vaultId); + /// Builds a secret reference for the specified file attachment's content. + /// The file attachment. + /// The item that contains the file attachment. + /// The vault that contains the item. + /// The secret reference for the file attachment content. + /// Thrown when there is an invalid argument. + public string GetFileAttachmentReference(FileAttachment fileAttachment, IItem item, IVault vault); + + /// Builds a secret reference for the specified file attachment's content. + /// The file attachment ID. + /// The item ID. + /// The vault ID. + /// The secret reference for the file attachment content. + /// Thrown when there is an invalid argument. + public string GetFileAttachmentReference(string fileAttachmentId, string itemId, string vaultId); + + /// Saves a file attachment's content to disk. + /// The file attachment. + /// The item that contains the file attachment. + /// The vault that contains the item. + /// The output file path. + /// The file mode to use when creating the file. + /// Thrown when there is an invalid argument. + public void SaveFileAttachmentContent(FileAttachment fileAttachment, IItem item, IVault vault, string filePath, string? fileMode = null); + + /// Saves a file attachment's content to disk. + /// The file attachment ID. + /// The item ID. + /// The vault ID. + /// The output file path. + /// The file mode to use when creating the file. + /// Thrown when there is an invalid argument. + public void SaveFileAttachmentContent(string fileAttachmentId, string itemId, string vaultId, string filePath, string? fileMode = null); + /// Archives an item. /// The item to archive. /// The vault that contains the item to archive. diff --git a/OnePassword.NET/Items/File.cs b/OnePassword.NET/Items/File.cs deleted file mode 100644 index c6ff09e..0000000 --- a/OnePassword.NET/Items/File.cs +++ /dev/null @@ -1,45 +0,0 @@ -using OnePassword.Common; - -namespace OnePassword.Items; - -/// Represents a file object. -public sealed class File : ITracked -{ - /// The file section. - [JsonInclude] - [JsonPropertyName("section")] - public Section? Section { get; internal set; } - - /// The file ID. - [JsonInclude] - [JsonPropertyName("id")] - public string Id { get; internal set; } = ""; - - /// The name of file. - [JsonInclude] - [JsonPropertyName("name")] - public string Name { get; internal set; } = ""; - - /// The file size. - [JsonInclude] - [JsonPropertyName("size")] - public int Size { get; internal set; } - - /// The content path. - [JsonInclude] - [JsonPropertyName("content_path")] - public string ContentPath { get; internal set; } = ""; - - /// - public bool Changed => false; - - /// Initializes a new instance of . - public File() - { - } - - /// - void ITracked.AcceptChanges() - { - } -} diff --git a/OnePassword.NET/Items/FileAttachment.cs b/OnePassword.NET/Items/FileAttachment.cs new file mode 100644 index 0000000..22bf7e3 --- /dev/null +++ b/OnePassword.NET/Items/FileAttachment.cs @@ -0,0 +1,56 @@ +using OnePassword.Common; + +namespace OnePassword.Items; + +/// Represents a file attachment associated with an item. +public sealed class FileAttachment : ITracked +{ + /// The section where the attachment is stored. + [JsonInclude] + [JsonPropertyName("section")] + public Section? Section { get; internal set; } + + /// The attachment ID. + [JsonInclude] + [JsonPropertyName("id")] + public string Id { get; internal set; } = ""; + + /// The attachment name. + [JsonInclude] + [JsonPropertyName("name")] + public string Name { get; internal set; } = ""; + + /// The attachment size in bytes. + [JsonInclude] + [JsonPropertyName("size")] + public int Size { get; internal set; } + + /// The path used to access the attachment content. + [JsonInclude] + [JsonPropertyName("content_path")] + public string ContentPath { get; internal set; } = ""; + + /// + public bool Changed => false; + + /// Initializes a new instance of . + public FileAttachment() + { + } + + /// Initializes a new instance of to attach a local file when creating or editing an item. + /// The local file path to attach. + /// The attachment name. Leave empty to preserve the source file name. + /// The section where the attachment should be added. + public FileAttachment(string filePath, string? name = null, Section? section = null) + { + ContentPath = filePath ?? ""; + Name = name ?? ""; + Section = section; + } + + /// + void ITracked.AcceptChanges() + { + } +} diff --git a/OnePassword.NET/Items/ItemBase.cs b/OnePassword.NET/Items/ItemBase.cs index 4d32b67..493fe30 100644 --- a/OnePassword.NET/Items/ItemBase.cs +++ b/OnePassword.NET/Items/ItemBase.cs @@ -76,11 +76,11 @@ public string CategoryId public TrackedList Tags { get; internal set; } = []; /// - /// The files associated with the item. + /// The file attachments associated with the item. /// [JsonInclude] [JsonPropertyName("files")] - public TrackedList Files { get; internal set; } = []; + public TrackedList FileAttachments { get; internal set; } = []; /// /// Returns when the title has changed, otherwise. @@ -98,7 +98,8 @@ public string CategoryId || ((ITracked)Sections).Changed || ((ITracked)Fields).Changed || ((ITracked)Urls).Changed - || ((ITracked)Tags).Changed; + || ((ITracked)Tags).Changed + || ((ITracked)FileAttachments).Changed; /// void ITracked.AcceptChanges() @@ -109,5 +110,6 @@ void ITracked.AcceptChanges() ((ITracked)Fields).AcceptChanges(); ((ITracked)Urls).AcceptChanges(); ((ITracked)Tags).AcceptChanges(); + ((ITracked)FileAttachments).AcceptChanges(); } } diff --git a/OnePassword.NET/OnePasswordManager.Items.cs b/OnePassword.NET/OnePasswordManager.Items.cs index 45f8463..b1b2f2f 100644 --- a/OnePassword.NET/OnePasswordManager.Items.cs +++ b/OnePassword.NET/OnePasswordManager.Items.cs @@ -125,9 +125,11 @@ public Item CreateItem(Template template, string vaultId) if (vaultId is null || vaultId.Length == 0) throw new ArgumentException($"{nameof(vaultId)} cannot be empty.", nameof(vaultId)); - var json = JsonSerializer.Serialize(template, JsonContext.Default.Template) + "\x04"; - var command = $"item create --vault {vaultId} -"; + foreach (var assignmentStatement in GetFileAttachmentAssignmentStatements(template.Fields, template.FileAttachments)) + command += $" {QuoteArgument(assignmentStatement)}"; + + var json = SerializeTemplateForItemCommand(template); ((ITracked)template).AcceptChanges(); if (template.TitleChanged) command += $" --title \"{template.Title}\""; @@ -155,14 +157,12 @@ public Item EditItem(Item item, string vaultId) if (vaultId is null || vaultId.Length == 0) throw new ArgumentException($"{nameof(vaultId)} cannot be empty.", nameof(vaultId)); - var json = JsonSerializer.Serialize(item -#if NET7_0_OR_GREATER - , JsonContext.Default.Item -#endif - ) - + "\x04"; - var command = $"item edit {item.Id} --vault {vaultId}"; + foreach (var assignmentStatement in GetFileAttachmentAssignmentStatements(item.Fields, item.FileAttachments) + .Concat(GetDeletedFileAttachmentAssignmentStatements(item.FileAttachments))) + command += $" {QuoteArgument(assignmentStatement)}"; + + var json = SerializeItemForEditCommand(item); if (item.TitleChanged) command += $" --title \"{item.Title}\""; if (((ITracked)item.Tags).Changed) @@ -176,6 +176,46 @@ public Item EditItem(Item item, string vaultId) return Op(JsonContext.Default.Item, command, json); } + /// + public string GetFileAttachmentReference(FileAttachment fileAttachment, IItem item, IVault vault) + { + if (fileAttachment is null || fileAttachment.Id.Length == 0) + throw new ArgumentException($"{nameof(fileAttachment.Id)} cannot be empty.", nameof(fileAttachment)); + if (item is null || item.Id.Length == 0) + throw new ArgumentException($"{nameof(item.Id)} cannot be empty.", nameof(item)); + if (vault is null || vault.Id.Length == 0) + throw new ArgumentException($"{nameof(vault.Id)} cannot be empty.", nameof(vault)); + + return GetFileAttachmentReference(fileAttachment.Id, item.Id, vault.Id); + } + + /// + public string GetFileAttachmentReference(string fileAttachmentId, string itemId, string vaultId) + { + if (fileAttachmentId is null || fileAttachmentId.Length == 0) + throw new ArgumentException($"{nameof(fileAttachmentId)} cannot be empty.", nameof(fileAttachmentId)); + if (itemId is null || itemId.Length == 0) + throw new ArgumentException($"{nameof(itemId)} cannot be empty.", nameof(itemId)); + if (vaultId is null || vaultId.Length == 0) + throw new ArgumentException($"{nameof(vaultId)} cannot be empty.", nameof(vaultId)); + + return $"op://{vaultId}/{itemId}/{fileAttachmentId}?attr=content"; + } + + /// + public void SaveFileAttachmentContent(FileAttachment fileAttachment, IItem item, IVault vault, string filePath, string? fileMode = null) + { + var reference = GetFileAttachmentReference(fileAttachment, item, vault); + SaveSecret(reference, filePath, fileMode); + } + + /// + public void SaveFileAttachmentContent(string fileAttachmentId, string itemId, string vaultId, string filePath, string? fileMode = null) + { + var reference = GetFileAttachmentReference(fileAttachmentId, itemId, vaultId); + SaveSecret(reference, filePath, fileMode); + } + /// public void ArchiveItem(IItem item, IVault vault) { @@ -393,6 +433,111 @@ private static ImmutableList NormalizeEmailAddresses(IReadOnlyCollection .Select(static emailAddress => emailAddress.Trim())]; } + private static string SerializeTemplateForItemCommand(Template template) + { + var commandTemplate = template.Clone(); + RemoveFileAttachmentFields(commandTemplate.Fields); + commandTemplate.FileAttachments = []; + return JsonSerializer.Serialize(commandTemplate, JsonContext.Default.Template) + "\x04"; + } + + private static string SerializeItemForEditCommand(Item item) + { + var json = JsonSerializer.Serialize(item +#if NET7_0_OR_GREATER + , JsonContext.Default.Item +#endif + ); + var commandItem = JsonSerializer.Deserialize(json, JsonContext.Default.Item) ?? throw new SerializationException("Could not deserialize the item edit payload."); + RemoveFileAttachmentFields(commandItem.Fields); + commandItem.FileAttachments = []; + + return JsonSerializer.Serialize(commandItem +#if NET7_0_OR_GREATER + , JsonContext.Default.Item +#endif + ) + + "\x04"; + } + + private static void RemoveFileAttachmentFields(TrackedList fields) + { + foreach (var field in fields.Where(static field => field.Type == FieldType.File).ToList()) + fields.Remove(field); + } + + private static IEnumerable GetFileAttachmentAssignmentStatements(IEnumerable fields, IEnumerable fileAttachments) + { + return fields + .Where(static field => field.Type == FieldType.File) + .Select(BuildFileAttachmentAssignmentStatement) + .Concat(fileAttachments + .Where(IsPendingFileAttachment) + .Select(BuildFileAttachmentAssignmentStatement)); + } + + private static IEnumerable GetDeletedFileAttachmentAssignmentStatements(TrackedList fileAttachments) + { + return fileAttachments.Removed.Select(BuildDeleteFileAttachmentAssignmentStatement); + } + + private static bool IsPendingFileAttachment(FileAttachment fileAttachment) + { + return fileAttachment.Id.Length == 0 && fileAttachment.ContentPath.Trim().Length > 0; + } + + private static string BuildFileAttachmentAssignmentStatement(Field field) + { + var trimmedFilePath = field.Value.Trim(); + if (trimmedFilePath.Length == 0) + throw new ArgumentException($"A file attachment field must include a local file path in {nameof(Field.Value)}.", nameof(field)); + if (!global::System.IO.File.Exists(trimmedFilePath)) + throw new ArgumentException($"File '{trimmedFilePath}' was not found or could not be accessed.", nameof(field)); + + var target = BuildFileAttachmentTarget(field.Label, field.Section, allowUnnamedAttachment: true); + return target.Length == 0 ? $"[file]={trimmedFilePath}" : $"{target}[file]={trimmedFilePath}"; + } + + private static string BuildFileAttachmentAssignmentStatement(FileAttachment fileAttachment) + { + var trimmedFilePath = fileAttachment.ContentPath.Trim(); + if (trimmedFilePath.Length == 0) + throw new ArgumentException($"{nameof(FileAttachment.ContentPath)} cannot be empty for a new file attachment.", nameof(fileAttachment)); + if (!global::System.IO.File.Exists(trimmedFilePath)) + throw new ArgumentException($"File '{trimmedFilePath}' was not found or could not be accessed.", nameof(fileAttachment)); + + var target = BuildFileAttachmentTarget(fileAttachment.Name, fileAttachment.Section, allowUnnamedAttachment: true); + return target.Length == 0 ? $"[file]={trimmedFilePath}" : $"{target}[file]={trimmedFilePath}"; + } + + private static string BuildDeleteFileAttachmentAssignmentStatement(FileAttachment fileAttachment) + { + var target = BuildFileAttachmentTarget(fileAttachment.Name, fileAttachment.Section); + return $"{target}[delete]"; + } + + private static string BuildFileAttachmentTarget(string? name, Section? section, bool allowUnnamedAttachment = false) + { + var trimmedName = name?.Trim() ?? ""; + var trimmedSection = section?.Label?.Trim() ?? ""; + + if (trimmedName.Length == 0) + { + if (trimmedSection.Length > 0) + throw new ArgumentException("A section-scoped file attachment must include a name."); + if (!allowUnnamedAttachment) + throw new ArgumentException("The file attachment name cannot be empty."); + return ""; + } + + return trimmedSection.Length == 0 ? trimmedName : $"{trimmedSection}.{trimmedName}"; + } + + private static string QuoteArgument(string argument) + { + return $"\"{argument.Replace("\"", "\\\"", StringComparison.InvariantCulture)}\""; + } + private static ItemShareResult ParseItemShareResult(string result) { var trimmedResult = result.Trim(); diff --git a/README.md b/README.md index c0c80b5..1af05be 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,35 @@ itemToExtend.Fields.Add(new Field("Environment", FieldType.String, "Production") onePassword.EditItem(itemToExtend, vault); ``` +### Adding file attachments to an item + +```csharp +var itemToAttach = onePassword.GetItem(itemSummary, vault); +itemToAttach.FileAttachments.Add(new FileAttachment(@"C:\Files\Production.env", "Production Env")); + +onePassword.EditItem(itemToAttach, vault); +``` + +### Removing a file attachment from an item + +```csharp +var itemToUpdate = onePassword.GetItem(itemSummary, vault); +var attachment = itemToUpdate.FileAttachments.First(x => x.Name == "Production Env"); +itemToUpdate.FileAttachments.Remove(attachment); + +onePassword.EditItem(itemToUpdate, vault); +``` + +### Reading file attachment metadata and content + +`FileAttachments` are hydrated by `GetItem(...)` and expose attachment metadata returned by the CLI. Use `SaveFileAttachmentContent(...)` to download the attachment content, or `GetFileAttachmentReference(...)` when you need the secret reference built from the vault, item, and attachment IDs. + +```csharp +var itemWithAttachments = onePassword.GetItem(itemSummary, vault); +var attachment = itemWithAttachments.FileAttachments.First(); +onePassword.SaveFileAttachmentContent(attachment, itemWithAttachments, vault, @"C:\Files\Production.env"); +``` + ### Sharing an item without email restrictions ```csharp diff --git a/docfx/docs/quick-start.md b/docfx/docs/quick-start.md index 5449f30..b089f41 100644 --- a/docfx/docs/quick-start.md +++ b/docfx/docs/quick-start.md @@ -108,6 +108,35 @@ itemToExtend.Fields.Add(new Field("Environment", FieldType.String, "Production") onePassword.EditItem(itemToExtend, vault); ``` +### Adding file attachments to an item + +```csharp +var itemToAttach = onePassword.GetItem(itemSummary, vault); +itemToAttach.FileAttachments.Add(new FileAttachment(@"C:\Files\Production.env", "Production Env")); + +onePassword.EditItem(itemToAttach, vault); +``` + +### Removing a file attachment from an item + +```csharp +var itemToUpdate = onePassword.GetItem(itemSummary, vault); +var attachment = itemToUpdate.FileAttachments.First(x => x.Name == "Production Env"); +itemToUpdate.FileAttachments.Remove(attachment); + +onePassword.EditItem(itemToUpdate, vault); +``` + +### Reading file attachment metadata and content + +`FileAttachments` are hydrated by `GetItem(...)` and expose attachment metadata returned by the CLI. Use `SaveFileAttachmentContent(...)` to download the attachment content, or `GetFileAttachmentReference(...)` when you need the secret reference built from the vault, item, and attachment IDs. + +```csharp +var itemWithAttachments = onePassword.GetItem(itemSummary, vault); +var attachment = itemWithAttachments.FileAttachments.First(); +onePassword.SaveFileAttachmentContent(attachment, itemWithAttachments, vault, @"C:\Files\Production.env"); +``` + ### Sharing an item without email restrictions ```csharp