Skip to content

Commit

Permalink
Export of versions
Browse files Browse the repository at this point in the history
  • Loading branch information
tomcrane committed Jan 17, 2024
1 parent 271522a commit c3b4268
Show file tree
Hide file tree
Showing 14 changed files with 366 additions and 89 deletions.
27 changes: 25 additions & 2 deletions LeedsExperiment/Dashboard/Controllers/ImportExportController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,17 @@ public async Task<IActionResult> ExportStartAsync(
[FromRoute] string path,
[FromQuery] string? version = null)
{
throw new NotImplementedException();
ViewBag.Path = path;
var ag = await preservation.GetArchivalGroup(path, version);
if (ag == null)
{
return NotFound();
}
if (ag.Type != "ArchivalGroup")
{
return BadRequest("Not an Archival Group");
}
return View("ExportStart", ag);
// display a list of what's going to be exported
// add a default destination in the staging bucket
// allow a different destination to be specified (bucket, key root)
Expand All @@ -36,8 +46,21 @@ public async Task<IActionResult> ExportStartAsync(
// display summary completion, link back to ArchivalGroup Browse head
}

[HttpPost]
[ActionName("ExportExecute")]
[Route("export/{*path}")]
public async Task<IActionResult> ExportExecuteAsync(
[FromRoute] string path,
[FromQuery] string? version = null)
{
var exportResult = await preservation.Export(path, version);
return View("ExportResult", exportResult);

[HttpGet]
// we could also supply the ag to the model for a richer display but at the expense of longer time
}


[HttpGet]
[ActionName("ImportStart")]
[Route("import/{*path}")]
public async Task<IActionResult> ImportStartAsync(
Expand Down
50 changes: 50 additions & 0 deletions LeedsExperiment/Dashboard/Views/ImportExport/ExportResult.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
@using Dashboard.Borrowed
@using Dashboard.Helpers
@using Utils
@model Preservation.ExportResult

<div>
<h1 class="display-4">Export Result 📦</h1>

@if (!string.IsNullOrEmpty(Model.Problem))
{
<div class="alert alert-danger" role="alert">
@Model.Problem
</div>
} else
{
<table class="table">
<tbody>
<tr>
<th scope="row">Archival Group</th>
<td><a href="/ocfl/@Model.ArchivalGroupPath">@Model.ArchivalGroupPath</a></td>
</tr>
<tr>
<th scope="row">Destination (@Model.StorageType)</th>
<td><a href="@S3Util.GetAwsConsoleUri(Model.Destination)">@Model.Destination</a></td>
</tr>
<tr>
<th scope="row">Version</th>
<td>@Model.Version</td>
</tr>
<tr>
<th scope="row">Time taken</th>
<td>@((Model.End - Model.Start).TotalMilliseconds) ms</td>
</tr>
<tr>
<th scope="row">Files</th>
<td>
<ul class="list-unstyled">
@foreach(var file in Model.Files)
{
<li><a href="@S3Util.GetAwsConsoleUri(file)">@file.RemoveStart($"{Model.Destination}/")</a></li>
}
</ul>
</td>
</tr>
</tbody>
</table>
}

</div>

61 changes: 61 additions & 0 deletions LeedsExperiment/Dashboard/Views/ImportExport/ExportStart.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
@using Dashboard.Helpers
@using Utils
@model Fedora.Abstractions.ArchivalGroup

<div>
<h1 class="display-4">Export 📦 - @Model.GetDisplayName()</h1>
<p>
Version: <strong>@Model.Version!.OcflVersion</strong> -
@if (Model.Version.Equals(Model.StorageMap!.HeadVersion))
{
<em>This is the current (head) version.</em>
}
else
{
<em>This is a <strong>previous</strong> version.</em>
}
</p>
<p><a href="/ocfl/@ViewBag.Path">View storage (OCFL)</a></p>

<hr/>

<p>The following files will be exported to a location in a staging bucket for you to pick up.</p>


<div class="alert alert-secondary" role="alert">
<small class="text-muted">(Later you will have more control about where you can have the Preservation API export content to)</small>
</div>

<form method="post" action="/export/@(ViewBag.Path)?version=@Model.Version.OcflVersion">
<div class="row g-3 align-items-center">
<div class="col-auto">
<input class="btn btn-primary form-control" type="submit" value="Export">
</div>
<div class="col-auto">
<span class="form-text">
Click here to export the following files:
</span>
</div>
</div>
</form>

<table class="table">
<thead>
<tr>
<th>File path</th>
<th>Hash</th>
<th>Versioned path</th>
</tr>
</thead>
<tbody>
@foreach (var fileEntry in Model.StorageMap.Files.OrderBy(kvp => kvp.Key))
{
<tr>
<td>@fileEntry.Key</td>
<td><span title="@fileEntry.Value.Hash">@fileEntry.Value.Hash.Substring(0, 8)</span></td>
<td>@fileEntry.Value.FullPath</td>
</tr>
}
</tbody>
</table>
</div>
18 changes: 1 addition & 17 deletions LeedsExperiment/Fedora/Abstractions/Transfer/BinaryFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,15 @@
/// Used when importing new files into the repository from a source location (disk or S3)
/// or exporting to a location
/// </summary>
public class BinaryFile
public class BinaryFile : ResourceWithParentUri
{
/// <summary>
/// The repository path (not a full Uri), will end with Slug
/// Only contains permitted characters (e.g., no spaces)
/// </summary>
public required string Path { get; set; }

/// <summary>
/// An S3 key, a filesystem path - somewhere accessible to the Preservation API, to import from or export to
/// </summary>
public required string ExternalLocation { get; set; }

public required string StorageType { get; set; }

/// <summary>
/// Only contains permitted characters (e.g., no spaces)
/// </summary>
public string Slug => Path.Split('/')[^1];

/// <summary>
/// The name of the resource in Fedora (dc:title)
/// </summary>
public required string Name { get; set; }

/// <summary>
/// The Original / actual name of the file, rather than the path-safe, reduced character set slug
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,5 @@
namespace Fedora.Abstractions.Transfer;

public class ContainerDirectory
public class ContainerDirectory : ResourceWithParentUri
{
/// <summary>
/// The repository path (not a full Uri), will end with Slug
/// Only contains permitted characters (e.g., no spaces)
///
/// This is not required if you supply Slug and a parent
/// </summary>
public string? Path { get; set; }

private string? slug;
public string? Slug
{
get
{
if (string.IsNullOrEmpty(Path))
{
return slug;
}
return Path.Split('/')[^1];
}
set
{
slug = value;
}
}

/// <summary>
/// The name of the resource in Fedora (dc:title)
/// </summary>
public required string Name { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace Fedora.Abstractions.Transfer;

public abstract class ResourceWithParentUri
{
/// <summary>
/// The repository path (not a full Uri), will end with Slug
/// Only contains permitted characters (e.g., no spaces)
///
/// The Path is relative to some container or parent, which may not be the immediate parent.
/// It will often be relative to the ArchivalGroup parent.
/// </summary>
public required string Path { get; set; }

/// <summary>
/// The Fedora repository Uri that is the parent of Path - i.e., where this file is to be put (or where it came from)
/// </summary>
public required Uri Parent { get; set; }

/// <summary>
/// The name of the resource in Fedora (dc:title)
/// Usually the original name of the directory or file, which will usually be the same as Slug if all characters are path safe
/// </summary>
public required string Name { get; set; }

/// <summary>
/// Only contains permitted characters (e.g., no spaces)
/// </summary>
public string Slug => Path.Split('/')[^1];
}
4 changes: 2 additions & 2 deletions LeedsExperiment/Fedora/IFedora.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public interface IFedora
Task<ArchivalGroup?> CreateArchivalGroup(Uri parent, string slug, string name, Transaction? transaction = null);
Task<ArchivalGroup?> CreateArchivalGroup(string parentPath, string slug, string name, Transaction? transaction = null);

Task<Container?> CreateContainer(Uri parent, ContainerDirectory containerDirectory, Transaction? transaction = null);
Task<Container?> CreateContainer(ContainerDirectory containerDirectory, Transaction? transaction = null);

/// <summary>
/// DISALLOW a POST for binaries, for now
Expand All @@ -36,7 +36,7 @@ public interface IFedora
/// <returns></returns>
// Task<Binary> AddBinary(Uri parent, FileInfo localFileInfo, string originalName, string contentType, Transaction? transaction = null, string? checksum = null);
// Task<Binary> PutBinary(Uri location, FileInfo localFileInfo, string originalName, string contentType, Transaction? transaction = null, string? checksum = null);
Task<Binary> PutBinary(Uri archivalGroupUri, BinaryFile binaryFile, Transaction? transaction = null);
Task<Binary> PutBinary(BinaryFile binaryFile, Transaction? transaction = null);

// Transactions
Task<Transaction> BeginTransaction();
Expand Down
63 changes: 63 additions & 0 deletions LeedsExperiment/Preservation.API/Controllers/ExportController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using Amazon.S3;
using Fedora;
using Fedora.Storage;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;

namespace Preservation.API.Controllers;

[Route("api/[controller]")]
[ApiController]
public class ExportController : Controller
{
private readonly IStorageMapper storageMapper;
private readonly IFedora fedora;
private readonly PreservationApiOptions options;
private IAmazonS3 s3Client;

public ExportController(
IStorageMapper storageMapper,
IFedora fedora,
IOptions<PreservationApiOptions> options,
IAmazonS3 awsS3Client
)
{
this.storageMapper = storageMapper;
this.fedora = fedora;
this.options = options.Value;
this.s3Client = awsS3Client;
}

[HttpGet(Name = "Export")]
[Route("{*path}")]
public async Task<ExportResult?> Index([FromRoute] string path, [FromQuery] string? version)
{
var agUri = fedora.GetUri(path);
var exportKey = $"exports/{path}/{DateTime.Now:yyyy-MM-dd-HH-mm-ss}";
var storageMap = await storageMapper.GetStorageMap(agUri, version);
var result = new ExportResult
{
ArchivalGroupPath = path,
Destination = $"s3://{options.StagingBucket}/{exportKey}",
StorageType = StorageTypes.S3,
Version = storageMap.Version,
Start = DateTime.Now
};
try
{
foreach (var file in storageMap.Files)
{
var sourceKey = $"{storageMap.ObjectPath}/{file.Value.FullPath}";
var destKey = $"{exportKey}/{file.Key}";
var resp = await s3Client.CopyObjectAsync(storageMap.Root, sourceKey, options.StagingBucket, destKey);
result.Files.Add($"s3://{options.StagingBucket}/{destKey}");
}
result.End = DateTime.Now;
}
catch(Exception ex)
{
result.Problem = ex.Message;
}
return result;
}
}
Loading

0 comments on commit c3b4268

Please sign in to comment.