File: Files\HostedFileClientExtensions.cs
Project: ..\..\..\src\Libraries\Microsoft.Extensions.AI.Abstractions\Microsoft.Extensions.AI.Abstractions.csproj (Microsoft.Extensions.AI.Abstractions)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net.Mime;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
 
namespace Microsoft.Extensions.AI;
 
/// <summary>
/// Extension methods for <see cref="IHostedFileClient"/>.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AIFiles, UrlFormat = DiagnosticIds.UrlFormat)]
public static class HostedFileClientExtensions
{
    /// <summary>
    /// Uploads content from a <see cref="DataContent"/>.
    /// </summary>
    /// <param name="client">The file client.</param>
    /// <param name="content">The content to upload.</param>
    /// <param name="options">Options to configure the upload.</param>
    /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
    /// <returns>Information about the uploaded file.</returns>
    /// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
    /// <exception cref="ArgumentNullException"><paramref name="content"/> is <see langword="null"/>.</exception>
    public static Task<HostedFileContent> UploadAsync(
        this IHostedFileClient client,
        DataContent content,
        HostedFileClientOptions? options = null,
        CancellationToken cancellationToken = default)
    {
        _ = Throw.IfNull(client);
        _ = Throw.IfNull(content);
 
        MemoryStream stream = MemoryMarshal.TryGetArray(content.Data, out ArraySegment<byte> arraySegment) ?
            new(arraySegment.Array!, arraySegment.Offset, arraySegment.Count) :
            new(content.Data.ToArray());
 
        return client.UploadAsync(stream, content.MediaType, content.Name, options, cancellationToken);
    }
 
    /// <summary>
    /// Uploads a file from a local file path.
    /// </summary>
    /// <param name="client">The file client.</param>
    /// <param name="filePath">The path to the file to upload.</param>
    /// <param name="options">Options to configure the upload.</param>
    /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
    /// <returns>Information about the uploaded file.</returns>
    /// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
    /// <exception cref="ArgumentNullException"><paramref name="filePath"/> is <see langword="null"/>.</exception>
    /// <exception cref="ArgumentException"><paramref name="filePath"/> is empty.</exception>
    public static async Task<HostedFileContent> UploadAsync(
        this IHostedFileClient client,
        string filePath,
        HostedFileClientOptions? options = null,
        CancellationToken cancellationToken = default)
    {
        _ = Throw.IfNull(client);
        _ = Throw.IfNullOrEmpty(filePath);
 
        string? mediaType = MediaTypeMap.GetMediaType(filePath);
        string fileName = Path.GetFileName(filePath);
 
        using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete, bufferSize: 4096, useAsync: true);
 
        return await client.UploadAsync(stream, mediaType, fileName, options, cancellationToken).ConfigureAwait(false);
    }
 
    /// <summary>
    /// Downloads a file and saves it to a local path.
    /// </summary>
    /// <param name="client">The file client.</param>
    /// <param name="fileId">The ID of the file to download.</param>
    /// <param name="destinationPath">
    /// The path to save the file to. If the path is a directory or empty, the file name will be inferred.
    /// An empty path is treated as the current directory.
    /// </param>
    /// <param name="options">Options to configure the download.</param>
    /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
    /// <returns>The actual path where the file was saved.</returns>
    /// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
    /// <exception cref="ArgumentNullException"><paramref name="fileId"/> is <see langword="null"/>.</exception>
    /// <exception cref="ArgumentException"><paramref name="fileId"/> is empty or whitespace.</exception>
    /// <exception cref="ArgumentNullException"><paramref name="destinationPath"/> is <see langword="null"/>.</exception>
    /// <exception cref="IOException">A file at the destination path already exists.</exception>
    public static async Task<string> DownloadToAsync(
        this IHostedFileClient client,
        string fileId,
        string destinationPath,
        HostedFileClientOptions? options = null,
        CancellationToken cancellationToken = default)
    {
        _ = Throw.IfNull(client);
        _ = Throw.IfNullOrWhitespace(fileId);
        _ = Throw.IfNull(destinationPath);
 
        using HostedFileDownloadStream downloadStream = await client.DownloadAsync(fileId, options, cancellationToken).ConfigureAwait(false);
 
        // Determine the final path. If the path is empty, treat it as the current directory.
        string finalPath = destinationPath;
        if (destinationPath.Length == 0 || Directory.Exists(destinationPath))
        {
            string? fileName = null;
 
            if (downloadStream.FileName is not null)
            {
                fileName = Path.GetFileName(downloadStream.FileName);
            }
 
            if (string.IsNullOrEmpty(fileName))
            {
                fileName = $"{fileId}{MediaTypeMap.GetExtension(downloadStream.MediaType)}";
            }
 
            finalPath = destinationPath.Length == 0 ? fileName! : Path.Combine(destinationPath, fileName);
        }
 
        using FileStream fileStream = new(
            finalPath, FileMode.CreateNew, FileAccess.Write, FileShare.Read,
            bufferSize: 1, // buffering in FileStream is a nop as CopyToAsync will use its own, larger buffer
            useAsync: true);
 
        await downloadStream.CopyToAsync(fileStream,
#if !NET
            81920,
#endif
            cancellationToken).ConfigureAwait(false);
 
        return finalPath;
    }
 
    /// <summary>
    /// Downloads a file referenced by a <see cref="HostedFileContent"/>.
    /// </summary>
    /// <param name="client">The file client.</param>
    /// <param name="hostedFile">The hosted file reference.</param>
    /// <param name="options">Options to configure the download.</param>
    /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
    /// <returns>A <see cref="HostedFileDownloadStream"/> containing the file content.</returns>
    /// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
    /// <exception cref="ArgumentNullException"><paramref name="hostedFile"/> is <see langword="null"/>.</exception>
    public static Task<HostedFileDownloadStream> DownloadAsync(
        this IHostedFileClient client,
        HostedFileContent hostedFile,
        HostedFileClientOptions? options = null,
        CancellationToken cancellationToken = default)
    {
        _ = Throw.IfNull(client);
        _ = Throw.IfNull(hostedFile);
 
        if (hostedFile.Scope is string scope && options?.Scope is null)
        {
            options = options?.Clone() ?? new();
            options.Scope = scope;
        }
 
        return client.DownloadAsync(hostedFile.FileId, options, cancellationToken);
    }
 
    /// <summary>
    /// Downloads a file and returns its content as a buffered <see cref="DataContent"/>.
    /// </summary>
    /// <param name="client">The file client.</param>
    /// <param name="fileId">The ID of the file to download.</param>
    /// <param name="options">Options to configure the download.</param>
    /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
    /// <returns>The file content as a <see cref="DataContent"/>.</returns>
    /// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
    /// <exception cref="ArgumentNullException"><paramref name="fileId"/> is <see langword="null"/>.</exception>
    /// <exception cref="ArgumentException"><paramref name="fileId"/> is empty or whitespace.</exception>
    /// <remarks>
    /// This method buffers the entire file content into memory. For large files,
    /// consider using <see cref="IHostedFileClient.DownloadAsync"/>
    /// and streaming directly to the destination.
    /// </remarks>
    public static async Task<DataContent> DownloadAsDataContentAsync(
        this IHostedFileClient client,
        string fileId,
        HostedFileClientOptions? options = null,
        CancellationToken cancellationToken = default)
    {
        _ = Throw.IfNull(client);
        _ = Throw.IfNullOrWhitespace(fileId);
 
        using HostedFileDownloadStream downloadStream = await client.DownloadAsync(fileId, options, cancellationToken).ConfigureAwait(false);
 
        return await downloadStream.ToDataContentAsync(cancellationToken).ConfigureAwait(false);
    }
 
    /// <summary>
    /// Gets the <see cref="HostedFileClientMetadata"/> for this client.
    /// </summary>
    /// <param name="client">The file client.</param>
    /// <returns>The metadata for this client, or <see langword="null"/> if not available.</returns>
    /// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
    public static HostedFileClientMetadata? GetMetadata(this IHostedFileClient client)
    {
        _ = Throw.IfNull(client);
 
        return client.GetService(typeof(HostedFileClientMetadata)) as HostedFileClientMetadata;
    }
 
    /// <summary>
    /// Gets a service of the specified type from the file client.
    /// </summary>
    /// <typeparam name="TService">The type of service to retrieve.</typeparam>
    /// <param name="client">The file client.</param>
    /// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
    /// <returns>The found service, or <see langword="null"/> if not available.</returns>
    /// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
    public static TService? GetService<TService>(this IHostedFileClient client, object? serviceKey = null)
    {
        _ = Throw.IfNull(client);
 
        return client.GetService(typeof(TService), serviceKey) is TService service ? service : default;
    }
 
    /// <summary>
    /// Asks the <see cref="IHostedFileClient"/> for an object of the specified type <paramref name="serviceType"/>
    /// and throws an exception if one isn't available.
    /// </summary>
    /// <param name="client">The file client.</param>
    /// <param name="serviceType">The type of object being requested.</param>
    /// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
    /// <returns>The found object.</returns>
    /// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
    /// <exception cref="ArgumentNullException"><paramref name="serviceType"/> is <see langword="null"/>.</exception>
    /// <exception cref="InvalidOperationException">No service of the requested type for the specified key is available.</exception>
    /// <remarks>
    /// The purpose of this method is to allow for the retrieval of services that are required to be provided by the <see cref="IHostedFileClient"/>,
    /// including itself or any services it might be wrapping.
    /// </remarks>
    public static object GetRequiredService(this IHostedFileClient client, Type serviceType, object? serviceKey = null)
    {
        _ = Throw.IfNull(client);
        _ = Throw.IfNull(serviceType);
 
        return
            client.GetService(serviceType, serviceKey) ??
            throw Throw.CreateMissingServiceException(serviceType, serviceKey);
    }
 
    /// <summary>
    /// Asks the <see cref="IHostedFileClient"/> for an object of type <typeparamref name="TService"/>
    /// and throws an exception if one isn't available.
    /// </summary>
    /// <typeparam name="TService">The type of the object to be retrieved.</typeparam>
    /// <param name="client">The file client.</param>
    /// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
    /// <returns>The found object.</returns>
    /// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
    /// <exception cref="InvalidOperationException">No service of the requested type for the specified key is available.</exception>
    /// <remarks>
    /// The purpose of this method is to allow for the retrieval of strongly typed services that are required to be provided by the <see cref="IHostedFileClient"/>,
    /// including itself or any services it might be wrapping.
    /// </remarks>
    public static TService GetRequiredService<TService>(this IHostedFileClient client, object? serviceKey = null)
    {
        _ = Throw.IfNull(client);
 
        if (client.GetService(typeof(TService), serviceKey) is not TService service)
        {
            throw Throw.CreateMissingServiceException(typeof(TService), serviceKey);
        }
 
        return service;
    }
}