Skip to content

Commit 87d8c9f

Browse files
Merge pull request #57 from matteobortolazzo/dev
1.2.0
2 parents 776df5e + e29a793 commit 87d8c9f

17 files changed

+550
-25
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# 1.2.0 (2020-01-24)
2+
3+
## Features
4+
* **Attachments:** Adds support for attachments. ([#PR56](https://github.com/matteobortolazzo/couchdb-net/pull/56))
5+
16
# 1.1.5 (2019-12-19)
27

38
## Bug Fixes

LATEST_CHANGE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
## Bug Fixes
2-
* **Database:** Fixing special characters escaping in databases names. ([#PR54](https://github.com/matteobortolazzo/couchdb-net/pull/54))
1+
## Features
2+
* **Attachments:** Adds support for attachments. ([#PR56](https://github.com/matteobortolazzo/couchdb-net/pull/56))

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,37 @@ public class OtherRebel : Rebel
345345
public DateTime BirthDate { get; set; }
346346
```
347347

348+
## Attachments
349+
350+
The driver fully support attachments, you can list, create, delete and download them.
351+
352+
```csharp
353+
// Get a document
354+
var luke = new Rebel { Name = "Luke", Age = 19 };
355+
356+
// Add in memory
357+
var pathToDocument = @".\luke.txt"
358+
luke.Attachments.AddOrUpdate(pathToDocument, MediaTypeNames.Text.Plain);
359+
360+
// Delete in memory
361+
luke.Attachments.Delete("yoda.txt");
362+
363+
// Save
364+
luke = await rebels.CreateOrUpdateAsync(luke);
365+
366+
// Get
367+
CouchAttachment lukeTxt = luke.Attachments["luke.txt"];
368+
369+
// Iterate
370+
foreach (CouchAttachment attachment in luke.Attachments)
371+
{
372+
...
373+
}
374+
375+
// Download
376+
string downloadFilePath = await rebels.DownloadAttachment(attachment, downloadFolderPath, "luke-downloaded.txt");
377+
```
378+
348379
## Advanced
349380

350381
If requests have to be modified before each call, it's possible to override OnBeforeCall.

src/CouchDB.Driver/CouchDB.Driver.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<OutputType>Library</OutputType>
1616
<StartupObject />
1717
<NeutralLanguage>en</NeutralLanguage>
18-
<Version>1.1.0</Version>
18+
<Version>1.2.0</Version>
1919
</PropertyGroup>
2020

2121
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

src/CouchDB.Driver/CouchDatabase.cs

Lines changed: 113 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Flurl.Http;
99
using System;
1010
using System.Collections.Generic;
11+
using System.IO;
1112
using System.Linq;
1213
using System.Linq.Expressions;
1314
using System.Net.Http;
@@ -25,7 +26,7 @@ public class CouchDatabase<TSource> where TSource : CouchDocument
2526
private readonly IFlurlClient _flurlClient;
2627
private readonly CouchSettings _settings;
2728
private readonly string _connectionString;
28-
private string _database;
29+
private readonly string _database;
2930

3031
/// <summary>
3132
/// The database name.
@@ -228,10 +229,13 @@ public async Task<TSource> FindAsync(string docId, bool withConflicts = false)
228229
request = request.SetQueryParam("conflicts", true);
229230
}
230231

231-
return await request
232+
TSource document = await request
232233
.GetJsonAsync<TSource>()
233234
.SendRequestAsync()
234235
.ConfigureAwait(false);
236+
237+
InitAttachments(document);
238+
return document;
235239
}
236240
catch (CouchNotFoundException)
237241
{
@@ -274,7 +278,14 @@ private async Task<List<TSource>> SendQueryAsync(Func<IFlurlRequest, Task<HttpRe
274278
.SendRequestAsync()
275279
.ConfigureAwait(false);
276280

277-
return findResult.Docs.ToList();
281+
var documents = findResult.Docs.ToList();
282+
283+
foreach (TSource document in documents)
284+
{
285+
InitAttachments(document);
286+
}
287+
288+
return documents;
278289
}
279290

280291
/// Finds all documents with given IDs.
@@ -291,8 +302,29 @@ public async Task<List<TSource>> FindManyAsync(IEnumerable<string> docIds)
291302
}).ReceiveJson<BulkGetResult<TSource>>()
292303
.SendRequestAsync()
293304
.ConfigureAwait(false);
305+
306+
var documents = bulkGetResult.Results
307+
.SelectMany(r => r.Docs)
308+
.Select(d => d.Item)
309+
.ToList();
294310

295-
return bulkGetResult.Results.SelectMany(r => r.Docs).Select(d => d.Item).ToList();
311+
foreach (TSource document in documents)
312+
{
313+
InitAttachments(document);
314+
}
315+
316+
return documents;
317+
}
318+
319+
private void InitAttachments(TSource document)
320+
{
321+
foreach (CouchAttachment attachment in document.Attachments)
322+
{
323+
attachment.DocumentId = document.Id;
324+
attachment.DocumentRev = document.Rev;
325+
var path = $"{_connectionString}/{_database}/{document.Id}/{Uri.EscapeUriString(attachment.Name)}";
326+
attachment.Uri = new Uri(path);
327+
}
296328
}
297329

298330
#endregion
@@ -325,8 +357,11 @@ public async Task<TSource> CreateAsync(TSource document, bool batch = false)
325357
.ReceiveJson<DocumentSaveResponse>()
326358
.SendRequestAsync()
327359
.ConfigureAwait(false);
328-
329360
document.ProcessSaveResponse(response);
361+
362+
await UpdateAttachments(document)
363+
.ConfigureAwait(false);
364+
330365
return document;
331366
}
332367

@@ -356,8 +391,11 @@ public async Task<TSource> CreateOrUpdateAsync(TSource document, bool batch = fa
356391
.ReceiveJson<DocumentSaveResponse>()
357392
.SendRequestAsync()
358393
.ConfigureAwait(false);
359-
360394
document.ProcessSaveResponse(response);
395+
396+
await UpdateAttachments(document)
397+
.ConfigureAwait(false);
398+
361399
return document;
362400
}
363401

@@ -410,6 +448,9 @@ public async Task<IEnumerable<TSource>> CreateOrUpdateRangeAsync(IEnumerable<TSo
410448
foreach ((TSource document, DocumentSaveResponse saveResponse) in zipped)
411449
{
412450
document.ProcessSaveResponse(saveResponse);
451+
452+
await UpdateAttachments(document)
453+
.ConfigureAwait(false);
413454
}
414455

415456
return documents;
@@ -435,10 +476,76 @@ public async Task EnsureFullCommitAsync()
435476
}
436477
}
437478

479+
private async Task UpdateAttachments(TSource document)
480+
{
481+
foreach (CouchAttachment attachment in document.Attachments.GetAddedAttachments())
482+
{
483+
var stream = new StreamContent(
484+
new FileStream(attachment.FileInfo.FullName, FileMode.Open));
485+
486+
AttachmentResult response = await NewRequest()
487+
.AppendPathSegment(document.Id)
488+
.AppendPathSegment(Uri.EscapeUriString(attachment.Name))
489+
.WithHeader("Content-Type", attachment.ContentType)
490+
.WithHeader("If-Match", document.Rev)
491+
.PutAsync(stream)
492+
.ReceiveJson<AttachmentResult>()
493+
.ConfigureAwait(false);
494+
495+
if (response.Ok)
496+
{
497+
document.Rev = response.Rev;
498+
attachment.FileInfo = null;
499+
}
500+
}
501+
502+
foreach (CouchAttachment attachment in document.Attachments.GetDeletedAttachments())
503+
{
504+
AttachmentResult response = await NewRequest()
505+
.AppendPathSegment(document.Id)
506+
.AppendPathSegment(attachment.Name)
507+
.WithHeader("If-Match", document.Rev)
508+
.DeleteAsync()
509+
.ReceiveJson<AttachmentResult>()
510+
.ConfigureAwait(false);
511+
512+
if (response.Ok)
513+
{
514+
document.Rev = response.Rev;
515+
document.Attachments.RemoveAttachment(attachment);
516+
}
517+
}
518+
519+
InitAttachments(document);
520+
}
521+
438522
#endregion
439523

440524
#region Utils
441525

526+
/// <summary>
527+
/// Asynchronously downloads a specific attachment.
528+
/// </summary>
529+
/// <param name="attachment">The attachment to download.</param>
530+
/// <param name="localFolderPath">Path of local folder where file is to be downloaded.</param>
531+
/// <param name="localFileName">Name of local file. If not specified, the source filename (from Content-Dispostion header, or last segment of the URL) is used.</param>
532+
/// <param name="bufferSize">Buffer size in bytes. Default is 4096.</param>
533+
/// <returns>The path of the downloaded file.</returns>
534+
public async Task<string> DownloadAttachment(CouchAttachment attachment, string localFolderPath, string localFileName = null, int bufferSize = 4096)
535+
{
536+
if (attachment.Uri == null)
537+
{
538+
throw new InvalidOperationException("The attachment is not uploaded yet.");
539+
}
540+
541+
return await NewRequest()
542+
.AppendPathSegment(attachment.DocumentId)
543+
.AppendPathSegment(Uri.EscapeUriString(attachment.Name))
544+
.WithHeader("If-Match", attachment.DocumentRev)
545+
.DownloadFileAsync(localFolderPath, localFileName, bufferSize)
546+
.ConfigureAwait(false);
547+
}
548+
442549
/// <summary>
443550
/// Requests compaction of the specified database.
444551
/// </summary>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Newtonsoft.Json;
2+
3+
namespace CouchDB.Driver.DTOs
4+
{
5+
public class AttachmentResult
6+
{
7+
[JsonProperty("id")]
8+
public string Id { get; set; }
9+
[JsonProperty("ok")]
10+
public bool Ok { get; set; }
11+
[JsonProperty("rev")]
12+
public string Rev { get; set; }
13+
}
14+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using CouchDB.Driver.DTOs;
2+
using CouchDB.Driver.Exceptions;
3+
using CouchDB.Driver.Types;
4+
5+
namespace CouchDB.Driver.Extensions
6+
{
7+
internal static class CouchDocumentExtensions
8+
{
9+
public static void ProcessSaveResponse(this CouchDocument item, DocumentSaveResponse response)
10+
{
11+
if (!response.Ok)
12+
{
13+
throw new CouchException(response.Error, response.Reason);
14+
}
15+
16+
item.Id = response.Id;
17+
item.Rev = response.Rev;
18+
}
19+
}
20+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System;
2+
using System.IO;
3+
using System.Runtime.Serialization;
4+
using Newtonsoft.Json;
5+
6+
namespace CouchDB.Driver.Types
7+
{
8+
public class CouchAttachment
9+
{
10+
[JsonIgnore]
11+
internal string DocumentId { get; set; }
12+
13+
[JsonIgnore]
14+
internal string DocumentRev { get; set; }
15+
16+
[JsonIgnore]
17+
internal FileInfo FileInfo { get; set; }
18+
19+
[JsonIgnore]
20+
internal bool Deleted { get; set; }
21+
22+
[JsonIgnore]
23+
public string Name { get; internal set; }
24+
25+
[JsonIgnore]
26+
public Uri Uri { get; internal set; }
27+
28+
[DataMember]
29+
[JsonProperty("content_type")]
30+
public string ContentType { get; set; }
31+
32+
[DataMember]
33+
[JsonProperty("digest")]
34+
public string Digest { get; private set; }
35+
36+
[DataMember]
37+
[JsonProperty("length")]
38+
public int? Length { get; private set; }
39+
}
40+
}

0 commit comments

Comments
 (0)