Skip to content

Commit

Permalink
Merge pull request #50 from Arlodotexe/dev/0.10.0
Browse files Browse the repository at this point in the history
Release 0.10.0
  • Loading branch information
Arlodotexe authored Mar 18, 2024
2 parents 7b8385b + 21c923d commit 132982f
Show file tree
Hide file tree
Showing 36 changed files with 254 additions and 215 deletions.
21 changes: 21 additions & 0 deletions src/Exceptions/FileAlreadyExistsException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using System.IO;

namespace OwlCore.Storage;

/// <summary>
/// The exception that is thrown when attempting to create or recreate a file that already exists.
/// </summary>
public class FileAlreadyExistsException : IOException
{
const int FileAlreadyExistsHResult = unchecked((int)0x80070050);

/// <summary>
/// Initializes a new instance of the <see cref="FileAlreadyExistsException"/> class.
/// </summary>
public FileAlreadyExistsException(string fileName)
: base($"(HRESULT:0x{FileAlreadyExistsHResult:X8}) The file {fileName} already exists.")
{
HResult = FileAlreadyExistsHResult;
}
}
109 changes: 68 additions & 41 deletions src/Extensions/CopyAndMoveExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,51 +14,60 @@ public static partial class ModifiableFolderExtensions
/// </summary>
/// <param name="destinationFolder">The folder where the copy is created.</param>
/// <param name="fileToCopy">The file to be copied into this folder.</param>
/// <param name="overwrite"><code>true</code> if the destination file can be overwritten; otherwise, <c>false</c>.</param>
/// <param name="overwrite"><code>true</code> if any existing destination file can be overwritten; otherwise, <c>false</c>.</param>
/// <param name="cancellationToken">A token that can be used to cancel the ongoing operation.</param>
public static async Task<IChildFile> CreateCopyOfAsync<T>(this IModifiableFolder destinationFolder, T fileToCopy, bool overwrite = default, CancellationToken cancellationToken = default)
where T : IFile
/// <exception cref="FileAlreadyExistsException">Thrown when <paramref name="overwrite"/> is false and the resource being created already exists.</exception>
public static async Task<IChildFile> CreateCopyOfAsync(this IModifiableFolder destinationFolder, IFile fileToCopy, bool overwrite, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
static async Task<IChildFile> CreateCopyOfFallbackAsync(IModifiableFolder destinationFolder, IFile fileToCopy, bool overwrite, CancellationToken cancellationToken = default)
{
// Open the source file
using var sourceStream = await fileToCopy.OpenStreamAsync(FileAccess.Read, cancellationToken: cancellationToken);

// Create the destination file
var newFile = await destinationFolder.CreateFileAsync(fileToCopy.Name, overwrite, cancellationToken);
using var destinationStream = await newFile.OpenStreamAsync(FileAccess.ReadWrite, cancellationToken: cancellationToken);
cancellationToken.ThrowIfCancellationRequested();

// If the destination folder can copy this file faster than us, use that.
if (destinationFolder is IFastFileCopy<T> fastPath)
return await fastPath.CreateCopyOfAsync(fileToCopy, overwrite, cancellationToken);
// Align stream positions (if possible)
if (destinationStream.CanSeek && destinationStream.Position != 0)
destinationStream.Seek(0, SeekOrigin.Begin);

if (sourceStream.CanSeek && sourceStream.Position != 0)
sourceStream.Seek(0, SeekOrigin.Begin);

// Set stream length to zero to clear any existing data.
// Otherwise, writing less bytes than already exists would leave extra bytes at the end.
destinationStream.SetLength(0);

// Copy the src into the dest file
await sourceStream.CopyToAsync(destinationStream, bufferSize: 81920, cancellationToken);

return newFile;
}

cancellationToken.ThrowIfCancellationRequested();

// If the destination file exists and overwrite is false, it shouldn't be overwritten or returned as-is. Throw an exception instead.
if (!overwrite)
{
try
{
var existingItem = await destinationFolder.GetFirstByNameAsync(fileToCopy.Name, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();

if (existingItem is IChildFile childFile)
return childFile;
}
catch (FileNotFoundException)
{
var existing = await destinationFolder.GetFirstByNameAsync(fileToCopy.Name, cancellationToken);
if (existing is not null)
throw new FileAlreadyExistsException(fileToCopy.Name);
}
catch (FileNotFoundException) { }
}

// Open the source file
using var sourceStream = await fileToCopy.OpenStreamAsync(FileAccess.Read, cancellationToken: cancellationToken);

// Create the destination file
var newFile = await destinationFolder.CreateFileAsync(fileToCopy.Name, overwrite, cancellationToken);
using var destinationStream = await newFile.OpenStreamAsync(FileAccess.ReadWrite, cancellationToken: cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
// If the destination folder declares a non-fallback copy path, try that.
// Provide fallback in case this file is not a handled type.
if (destinationFolder is ICreateCopyOf fastPath)
return await fastPath.CreateCopyOfAsync(fileToCopy, overwrite, cancellationToken, fallback: CreateCopyOfFallbackAsync);

// Align stream positions (if possible)
if (destinationStream.CanSeek && destinationStream.Position != 0)
destinationStream.Seek(0, SeekOrigin.Begin);
// Manual copy. Slower, but covers all scenarios.
return await CreateCopyOfFallbackAsync(destinationFolder, fileToCopy, overwrite, cancellationToken);

if (sourceStream.CanSeek && sourceStream.Position != 0)
sourceStream.Seek(0, SeekOrigin.Begin);

// Copy the src into the dest file
await sourceStream.CopyToAsync(destinationStream, bufferSize: 81920, cancellationToken);

return newFile;
}

/// <summary>
Expand All @@ -69,19 +78,37 @@ public static async Task<IChildFile> CreateCopyOfAsync<T>(this IModifiableFolder
/// <param name="source">The folder that <paramref name="fileToMove"/> is being moved from.</param>
/// <param name="overwrite"><code>true</code> if the destination file can be overwritten; otherwise, <c>false</c>.</param>
/// <param name="cancellationToken">A token that can be used to cancel the ongoing operation.</param>
public static async Task<IChildFile> MoveFromAsync<T>(this IModifiableFolder destinationFolder, T fileToMove, IModifiableFolder source, bool overwrite = default, CancellationToken cancellationToken = default)
where T : IFile, IStorableChild
/// <exception cref="FileAlreadyExistsException">Thrown when <paramref name="overwrite"/> is false and the resource being created already exists.</exception>
public static async Task<IChildFile> MoveFromAsync(this IModifiableFolder destinationFolder, IChildFile fileToMove, IModifiableFolder source, bool overwrite, CancellationToken cancellationToken = default)
{
static async Task<IChildFile> MoveFromFallbackAsync(IModifiableFolder destinationFolder, IChildFile fileToMove, IModifiableFolder source, bool overwrite, CancellationToken cancellationToken = default)
{
var file = await destinationFolder.CreateCopyOfAsync(fileToMove, overwrite, cancellationToken);
await source.DeleteAsync(fileToMove, cancellationToken);

return file;
}

cancellationToken.ThrowIfCancellationRequested();

// If the destination folder can move this file faster than us, use that.
if (destinationFolder is IFastFileMove<T> fastPath)
return await fastPath.MoveFromAsync(fileToMove, source, overwrite, cancellationToken);
// If the destination file exists and overwrite is false, it shouldn't be overwritten or returned as-is. Throw an exception instead.
if (!overwrite)
{
try
{
var existing = await destinationFolder.GetFirstByNameAsync(fileToMove.Name, cancellationToken);
if (existing is not null)
throw new FileAlreadyExistsException(fileToMove.Name);
}
catch (FileNotFoundException) { }
}

// If the destination folder declares a non-fallback move path, try that.
// Provide fallback in case this file is not a handled type.
if (destinationFolder is IMoveFrom fastPath)
return await fastPath.MoveFromAsync(fileToMove, source, overwrite, cancellationToken, fallback: MoveFromFallbackAsync);

// Manual move. Slower, but covers all scenarios.
var file = await destinationFolder.CreateCopyOfAsync(fileToMove, overwrite, cancellationToken);
await source.DeleteAsync(fileToMove, cancellationToken);

return file;
return await MoveFromFallbackAsync(destinationFolder, fileToMove, source, overwrite, cancellationToken);
}
}
2 changes: 1 addition & 1 deletion src/Extensions/FileOpenExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using System.Threading;
using System.Threading.Tasks;

namespace OwlCore.Storage.Extensions;
namespace OwlCore.Storage;

/// <summary>
/// Extension methods for <see cref="IFile"/>.
Expand Down
4 changes: 2 additions & 2 deletions src/Extensions/GetFirstByNameExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ public static partial class FolderExtensions
/// <exception cref="FileNotFoundException">The item was not found in the provided folder.</exception>
public static async Task<IStorableChild> GetFirstByNameAsync(this IFolder folder, string name, CancellationToken cancellationToken = default)
{
if (folder is IFastGetFirstByName fastPath)
if (folder is IGetFirstByName fastPath)
return await fastPath.GetFirstByNameAsync(name, cancellationToken);

var targetItem = await folder.GetItemsAsync(cancellationToken: cancellationToken).FirstOrDefaultAsync(x => name.Equals(x.Name, StringComparison.Ordinal), cancellationToken);
if (targetItem is null)
{
throw new FileNotFoundException($"No storage item with the name \"{name}\" could be found.");
throw new FileNotFoundException($"No storage item with the name '{name}' could be found.");
}

return targetItem;
Expand Down
4 changes: 2 additions & 2 deletions src/Extensions/GetItemAsyncExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ public static partial class FolderExtensions
/// <exception cref="FileNotFoundException">The item was not found in the provided folder.</exception>
public static async Task<IStorableChild> GetItemAsync(this IFolder folder, string id, CancellationToken cancellationToken = default)
{
if (folder is IFastGetItem fastPath)
if (folder is IGetItem fastPath)
return await fastPath.GetItemAsync(id, cancellationToken);

var targetItem = await folder.GetItemsAsync(cancellationToken: cancellationToken).FirstOrDefaultAsync(x => x.Id == id, cancellationToken: cancellationToken);
if (targetItem is null)
throw new FileNotFoundException($"No storage item with the ID \"{id}\" could be found.");
throw new FileNotFoundException($"No storage item with the Id '{id}' could be found.");

return targetItem;
}
Expand Down
6 changes: 3 additions & 3 deletions src/Extensions/GetItemByRelativePathExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public static async Task<IStorable> GetItemByRelativePathAsync(this IStorable fr
if (nextPathPart == "..")
{
if (from is not IStorableChild child)
throw new ArgumentException($"A parent folder was requested, but the storable item named {from.Name} is not the child of a directory.", nameof(relativePath));
throw new ArgumentException($"A parent folder was requested, but the storable item named '{from.Name}' is not the child of a directory.", nameof(relativePath));

var parent = await child.GetParentAsync(cancellationToken);

Expand All @@ -63,11 +63,11 @@ public static async Task<IStorable> GetItemByRelativePathAsync(this IStorable fr

// Get item by name.
if (from is not IFolder folder)
throw new ArgumentException($"An item named {nextPathPart} was requested from the folder named {from.Name}, but {from.Name} is not a folder.");
throw new ArgumentException($"An item named '{nextPathPart}' was requested from the folder named '{from.Name}', but '{from.Name}' is not a folder.");

var item = await folder.GetFirstByNameAsync(nextPathPart, cancellationToken);
if (item is null)
throw new FileNotFoundException($"An item named {nextPathPart} was requested from the folder named {from.Name}, but {nextPathPart} wasn't found in the folder.");
throw new FileNotFoundException($"An item named '{nextPathPart}' was requested from the folder named '{from.Name}', but '{nextPathPart}' wasn't found in the folder.");

return await GetItemByRelativePathAsync(item, string.Join(ourPathSeparator, pathParts.Skip(1)));
}
Expand Down
6 changes: 3 additions & 3 deletions src/Extensions/GetItemRecursiveExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ public static partial class FolderExtensions
/// <returns>The item</returns>
public static async Task<IStorableChild> GetItemRecursiveAsync(this IFolder folder, string id, CancellationToken cancellationToken = default)
{
if (folder is IFastGetItemRecursive fastPath)
if (folder is IGetItemRecursive fastPath)
{
var item = await fastPath.GetItemRecursiveAsync(id, cancellationToken);
if (item.Id != id)
{
throw new ArgumentException(@$"The item returned by the interface ""{nameof(IFastGetItemRecursive)}"" implemented in ""{folder.GetType()}"" does not have the requested Id ""{id}"". Actual value: ""{item.Id}"".", nameof(item));
throw new ArgumentException($"The item returned by {nameof(IGetItemRecursive)}.{nameof(IGetItemRecursive.GetItemRecursiveAsync)} implemented in {folder.GetType()} does not have an Id that matches the requested '{id}'. Actual value: '{item.Id}'.", nameof(item));
}

return item;
Expand Down Expand Up @@ -50,6 +50,6 @@ public static async Task<IStorableChild> GetItemRecursiveAsync(this IFolder fold
}
}

throw new FileNotFoundException($"No storage item with the ID \"{id}\" could be found.");
throw new FileNotFoundException($"No storage item with the Id '{id}' could be found.");
}
}
10 changes: 6 additions & 4 deletions src/Extensions/GetRelativePathToExtension.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace OwlCore.Storage;
Expand All @@ -10,7 +11,7 @@ public static partial class FolderExtensions
/// <summary>
/// Crawls the ancestors of <paramref name="to" /> until <paramref name="from"/> is found, then returns the constructed relative path.
/// </summary>
public static async Task<string> GetRelativePathToAsync(this IFolder from, IStorableChild to)
public static async Task<string> GetRelativePathToAsync(this IFolder from, IStorableChild to, CancellationToken cancellationToken = default)
{
if (Equals(from, to) || from.Id == to.Id)
return @"/";
Expand All @@ -19,7 +20,8 @@ public static async Task<string> GetRelativePathToAsync(this IFolder from, IStor
{
to.Name,
};


cancellationToken.ThrowIfCancellationRequested();
await RecursiveAddParentToPathAsync(to);

// Relative path to a folder should end with a directory separator '/'
Expand All @@ -28,12 +30,12 @@ public static async Task<string> GetRelativePathToAsync(this IFolder from, IStor
{
IFolder => $"/{string.Join(@"/", pathComponents)}/",
IFile => $"/{string.Join(@"/", pathComponents)}",
_ => throw new NotSupportedException($"{to.GetType()} is not an {nameof(IFile)} or an {nameof(IFolder)}. Unable to generate a path."),
_ => throw new NotSupportedException($"{to.GetType()} is not an implementation of {nameof(IFile)} or {nameof(IFolder)}. Unable to generate a path."),
};

async Task RecursiveAddParentToPathAsync(IStorableChild item)
{
var parent = await item.GetParentAsync();
var parent = await item.GetParentAsync(cancellationToken);
if (parent is IStorableChild child && parent.Id != from.Id)
{
pathComponents.Insert(0, parent.Name);
Expand Down
22 changes: 11 additions & 11 deletions src/Extensions/GetRootExtension.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;

namespace OwlCore.Storage;

Expand All @@ -11,24 +12,23 @@ public static partial class StorableChildExtensions
/// Retrieves the root of the provided <paramref name="item"/>.
/// </summary>
/// <param name="item">The item which the root should be retrieved from.</param>
/// <param name="cancellationToken">A token that can be used to cancel the ongoing operation.</param>
/// <returns>The folder that this implementation considers the "root".</returns>
public static async Task<IFolder?> GetRootAsync(this IStorableChild item)
public static async Task<IFolder?> GetRootAsync(this IStorableChild item, CancellationToken cancellationToken = default)
{
// If the item knows how to find the root quickly.
if (item is IFastGetRoot fastRoot)
return await fastRoot.GetRootAsync();
if (item is IGetRoot fastRoot)
return await fastRoot.GetRootAsync(cancellationToken);

// Otherwise, manually recurse to the root.
var parent = await item.GetParentAsync();
if (parent is null || parent is not IStorableChild parentAsChild)
var parent = await item.GetParentAsync(cancellationToken);
if (parent is not IStorableChild parentAsChild)
{
// Item is the root already.
return null;
}
else
{
// Item is not the root, try asking the parent.
return await parentAsChild.GetRootAsync();
}

// Item is not the root, try asking the parent.
return await parentAsChild.GetRootAsync(cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,24 @@
namespace OwlCore.Storage;

/// <summary>
/// Provides a fast-path for the <see cref="ModifiableFolderExtensions.CreateCopyOfAsync{T}"/> extension method.
/// Provides a way for implementations to override behavior of the <see cref="ModifiableFolderExtensions.CreateCopyOfAsync"/> extension method.
/// </summary>
/// <exception cref="FileNotFoundException">The item was not found in the provided folder.</exception>
public interface IFastFileCopy<in T> : IModifiableFolder
where T : IFile
public interface ICreateCopyOf : IModifiableFolder
{
/// <summary>
/// Creates a copy of the provided file within this folder.
/// </summary>
/// <param name="fileToCopy">The file to be copied into this folder.</param>
/// <param name="overwrite">If there is an existing destination file, <c>true</c> will overwrite it; otherwise <c>false</c> and the existing file is opened.</param>
/// <param name="cancellationToken">A token that can be used to cancel the ongoing operation.</param>
/// <param name="fallback">The fallback to use if the provided <paramref name="fileToCopy"/> isn't supported.</param>
/// <returns>The newly created (or opened if existing) file.</returns>
Task<IChildFile> CreateCopyOfAsync(T fileToCopy, bool overwrite = default, CancellationToken cancellationToken = default);
Task<IChildFile> CreateCopyOfAsync(IFile fileToCopy, bool overwrite, CancellationToken cancellationToken, CreateCopyOfDelegate fallback);
}

/// <summary>
/// A delegate that provides a fallback for the <see cref="IMoveFrom.MoveFromAsync"/> method.
/// </summary>
/// <returns></returns>
public delegate Task<IChildFile> CreateCopyOfDelegate(IModifiableFolder destination, IFile fileToCopy, bool overwrite, CancellationToken cancellationToken);
Loading

0 comments on commit 132982f

Please sign in to comment.