diff --git a/LeedsExperiment/Fedora/ApiModel/FedoraJsonLdResponse.cs b/LeedsExperiment/Fedora/ApiModel/FedoraJsonLdResponse.cs index 1a801b3..0283a66 100644 --- a/LeedsExperiment/Fedora/ApiModel/FedoraJsonLdResponse.cs +++ b/LeedsExperiment/Fedora/ApiModel/FedoraJsonLdResponse.cs @@ -10,7 +10,7 @@ public class FedoraJsonLdResponse [JsonPropertyName("@id")] [JsonPropertyOrder(1)] - public string? Id { get; set; } + public string Id { get; set; } [JsonPropertyName("@type")] [JsonPropertyOrder(2)] @@ -34,6 +34,9 @@ public class FedoraJsonLdResponse // Our custom(ish) additions + /// + /// Assumes we are using dc:title and that Fedora maps it to title + /// [JsonPropertyName("title")] [JsonPropertyOrder(101)] public string? Title { get; set; } diff --git a/LeedsExperiment/Fedora/ApiModel/Transaction.cs b/LeedsExperiment/Fedora/ApiModel/Transaction.cs new file mode 100644 index 0000000..c7f959f --- /dev/null +++ b/LeedsExperiment/Fedora/ApiModel/Transaction.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Fedora.ApiModel +{ + public class Transaction + { + public Uri Location { get; set; } + public DateTime Expires { get; set; } + public bool Expired { get; set; } + public bool Committed { get; set; } + + public const string HeaderName = "Atomic-ID"; + } +} diff --git a/LeedsExperiment/Fedora/ArchivalGroup.cs b/LeedsExperiment/Fedora/ArchivalGroup.cs index 5188a54..7b3317f 100644 --- a/LeedsExperiment/Fedora/ArchivalGroup.cs +++ b/LeedsExperiment/Fedora/ArchivalGroup.cs @@ -1,4 +1,5 @@ -using System; +using Fedora.ApiModel; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -8,5 +9,9 @@ namespace Fedora { public class ArchivalGroup : Directory { + public ArchivalGroup(FedoraJsonLdResponse fedoraResponse) : base(fedoraResponse) + { + + } } } diff --git a/LeedsExperiment/Fedora/Directory.cs b/LeedsExperiment/Fedora/Directory.cs index 3dc0e1a..8981e21 100644 --- a/LeedsExperiment/Fedora/Directory.cs +++ b/LeedsExperiment/Fedora/Directory.cs @@ -1,4 +1,5 @@ -using System; +using Fedora.ApiModel; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -8,6 +9,10 @@ namespace Fedora { public class Directory : Resource { + public Directory(FedoraJsonLdResponse jsonLdResponse) : base(jsonLdResponse) + { + } + public required List Directories { get; set; } = new List(); public required List Files { get; set; } = new List(); } diff --git a/LeedsExperiment/Fedora/File.cs b/LeedsExperiment/Fedora/File.cs index 69fa0c8..358bc86 100644 --- a/LeedsExperiment/Fedora/File.cs +++ b/LeedsExperiment/Fedora/File.cs @@ -1,15 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Fedora.ApiModel; namespace Fedora { public class File : Resource { + public File(FedoraJsonLdResponse jsonLdResponse) : base(jsonLdResponse) + { + } - public string Origin { get + public string Origin + { + get { return "origin"; } diff --git a/LeedsExperiment/Fedora/IFedora.cs b/LeedsExperiment/Fedora/IFedora.cs index 6261948..cb1cc43 100644 --- a/LeedsExperiment/Fedora/IFedora.cs +++ b/LeedsExperiment/Fedora/IFedora.cs @@ -1,15 +1,18 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Fedora.ApiModel; namespace Fedora { public interface IFedora { Task Proxy(string contentType, string path, string? jsonLdMode = null, bool preferContained = false); - Task CreateArchivalGroup(string parentPath, string slug, string name); + Task CreateArchivalGroup(string parentPath, string slug, string name, Transaction? transaction = null); + + // Transactions + Task BeginTransaction(); + Task CheckTransaction(Transaction tx); + Task KeepTransactionAlive(Transaction tx); + Task CommitTransaction(Transaction tx); + Task RollbackTransaction(Transaction tx); } } diff --git a/LeedsExperiment/Fedora/RequestX.cs b/LeedsExperiment/Fedora/RequestX.cs index 7bf7e41..b0e1d52 100644 --- a/LeedsExperiment/Fedora/RequestX.cs +++ b/LeedsExperiment/Fedora/RequestX.cs @@ -5,6 +5,7 @@ using System.Net.Mime; using System.Text; using System.Threading.Tasks; +using Fedora.ApiModel; using Fedora.Vocab; namespace Fedora @@ -26,11 +27,21 @@ public static HttpRequestMessage WithContainedDescriptions(this HttpRequestMessa return requestMessage; } + public static HttpRequestMessage InTransaction(this HttpRequestMessage requestMessage, Transaction? transaction) + { + if(transaction != null) + { + requestMessage.Headers.Add(Transaction.HeaderName, transaction.Location.ToString()); + } + return requestMessage; + } + public static HttpRequestMessage WithName(this HttpRequestMessage requestMessage, string? name) { if(requestMessage.Content == null && !string.IsNullOrWhiteSpace(name)) { - requestMessage.Content = new StringContent($"PREFIX dc: <> dc:title \"{name}\""); + var turtle = MediaTypeHeaderValue.Parse("text/turtle"); + requestMessage.Content = new StringContent($"PREFIX dc: <> dc:title \"{name}\"", turtle); } return requestMessage; } @@ -40,5 +51,11 @@ public static HttpRequestMessage WithSlug(this HttpRequestMessage requestMessage requestMessage.Headers.Add("slug", slug); return requestMessage; } + + public static HttpRequestMessage AsArchivalGroup(this HttpRequestMessage requestMessage) + { + requestMessage.Headers.Add("Link", $"<{RepositoryTypes.ArchivalGroup}>;rel=\"type\""); + return requestMessage; + } } } diff --git a/LeedsExperiment/Fedora/Resource.cs b/LeedsExperiment/Fedora/Resource.cs index 156c02b..b0a73e4 100644 --- a/LeedsExperiment/Fedora/Resource.cs +++ b/LeedsExperiment/Fedora/Resource.cs @@ -1,18 +1,34 @@ -using System; +using Fedora.ApiModel; +using System; using System.Collections.Generic; using System.Linq; using System.Text; +using System.Text.Json.Serialization; using System.Threading.Tasks; namespace Fedora { public abstract class Resource { + public Resource(FedoraJsonLdResponse jsonLdResponse) + { + Name = jsonLdResponse.Title; + Created = jsonLdResponse.Created; + CreatedBy = jsonLdResponse.CreatedBy; + LastModified = jsonLdResponse.LastModified; + LastModifiedBy = jsonLdResponse.LastModifiedBy; + } + // The original name of the resource (possibly non-filesystem-safe) // Use dc:title on the fedora resource public string? Name { get; set; } // The Fedora identifier public required string Identifier { get; set; } + + public DateTime? Created { get; set; } + public string? CreatedBy { get; set; } + public DateTime? LastModified { get; set; } + public string? LastModifiedBy { get; set; } } } diff --git a/LeedsExperiment/Preservation.API/Controllers/FedoraController.cs b/LeedsExperiment/Preservation.API/Controllers/FedoraController.cs index 60923f1..4506bb3 100644 --- a/LeedsExperiment/Preservation.API/Controllers/FedoraController.cs +++ b/LeedsExperiment/Preservation.API/Controllers/FedoraController.cs @@ -33,7 +33,6 @@ public async Task Index(string contentTypeMajor, string contentTy var result = await fedora.Proxy(contentType, fullPath, jsonLdMode, contained); return Content(result, contentType); } - - public + } } diff --git a/LeedsExperiment/Preservation/FedoraWrapper.cs b/LeedsExperiment/Preservation/FedoraWrapper.cs index a318a92..240b351 100644 --- a/LeedsExperiment/Preservation/FedoraWrapper.cs +++ b/LeedsExperiment/Preservation/FedoraWrapper.cs @@ -43,35 +43,129 @@ public async Task Proxy(string contentType, string path, string? jsonLdM } - public async Task CreateArchivalGroup(string parentPath, string slug, string name) + public async Task CreateArchivalGroup(string parentPath, string slug, string name, Transaction? transaction = null) { - var uri = new Uri(parentPath, UriKind.Relative); - var req = MakeHttpRequestMessage(parentPath, HttpMethod.Post); - // .ForJsonLd() // no? for POST? - - // Now uncomment these: - - // .WithName(name) - // .WithSlug(slug); - // req.Headers.Add("Link", $"<{RepositoryTypes.ArchivalGroup}>;rel=\"type\""); + var req = MakeHttpRequestMessage(parentPath, HttpMethod.Post) + .InTransaction(transaction) + .WithName(name) + .WithSlug(slug) + .AsArchivalGroup(); var response = await _httpClient.SendAsync(req); response.EnsureSuccessStatusCode(); // The body is the new resource URL - var newReq = MakeHttpRequestMessage(response.Headers.Location!, HttpMethod.Get).ForJsonLd(); + var newReq = MakeHttpRequestMessage(response.Headers.Location!, HttpMethod.Get) + .ForJsonLd(); var newResponse = await _httpClient.SendAsync(newReq); var archivalGroupResponse = await MakeFedoraResponse(newResponse); - var ag = new ArchivalGroup + if(archivalGroupResponse != null) { - Name = archivalGroupResponse.Title, - Identifier = archivalGroupResponse.Id, - Directories = new List(), - Files = new List() - }; - - // add the extra created/modified/by fields to Fedora.Resource - return ag; + var archivalGroup = new ArchivalGroup(archivalGroupResponse) + { + Identifier = archivalGroupResponse.Id, + Directories = new List(), + Files = new List() + }; + + // add the extra created/modified/by fields to Fedora.Resource + return archivalGroup; + } + return null; + } + + public async Task BeginTransaction() + { + var req = MakeHttpRequestMessage("fcr:tx", HttpMethod.Post); + var response = await _httpClient.SendAsync(req); + response.EnsureSuccessStatusCode(); + var tx = new Transaction(); + tx.Location = response.Headers.Location!; + if (response.Headers.TryGetValues("Atomic-Expires", out IEnumerable? values)) + { + tx.Expires = DateTime.Parse(values.First()); + } + return tx; + } + + public async Task CheckTransaction(Transaction tx) + { + HttpRequestMessage req = MakeHttpRequestMessage(tx.Location, HttpMethod.Get); + var response = await _httpClient.SendAsync(req); + switch (response.StatusCode) + { + case System.Net.HttpStatusCode.NoContent: + tx.Expired = false; + break; + case System.Net.HttpStatusCode.NotFound: + // error? + break; + case System.Net.HttpStatusCode.Gone: + tx.Expired = true; + break; + default: + // error? + break; + } + } + + public async Task KeepTransactionAlive(Transaction tx) + { + HttpRequestMessage req = MakeHttpRequestMessage(tx.Location, HttpMethod.Post); + var response = await _httpClient.SendAsync(req); + response.EnsureSuccessStatusCode(); + + if (response.Headers.TryGetValues("Atomic-Expires", out IEnumerable? values)) + { + tx.Expires = DateTime.Parse(values.First()); + } + } + + public async Task CommitTransaction(Transaction tx) + { + HttpRequestMessage req = MakeHttpRequestMessage(tx.Location, HttpMethod.Put); + var response = await _httpClient.SendAsync(req); + switch (response.StatusCode) + { + case System.Net.HttpStatusCode.NoContent: + tx.Committed = true; + break; + case System.Net.HttpStatusCode.NotFound: + // error? + break; + case System.Net.HttpStatusCode.Conflict: + tx.Committed = false; + break; + case System.Net.HttpStatusCode.Gone: + tx.Expired = true; + break; + default: + // error? + break; + } + response.EnsureSuccessStatusCode(); + } + + public async Task RollbackTransaction(Transaction tx) + { + HttpRequestMessage req = MakeHttpRequestMessage(tx.Location, HttpMethod.Delete); + var response = await _httpClient.SendAsync(req); + switch (response.StatusCode) + { + case System.Net.HttpStatusCode.NoContent: + tx.RolledBack = true; + break; + case System.Net.HttpStatusCode.NotFound: + // error? + break; + case System.Net.HttpStatusCode.Gone: + tx.Expired = true; + break; + default: + // error? + break; + } + response.EnsureSuccessStatusCode(); } private HttpRequestMessage MakeHttpRequestMessage(string path, HttpMethod method) @@ -98,4 +192,5 @@ private HttpRequestMessage MakeHttpRequestMessage(Uri uri, HttpMethod method) } return fedoraResponse; } + } diff --git a/LeedsExperiment/SamplesWorker/Worker.cs b/LeedsExperiment/SamplesWorker/Worker.cs index 0b9c1b5..92c903f 100644 --- a/LeedsExperiment/SamplesWorker/Worker.cs +++ b/LeedsExperiment/SamplesWorker/Worker.cs @@ -37,7 +37,7 @@ private async Task EntryPoint(string[] args, CancellationToken stoppingToken) private async Task DoDefault() { // Console.WriteLine("supply something"); - await fedora.CreateArchivalGroup("ag-root", Now(), "But same title"); + await fedora.CreateArchivalGroup("ag-demo-root", Now(), "This is the title"); } private async Task OcflV1() @@ -45,6 +45,7 @@ private async Task OcflV1() var path = @"C:\Users\TomCrane\Dropbox\digirati\leeds\fedora-experiments\versioned-example\working\v1"; // begin transaction + var transaction = await fedora.BeginTransaction(); // make an archival group named Now()