Newer
Older
Sunfish / src / Sunfish.Core / Web / WebMac.cs
@Ivan Dominguez Ivan Dominguez 21 hours ago 4 KB 3.0
using Sunfish.Http;
using Sunfish.Mac;

namespace Sunfish.Web;

/// <summary>
/// Mac clasico y nombres cortos en la web (DOC/MAC-CLASICO.md, DOC/CLIENTES.md): las subidas en
/// MacBinary o BinHex se separan solas; a un Mac clasico se le entrega en MacBinary lo que tiene
/// resource fork; ?as=macbin y ?as=hqx lo piden explicitamente; DOS y Windows 3.1 reciben el
/// nombre 8.3 en la descarga y Mac clasico uno de 31 caracteres como mucho.
/// </summary>
public sealed partial class WebApp
{
    /// <summary>Tras una subida: si es MacBinary/BinHex y el share tiene Mac, lo separa. Devuelve el nombre final.</summary>
    private async Task<string> UnpackMacAsync(ShareRequest sr, string path)
    {
        if (!sr.Share.Mac)
            return Path.GetFileName(path);
        try
        {
            var (result, name) = await MacFiles.TryUnpackAsync(path, sr.Share.Overwrite, sr.Profile.Kind == ClientKind.Mac, sr.Ctx.Aborted);
            if (result == MacFiles.Unpacked.Unpacked)
                log.Info($"{sr.Ctx.Request.RemoteEndPoint?.Address} mac {Path.GetFileName(path)} -> {name}");
            else if (result == MacFiles.Unpacked.ExistsKept)
                log.Warning($"{sr.Ctx.Request.RemoteEndPoint?.Address} mac {Path.GetFileName(path)}: name inside already exists, kept encoded");
            return name;
        }
        catch (Exception e) when (e is IOException or UnauthorizedAccessException or EndOfStreamException)
        {
            log.Warning($"{sr.Ctx.Request.RemoteEndPoint?.Address} mac {Path.GetFileName(path)}: {e.Message}");
            return Path.GetFileName(path);
        }
    }

    /// <summary>Descarga de un fichero de un share, con las adaptaciones del perfil del cliente.</summary>
    private async Task SendShareFileAsync(ShareRequest sr, string physical)
    {
        var (ctx, share, profile) = (sr.Ctx, sr.Share, sr.Profile);
        string name = Path.GetFileName(physical);
        string? encoding = ctx.Request.GetQuery("as");
        bool macFork = share.Mac && MacFiles.HasCompanion(physical);

        if (share.Mac && (encoding is "macbin" or "hqx" || (encoding is null && profile.Kind == ClientKind.Mac && macFork)))
        {
            await SendMacEncodedAsync(ctx, physical, encoding == "hqx", profile);
            return;
        }

        // Nombre de la descarga segun el cliente: 8.3 para DOS y Windows 3.1, 31 caracteres para Mac.
        string? downloadName = null;
        if (profile.UsesShortNames && share.ShortNames)
        {
            var dir = Path.GetDirectoryName(physical)!;
            downloadName = Shares.ShortNames.Map(new DirectoryInfo(dir).EnumerateFileSystemInfos().Select(i => i.Name)).GetValueOrDefault(name);
        }
        else if (profile.Kind == ClientKind.Mac && name.Length > 31)
        {
            downloadName = MacNames.Shorten(name, 31);
        }
        if (downloadName is not null && downloadName != name)
            ctx.Response.Headers["Content-Disposition"] = (ctx.Request.HasQuery("dl") ? "attachment" : "inline") +
                                                         $"; filename=\"{AsciiOnly(downloadName)}\"";
        await SendFileAsync(ctx, physical);
    }

    /// <summary>MacBinary III (con Content-Length) o BinHex 4.0 del fichero con su AppleDouble.</summary>
    private static async Task SendMacEncodedAsync(HttpContext ctx, string physical, bool binHex, ClientProfile profile)
    {
        var file = MacFiles.Describe(physical);
        long dataLength = new FileInfo(physical).Length;
        string baseName = profile.Kind == ClientKind.Mac ? MacNames.Shorten(file.Name, 27) : file.Name;
        if (binHex)
        {
            if (dataLength > MacFiles.MaxBinHex)
            {
                await SendTextAsync(ctx, 413, Strings.ErrorTitle(413));
                return;
            }
            var text = BinHex.Encode(file, await File.ReadAllBytesAsync(physical, ctx.Aborted));
            ctx.Response.Headers["Content-Disposition"] = ContentDisposition(baseName + ".hqx");
            await ctx.Response.SendAsync(text, "application/mac-binhex40", ctx.Aborted);
            return;
        }
        ctx.Response.Headers["Content-Type"] = "application/x-macbinary";
        ctx.Response.Headers["Content-Disposition"] = ContentDisposition(baseName + ".bin");
        var body = await ctx.Response.StartAsync(MacBinary.EncodedLength(dataLength, file.ResourceFork.Length), ctx.Aborted);
        if (ctx.Request.IsHead)
            return;
        await using var data = new FileStream(physical, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, 1, useAsync: true);
        await MacBinary.EncodeAsync(file, data, dataLength, body, ctx.Aborted);
    }
}