Skip to content

Commit 2c8fb99

Browse files
authored
Legal hold & refactored tests (#25)
* Added methods for StorageController * Added Length to blob metadata & some fixes * SetLegalHold * HasLegalHold * Refactored tests * Upgrade version
1 parent c243bbb commit 2c8fb99

File tree

13 files changed

+236
-68
lines changed

13 files changed

+236
-68
lines changed

.github/workflows/dotnet.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,12 @@ jobs:
7070
AWS_ACCESS_KEY_ID: localkey
7171
AWS_SECRET_ACCESS_KEY: localsecret
7272

73-
- name: test-reports
74-
uses: dorny/test-reporter@v1.5.0
75-
with:
76-
name: Test Reporter
77-
reporter: dotnet-trx
78-
path: ManagedCode.Storage.Tests/test-results.trx
73+
# - name: test-reports
74+
# uses: dorny/test-reporter@v1.5.0
75+
# with:
76+
# name: Test Reporter
77+
# reporter: dotnet-trx
78+
# path: ManagedCode.Storage.Tests/test-results.trx
7979

8080
- name : coverlet
8181
uses: b3b00/coverlet-action@1.1.9

Directory.Build.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
<PackageLicenseExpression>MIT</PackageLicenseExpression>
1414
<PackageReadmeFile>README.md</PackageReadmeFile>
1515
<Product>Managed Code - Storage</Product>
16-
<Version>1.1.1</Version>
17-
<PackageVersion>1.1.1</PackageVersion>
16+
<Version>1.1.2</Version>
17+
<PackageVersion>1.1.2</PackageVersion>
1818
</PropertyGroup>
1919
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
2020
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>

ManagedCode.Storage.Aws/AWSStorage.cs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,8 @@ public async IAsyncEnumerable<bool> ExistsAsync(IEnumerable<BlobMetadata> blobs,
167167
{
168168
Name = blobName,
169169
Uri = new Uri($"https://s3.amazonaws.com/{_bucket}/{blobName}"),
170-
ContentType = objectMetaResponse.Headers.ContentType
170+
ContentType = objectMetaResponse.Headers.ContentType,
171+
Length = objectMetaResponse.Headers.ContentLength
171172
};
172173
}
173174
catch (AmazonS3Exception ex) when (ex.StatusCode is HttpStatusCode.NotFound)
@@ -204,7 +205,8 @@ public async IAsyncEnumerable<BlobMetadata> GetBlobListAsync(
204205
{
205206
Name = entry.Key,
206207
Uri = new Uri($"https://s3.amazonaws.com/{_bucket}/{entry.Key}"),
207-
ContentType = objectMetaResponse.Headers.ContentType
208+
ContentType = objectMetaResponse.Headers.ContentType,
209+
Length = objectMetaResponse.Headers.ContentLength
208210
};
209211
}
210212

@@ -330,4 +332,36 @@ public async Task CreateContainerAsync(CancellationToken cancellationToken = def
330332
}
331333

332334
#endregion
335+
336+
public async Task SetLegalHold(string blobName, bool hasLegalHold, CancellationToken cancellationToken = default)
337+
{
338+
var status = hasLegalHold
339+
? ObjectLockLegalHoldStatus.On
340+
: ObjectLockLegalHoldStatus.Off;
341+
342+
PutObjectLegalHoldRequest request = new()
343+
{
344+
BucketName = _bucket,
345+
Key = blobName,
346+
LegalHold = new ObjectLockLegalHold
347+
{
348+
Status = status,
349+
},
350+
};
351+
352+
await _s3Client.PutObjectLegalHoldAsync(request, cancellationToken);
353+
}
354+
355+
public async Task<bool> HasLegalHold(string blobName, CancellationToken cancellationToken = default)
356+
{
357+
GetObjectLegalHoldRequest request = new()
358+
{
359+
BucketName = _bucket,
360+
Key = blobName
361+
};
362+
363+
var response = await _s3Client.GetObjectLegalHoldAsync(request, cancellationToken);
364+
365+
return response.LegalHold.Status == ObjectLockLegalHoldStatus.On;
366+
}
333367
}

ManagedCode.Storage.Azure/AzureStorage.cs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,17 +155,20 @@ public async IAsyncEnumerable<bool> ExistsAsync(IEnumerable<BlobMetadata> blobs,
155155
await Task.Yield();
156156

157157
var blobClient = _blobContainerClient.GetBlobClient(blobName);
158-
158+
159159
if (!await blobClient.ExistsAsync(cancellationToken))
160160
{
161161
return null;
162162
}
163163

164+
var properties = await blobClient.GetPropertiesAsync(cancellationToken: cancellationToken);
165+
164166
return new BlobMetadata
165167
{
166168
Name = blobClient.Name,
167169
Uri = blobClient.Uri,
168170
Container = blobClient.BlobContainerName,
171+
Length = properties.Value.ContentLength
169172
};
170173
}
171174

@@ -191,10 +194,13 @@ public async IAsyncEnumerable<BlobMetadata> GetBlobListAsync([EnumeratorCancella
191194
{
192195
foreach (var blobItem in item.Values)
193196
{
194-
yield return new BlobMetadata
197+
var blobMetadata = new BlobMetadata
195198
{
196199
Name = blobItem.Name,
200+
Length = blobItem.Properties.ContentLength ?? 0
197201
};
202+
203+
yield return blobMetadata;
198204
}
199205
}
200206
}
@@ -333,4 +339,19 @@ public async Task CreateContainerAsync(CancellationToken cancellationToken = def
333339
}
334340

335341
#endregion
342+
343+
public async Task SetLegalHold(string blobName, bool hasLegalHold, CancellationToken cancellationToken = default)
344+
{
345+
var blobClient = _blobContainerClient.GetBlobClient(blobName);
346+
347+
await blobClient.SetLegalHoldAsync(hasLegalHold, cancellationToken);
348+
}
349+
350+
public async Task<bool> HasLegalHold(string blobName, CancellationToken cancellationToken = default)
351+
{
352+
var blobClient = _blobContainerClient.GetBlobClient(blobName);
353+
var properties = await blobClient.GetPropertiesAsync(cancellationToken: cancellationToken);
354+
355+
return properties.Value?.HasLegalHold ?? false;
356+
}
336357
}

ManagedCode.Storage.Core/IStorage.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,7 @@ public interface IStorage : IDisposable
3939
IAsyncEnumerable<bool> ExistsAsync(IEnumerable<BlobMetadata> blobs, CancellationToken cancellationToken = default);
4040

4141
Task CreateContainerAsync(CancellationToken cancellationToken = default);
42+
43+
Task SetLegalHold(string blobName, bool hasLegalHold, CancellationToken cancellationToken = default);
44+
Task<bool> HasLegalHold(string blobName, CancellationToken cancellationToken = default);
4245
}

ManagedCode.Storage.Core/Models/BlobMetadata.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ public class BlobMetadata
99
public string? Container { get; set; }
1010
public string? ContentType { get; set; }
1111
public bool Rewrite { get; set; }
12+
public long Length { get; set; }
1213
}

ManagedCode.Storage.FileSystem/FileSystemStorage.cs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ namespace ManagedCode.Storage.FileSystem;
1414
public class FileSystemStorage : IFileSystemStorage
1515
{
1616
private readonly string _path;
17+
private readonly Dictionary<string, FileStream> _lockedFiles = new();
1718

1819
public FileSystemStorage(FileSystemStorageOptions fileSystemStorageOptions)
1920
{
@@ -157,7 +158,8 @@ public async IAsyncEnumerable<bool> ExistsAsync(IEnumerable<BlobMetadata> blobs,
157158
{
158159
Name = fileInfo.Name,
159160
Uri = new Uri(Path.Combine(_path, blobName)),
160-
ContentType = MimeHelper.GetMimeType(fileInfo.Extension)
161+
ContentType = MimeHelper.GetMimeType(fileInfo.Extension),
162+
Length = fileInfo.Length
161163
};
162164

163165
return Task.FromResult<BlobMetadata?>(result);
@@ -280,4 +282,36 @@ public async Task CreateContainerAsync(CancellationToken cancellationToken = def
280282
}
281283

282284
#endregion
285+
286+
public async Task SetLegalHold(string blobName, bool hasLegalHold, CancellationToken cancellationToken = default)
287+
{
288+
if (hasLegalHold && !_lockedFiles.ContainsKey(blobName))
289+
{
290+
var file = await DownloadAsync(blobName, cancellationToken);
291+
292+
if (file is null) return;
293+
294+
var fileStream = File.OpenRead(file.FilePath); // Opening with FileAccess.Read only
295+
fileStream.Lock(0, fileStream.Length); // Attempting to lock a region of the read-only file
296+
297+
_lockedFiles.Add(blobName, fileStream);
298+
299+
return;
300+
}
301+
302+
if (!hasLegalHold)
303+
{
304+
if (_lockedFiles.ContainsKey(blobName))
305+
{
306+
_lockedFiles[blobName].Unlock(0, _lockedFiles[blobName].Length);
307+
_lockedFiles[blobName].Dispose();
308+
_lockedFiles.Remove(blobName);
309+
}
310+
}
311+
}
312+
313+
public Task<bool> HasLegalHold(string blobName, CancellationToken cancellationToken = default)
314+
{
315+
return Task.FromResult(_lockedFiles.ContainsKey(blobName));
316+
}
283317
}

ManagedCode.Storage.Gcp/GCPStorage.cs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,10 @@ public async IAsyncEnumerable<bool> ExistsAsync(IEnumerable<BlobMetadata> blobs,
171171
return new BlobMetadata
172172
{
173173
Name = obj.Name,
174-
Uri = string.IsNullOrEmpty(obj.MediaLink) ? null : new Uri(obj.MediaLink)
174+
Uri = string.IsNullOrEmpty(obj.MediaLink) ? null : new Uri(obj.MediaLink),
175+
Container = obj.Bucket,
176+
ContentType = obj.ContentType,
177+
Length = (long) (obj.Size ?? 0),
175178
};
176179
}
177180
catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.NotFound)
@@ -188,7 +191,10 @@ public IAsyncEnumerable<BlobMetadata> GetBlobListAsync(CancellationToken cancell
188191
x => new BlobMetadata
189192
{
190193
Name = x.Name,
191-
Uri = string.IsNullOrEmpty(x.MediaLink) ? null : new Uri(x.MediaLink)
194+
Uri = string.IsNullOrEmpty(x.MediaLink) ? null : new Uri(x.MediaLink),
195+
Container = x.Bucket,
196+
ContentType = x.ContentType,
197+
Length = (long) (x.Size ?? 0),
192198
}
193199
);
194200
}
@@ -304,4 +310,19 @@ await _storageClient.CreateBucketAsync(_gcpStorageOptions.BucketOptions.ProjectI
304310
}
305311

306312
#endregion
313+
314+
public async Task SetLegalHold(string blobName, bool hasLegalHold, CancellationToken cancellationToken = default)
315+
{
316+
var storageObject = await _storageClient.GetObjectAsync(_bucket, blobName, cancellationToken: cancellationToken);
317+
storageObject.TemporaryHold = hasLegalHold;
318+
319+
await _storageClient.UpdateObjectAsync(storageObject, cancellationToken: cancellationToken);
320+
}
321+
322+
public async Task<bool> HasLegalHold(string blobName, CancellationToken cancellationToken = default)
323+
{
324+
var storageObject = await _storageClient.GetObjectAsync(_bucket, blobName, cancellationToken: cancellationToken);
325+
326+
return storageObject.TemporaryHold ?? false;
327+
}
307328
}

ManagedCode.Storage.Tests/FileHelper.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.IO;
3+
using System.Linq;
34
using ManagedCode.Storage.Core;
45
using ManagedCode.Storage.Core.Helpers;
56
using Microsoft.AspNetCore.Http;
@@ -9,6 +10,8 @@ namespace ManagedCode.Storage.Tests;
910

1011
public static class FileHelper
1112
{
13+
private static readonly Random Random = new();
14+
1215
public static LocalFile GenerateLocalFile(string fileName, int byteSize)
1316
{
1417
var path = Path.Combine(Path.GetTempPath(), fileName);
@@ -44,4 +47,12 @@ public static string GenerateRandomFileName(string extension = ".txt")
4447
{
4548
return $"{Guid.NewGuid().ToString("N").ToLowerInvariant()}.{extension}";
4649
}
50+
51+
public static string GenerateRandomFileContent(string extension = ".txt")
52+
{
53+
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
54+
55+
return new string(Enumerable.Repeat(chars, 100)
56+
.Select(s => s[Random.Next(s.Length)]).ToArray());
57+
}
4758
}

ManagedCode.Storage.Tests/GCP/GoogleStorageTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public void StorageAsDefaultTest()
120120
[Fact]
121121
public override async Task GetBlobsAsync()
122122
{
123-
var fileList = await CreateFileList(nameof(GetBlobsAsync), 5);
123+
var fileList = await CreateFileList(5);
124124

125125
var blobList = fileList.Select(f => f.FileName).ToList();
126126

0 commit comments

Comments
 (0)