From 0e3ed7273c92d84d81cd014df3779c40c23cc02d Mon Sep 17 00:00:00 2001 From: tomcrane Date: Thu, 4 Jan 2024 13:03:57 +0000 Subject: [PATCH] date time expts --- LeedsExperiment/Fedora/IFedora.cs | 2 +- .../Fedora/Vocab/MementoDateTime.cs | 29 +++++++++ .../Controllers/FedoraController.cs | 3 +- LeedsExperiment/Preservation/FedoraWrapper.cs | 60 ++++++++++--------- .../Preservation/OcflS3StorageMapper.cs | 3 +- LeedsExperiment/Preservation/RequestX.cs | 17 ++++++ 6 files changed, 82 insertions(+), 32 deletions(-) create mode 100644 LeedsExperiment/Fedora/Vocab/MementoDateTime.cs diff --git a/LeedsExperiment/Fedora/IFedora.cs b/LeedsExperiment/Fedora/IFedora.cs index 025b529..ce6b53d 100644 --- a/LeedsExperiment/Fedora/IFedora.cs +++ b/LeedsExperiment/Fedora/IFedora.cs @@ -4,7 +4,7 @@ namespace Fedora { public interface IFedora { - Task Proxy(string contentType, string path, string? jsonLdMode = null, bool preferContained = false); + Task Proxy(string contentType, string path, string? jsonLdMode = null, bool preferContained = false, string? acceptDate = null); Task GetObject(Uri uri, Transaction? transaction = null) where T: Resource; Task GetObject(string path, Transaction? transaction = null) where T : Resource; diff --git a/LeedsExperiment/Fedora/Vocab/MementoDateTime.cs b/LeedsExperiment/Fedora/Vocab/MementoDateTime.cs new file mode 100644 index 0000000..e035c09 --- /dev/null +++ b/LeedsExperiment/Fedora/Vocab/MementoDateTime.cs @@ -0,0 +1,29 @@ + +using System.Globalization; + +namespace Fedora.Vocab; + +public static class MementoDateTime +{ + public const string Format = "yyyyMMddHHmmss"; + + public static DateTime DateTimeFromMementoTimestamp(this string mementoTimestamp) + { + return DateTime.ParseExact(mementoTimestamp, Format, CultureInfo.InvariantCulture, DateTimeStyles.None); + } + + public static string ToMementoTimestamp(this DateTime dt) + { + return dt.ToString(Format); + } + + public static string ToRFC1123(this DateTime dt) + { + return dt.ToString(CultureInfo.InvariantCulture.DateTimeFormat.RFC1123Pattern); + } + + public static string ToRFC1123(this string mementoTimestamp) + { + return DateTimeFromMementoTimestamp(mementoTimestamp).ToRFC1123(); + } +} \ No newline at end of file diff --git a/LeedsExperiment/Preservation.API/Controllers/FedoraController.cs b/LeedsExperiment/Preservation.API/Controllers/FedoraController.cs index 4506bb3..fef4fbe 100644 --- a/LeedsExperiment/Preservation.API/Controllers/FedoraController.cs +++ b/LeedsExperiment/Preservation.API/Controllers/FedoraController.cs @@ -26,11 +26,12 @@ public async Task Index(string contentTypeMajor, string contentTy if (jsonld == "flattened") { jsonLdMode = JsonLdModes.Flattened; } bool contained = Convert.ToBoolean(Request.Query["contained"]); + string? acceptDate = Request.Query["acceptDate"]; // in WebAPI, path is not giving us the full path var fullPath = string.Join("/", Request.Path.ToString().Split('/')[5..]); var contentType = $"{contentTypeMajor}/{contentTypeMinor}"; - var result = await fedora.Proxy(contentType, fullPath, jsonLdMode, contained); + var result = await fedora.Proxy(contentType, fullPath, jsonLdMode, contained, acceptDate); return Content(result, contentType); } diff --git a/LeedsExperiment/Preservation/FedoraWrapper.cs b/LeedsExperiment/Preservation/FedoraWrapper.cs index 804e55f..9fb79fa 100644 --- a/LeedsExperiment/Preservation/FedoraWrapper.cs +++ b/LeedsExperiment/Preservation/FedoraWrapper.cs @@ -2,7 +2,6 @@ using Fedora.ApiModel; using Fedora.Storage; using Fedora.Vocab; -using System.Globalization; using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; @@ -21,7 +20,7 @@ public FedoraWrapper(HttpClient httpClient, IStorageMapper storageMapper) this.storageMapper = storageMapper; } - public async Task Proxy(string contentType, string path, string? jsonLdMode = null, bool preferContained = false) + public async Task Proxy(string contentType, string path, string? jsonLdMode = null, bool preferContained = false, string? acceptDate = null) { var req = new HttpRequestMessage(); req.Headers.Accept.Clear(); @@ -42,6 +41,7 @@ public async Task Proxy(string contentType, string path, string? jsonLdM { req.WithContainedDescriptions(); } + req.WithAcceptDate(acceptDate); req.RequestUri = new Uri(path, UriKind.Relative); var response = await httpClient.SendAsync(req); var raw = await response.Content.ReadAsStringAsync(); @@ -404,49 +404,49 @@ private HttpRequestMessage MakeHttpRequestMessage(Uri uri, HttpMethod method) public async Task GetPopulatedArchivalGroup(Uri uri, string? version = null, Transaction? transaction = null) { - if(version != null) + var versions = await GetFedoraVersions(uri); + var storageMap = await storageMapper.GetStorageMap(uri, version); + MergeVersions(versions, storageMap.AllVersions); + ObjectVersion? objectVersion = null; + if(!string.IsNullOrWhiteSpace(version)) { - // throw new NotImplementedException("Can't ask for fedora version yet"); - // we'll need to carry version all the way through this, GetPopulatedContainer will need to take version + objectVersion = versions.Single(v => v.MementoTimestamp == version || v.OcflVersion == version); } - var archivalGroup = await GetPopulatedContainer(uri, true, transaction) as ArchivalGroup; + + var archivalGroup = await GetPopulatedContainer(uri, true, transaction, objectVersion) as ArchivalGroup; if(archivalGroup == null) { return null; } + if(archivalGroup.Location != uri) + { + throw new InvalidOperationException("location doesnt match uri"); + } archivalGroup.Origin = storageMapper.GetArchivalGroupOrigin(archivalGroup.Location); - - // Partially populate - archivalGroup.Versions = await GetFedoraVersions(uri); - - archivalGroup.StorageMap = await storageMapper.GetStorageMap(archivalGroup.Location, version); - - MergeVersions(archivalGroup); + archivalGroup.Versions = versions; + archivalGroup.StorageMap = storageMap; return archivalGroup; } - private void MergeVersions(ArchivalGroup archivalGroup) + private void MergeVersions(ObjectVersion[] fedoraVersions, ObjectVersion[] ocflVersions) { - if(archivalGroup.Versions.Length != archivalGroup.StorageMap.AllVersions.Length) + if(fedoraVersions.Length != ocflVersions.Length) { throw new InvalidOperationException("Fedora reports a different number of versions from OCFL"); } - for(int i = 0; i < archivalGroup.Versions.Length; i++) + for(int i = 0; i < fedoraVersions.Length; i++) { - var fedoraVersion = archivalGroup.Versions[i]; - var ocflVersion = archivalGroup.StorageMap.AllVersions[i]; - if(fedoraVersion.MementoTimestamp != ocflVersion.MementoTimestamp) + if(fedoraVersions[i].MementoTimestamp != ocflVersions[i].MementoTimestamp) { - throw new InvalidOperationException($"Fedora reports a different MementoTimestamp {fedoraVersion.MementoTimestamp} from OCFL: {ocflVersion.MementoTimestamp}"); + throw new InvalidOperationException($"Fedora reports a different MementoTimestamp {fedoraVersions[i].MementoTimestamp} from OCFL: {ocflVersions[i].MementoTimestamp}"); } - fedoraVersion.OcflVersion = ocflVersion.OcflVersion; + fedoraVersions[i].OcflVersion = ocflVersions[i].OcflVersion; } - archivalGroup.Version = archivalGroup.StorageMap.Version; } - private async Task GetFedoraVersions(Uri uri) + private async Task GetFedoraVersions(Uri uri) { var request = MakeHttpRequestMessage(uri.VersionsUri(), HttpMethod.Get) .ForJsonLd(); @@ -463,17 +463,13 @@ private void MergeVersions(ArchivalGroup archivalGroup) // We're not going to learn anything more than we would by parsing the memento path elements - which is TERRIBLY non-REST-y return childIds .Select(id => id.Split('/').Last()) - .Select(p => new ObjectVersion { MementoTimestamp = p, MementoDateTime = GetDateFromMemento(p) }) + .Select(p => new ObjectVersion { MementoTimestamp = p, MementoDateTime = p.DateTimeFromMementoTimestamp() }) .OrderBy(ov => ov.MementoTimestamp) .ToArray(); } } - private DateTime GetDateFromMemento(string mementoFormat) - { - return DateTime.ParseExact(mementoFormat, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.None); - } private List GetIdsFromContainsProperty(JsonElement element) { @@ -492,7 +488,7 @@ private List GetIdsFromContainsProperty(JsonElement element) return childIds; } - public async Task GetPopulatedContainer(Uri uri, bool isArchivalGroup, Transaction? transaction = null) + public async Task GetPopulatedContainer(Uri uri, bool isArchivalGroup, Transaction? transaction = null, ObjectVersion? objectVersion = null) { var request = MakeHttpRequestMessage(uri, HttpMethod.Get) .ForJsonLd() @@ -500,6 +496,12 @@ private List GetIdsFromContainsProperty(JsonElement element) // WithContainedDescriptions could return @graph or it could return a single object if the container has no children + // PROBLEM - I can't just append /fcr:version/20240103160421 because that causes an error if you also ask for .WithContainedDescriptions() + // "Invalid request for memento" + // presumably because the contained descriptions don't each have that version? + + // but + // what's most efficient way? var response = await httpClient.SendAsync(request); bool hasArchivalGroupHeader = response.HasArchivalGroupHeader(); diff --git a/LeedsExperiment/Preservation/OcflS3StorageMapper.cs b/LeedsExperiment/Preservation/OcflS3StorageMapper.cs index 3dccb2f..9d6d7fd 100644 --- a/LeedsExperiment/Preservation/OcflS3StorageMapper.cs +++ b/LeedsExperiment/Preservation/OcflS3StorageMapper.cs @@ -3,6 +3,7 @@ using Fedora; using Fedora.Storage; using Fedora.Storage.Ocfl; +using Fedora.Vocab; using Microsoft.Extensions.Options; using System.Text.Json; @@ -36,7 +37,7 @@ public async Task GetStorageMap(Uri archivalGroupUri, string? versio { OcflVersion = kvp.Key, MementoDateTime = kvp.Value.Created, - MementoTimestamp = kvp.Value.Created.ToString("yyyyMMddHHmmss") + MementoTimestamp = kvp.Value.Created.ToMementoTimestamp(), }) .OrderBy(o => o.MementoDateTime) .ToList(); diff --git a/LeedsExperiment/Preservation/RequestX.cs b/LeedsExperiment/Preservation/RequestX.cs index 58a6f15..7c82ab0 100644 --- a/LeedsExperiment/Preservation/RequestX.cs +++ b/LeedsExperiment/Preservation/RequestX.cs @@ -1,5 +1,6 @@ using System.Net.Http.Headers; using System.Net.Mime; +using System.Security.Cryptography; using Fedora.ApiModel; using Fedora.Vocab; @@ -68,6 +69,22 @@ public static HttpRequestMessage WithDigest(this HttpRequestMessage requestMessa return requestMessage; } + + public static HttpRequestMessage WithAcceptDate(this HttpRequestMessage requestMessage, string? mementoTimestamp) + { + if (!string.IsNullOrWhiteSpace(mementoTimestamp)) + { + requestMessage.Headers.Add("Accept-Datetime", mementoTimestamp.ToRFC1123()); + } + return requestMessage; + } + + public static HttpRequestMessage WithAcceptDate(this HttpRequestMessage requestMessage, DateTime dt) + { + requestMessage.Headers.Add("Accept-Datetime", dt.ToRFC1123()); + return requestMessage; + } + public static HttpRequestMessage WithSlug(this HttpRequestMessage requestMessage, string slug) { requestMessage.Headers.Add("slug", slug);