XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

STHApplication

【MAUI】寄当家——安卓端交易管理系统

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Buffers.Binary;
using System.Security.Cryptography;
using JiDangJia.Contracts;
using static JiDangJia.Server.ServerStore;

namespace JiDangJia.Server;

public sealed partial class CommunityMediaStore
{
    public async Task<CommunityMedia> UploadRecommendation(Account actor,HttpRequest request,CancellationToken ct)
    {
        store.Read(c=>{SocialWrite(c,actor);if(!CommunityStore.Actor(c,actor))throw new CommunityException(403,"推荐媒体仅限管理员上传");return 0;});
        var kind=request.Headers["X-Media-Kind"].ToString();var nonce=request.Headers["X-Upload-Id"].ToString().ToLowerInvariant();
        if(kind is not ("IMAGE" or "VIDEO")||!System.Text.RegularExpressions.Regex.IsMatch(nonce,"^[a-f0-9]{32}$"))throw new ArgumentException("推荐仅支持图片或视频,请重新选择文件");
        var origin=MediaOrigin.Read(request,kind);
        // 推荐不设数量、时长或账户空间额度。流式写入,文件大小只受服务器可用存储限制。
        var temporary=Path.Combine(directory,".upload-"+Guid.NewGuid().ToString("N"));
        var processed=temporary+".marked";
        try
        {
            long size=0;using var hash=IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
            await using(var file=new FileStream(temporary,FileMode.CreateNew,FileAccess.Write,FileShare.None,65536,true))
            {
                var buffer=new byte[65536];int length;while((length=await request.Body.ReadAsync(buffer,ct))>0){hash.AppendData(buffer,0,length);await file.WriteAsync(buffer.AsMemory(0,length),ct);size+=length;}
            }
            if(size==0)throw new ArgumentException("媒体文件为空");var digest=Convert.ToHexString(hash.GetHashAndReset());var format=RecommendationMediaInspector.Inspect(temporary,kind);
            var duplicate=store.Read(c=>{var previous=Rows(c,"SELECT * FROM CommunityMedia WHERE ownerId=@p0 AND clientId=@p1",actor.Id,nonce).FirstOrDefault();if(previous is null)return null;if(Text(previous,"hash")!=digest||Text(previous,"kind")!=kind||Text(previous,"purpose")!="RECOMMEND")throw new CommunityException(409,"重复上传的文件或用途不同");origin.CheckRetry(previous);return Item(previous);});if(duplicate is not null)return duplicate;
            if(kind=="IMAGE"){await File.WriteAllBytesAsync(processed,watermark.Image(temporary,origin),ct);format=("image/jpeg",0);}else format=await watermark.Video(temporary,processed,origin,true,ct);
            size=new FileInfo(processed).Length;ct.ThrowIfCancellationRequested();
            return store.Write(c=>
            {
                SocialWrite(c,actor);if(!CommunityStore.Actor(c,actor))throw new CommunityException(403,"需要管理员权限");
                var previous=Rows(c,"SELECT * FROM CommunityMedia WHERE ownerId=@p0 AND clientId=@p1",actor.Id,nonce).FirstOrDefault();
                if(previous is not null){if(Text(previous,"hash")!=digest||Text(previous,"kind")!=kind||Text(previous,"purpose")!="RECOMMEND")throw new CommunityException(409,"重复上传的文件或用途不同");origin.CheckRetry(previous);return Item(previous);}
                var media=new CommunityMedia(Guid.NewGuid().ToString("N"),kind,format.Mime,size,format.Duration,origin.Source,origin.Timestamp);var target=Path.Combine(directory,media.Id+".media");
                try{File.Move(processed,target);Exec(c,"INSERT INTO CommunityMedia(id,ownerId,kind,mimeType,size,duration,hash,clientId,createdAt,purpose,source,markedAt) VALUES(@p0,@p1,@p2,@p3,@p4,@p5,@p6,@p7,@p8,'RECOMMEND',@p9,@p10)",media.Id,actor.Id,kind,media.MimeType,size,media.Duration,digest,nonce,Now,media.Source,media.MarkedAt);return media;}
                catch{File.Delete(target);throw;}
            });
        }
        finally{File.Delete(temporary);File.Delete(processed);}
    }
}

internal static class RecommendationMediaInspector
{
    public static (string Mime,double Duration) Inspect(string path,string kind)
    {
        using var stream=File.OpenRead(path);var probe=new byte[12];if(stream.Read(probe)!=12)throw new ArgumentException("媒体文件不完整");stream.Position=0;
        if(kind=="IMAGE")return (Image(stream,probe),0);
        var parser=new Video(stream);return parser.Parse(probe);
    }
    private static void Dimensions(int width,int height){if(width is <1 or >8192||height is <1 or >8192||(long)width*height>25_000_000)throw new ArgumentException("图片分辨率过大或无效");}
    private static string Image(Stream s,byte[] probe)
    {
        var bytes=new byte[32];
        if(probe.AsSpan(0,8).SequenceEqual(new byte[]{137,80,78,71,13,10,26,10}))
        {
            s.Position=8;bool header=false,data=false,end=false;
            while(s.Position+12<=s.Length)
            {
                s.ReadExactly(bytes.AsSpan(0,8));int length=BinaryPrimitives.ReadInt32BigEndian(bytes);if(length<0||length>s.Length-s.Position-4)throw new ArgumentException("PNG 图片损坏");var type=System.Text.Encoding.ASCII.GetString(bytes,4,4);
                if(!header){if(type!="IHDR"||length!=13)throw new ArgumentException("PNG 图片无效");s.ReadExactly(bytes.AsSpan(0,13));Dimensions(BinaryPrimitives.ReadInt32BigEndian(bytes),BinaryPrimitives.ReadInt32BigEndian(bytes.AsSpan(4)));header=true;s.Position+=4;continue;}
                data|=type=="IDAT"&&length>0;if(type=="IEND"){end=length==0;break;}s.Position+=length+4;
            }
            if(!data||!end)throw new ArgumentException("PNG 图片不完整");return "image/png";
        }
        if(probe[0]==255&&probe[1]==216)
        {
            s.Position=s.Length-2;if(s.ReadByte()!=255||s.ReadByte()!=217)throw new ArgumentException("JPEG 图片不完整");s.Position=2;
            while(s.Position+4<s.Length)
            {
                if(s.ReadByte()!=255)continue;int marker=s.ReadByte();if(marker is 255 or 0 or 216)continue;if(marker==218)break;s.ReadExactly(bytes.AsSpan(0,2));int n=BinaryPrimitives.ReadUInt16BigEndian(bytes);if(n<2||n>s.Length-s.Position+2)break;
                if(marker is >=0xc0 and <=0xc3 or >=0xc5 and <=0xc7 or >=0xc9 and <=0xcb or >=0xcd and <=0xcf){if(n<7)break;s.ReadExactly(bytes.AsSpan(0,5));Dimensions(BinaryPrimitives.ReadUInt16BigEndian(bytes.AsSpan(3)),BinaryPrimitives.ReadUInt16BigEndian(bytes.AsSpan(1)));return "image/jpeg";}s.Position+=n-2;
            }
        }
        throw new ArgumentException("请选择完整的 JPEG 或 PNG 图片");
    }
    private sealed class Video(Stream s)
    {
        private readonly byte[] bytes=new byte[24];private bool header,segment,block,video,moov,mdat;private double scale=1_000_000,duration,ticks;
        private ulong UInt(int count){if(count is <1 or >8)throw new ArgumentException();s.ReadExactly(bytes.AsSpan(0,count));ulong value=0;for(int i=0;i<count;i++)value=(value<<8)|bytes[i];return value;}
        private ulong Vint(bool id,out bool unknown)
        {
            int first=s.ReadByte();if(first<=0)throw new ArgumentException();int length=1,mask=128;while((first&mask)==0){length++;mask>>=1;}if(length>(id?4:8))throw new ArgumentException();ulong value=(ulong)(id?first:first&(mask-1));for(int i=1;i<length;i++){int next=s.ReadByte();if(next<0)throw new ArgumentException();value=(value<<8)|(uint)next;}unknown=!id&&value==(1UL<<(length*7))-1;return value;
        }
        public (string Mime,double Duration) Parse(byte[] probe)
        {
            try
            {
                if(probe.AsSpan(0,4).SequenceEqual(new byte[]{0x1a,0x45,0xdf,0xa3}))
                {Ebml(s.Length,0,0);duration=Math.Max(duration,ticks*scale/1_000_000_000);if(!header||!segment||!block||!video)throw new ArgumentException();return("video/webm",ValidDuration());}
                if(System.Text.Encoding.ASCII.GetString(probe,4,4)=="ftyp")
                {Atoms(s.Length,0);if(!moov||!mdat||!video)throw new ArgumentException();return("video/mp4",ValidDuration());}
                throw new ArgumentException();
            }
            catch(Exception e)when(e is ArgumentException or EndOfStreamException or OverflowException){throw new ArgumentException("请选择完整的 MP4 或 WebM 视频");}
        }
        private double ValidDuration(){if(!double.IsFinite(duration)||duration<=0)throw new ArgumentException();return duration;}
        private void Ebml(long end,int depth,long cluster,ulong unknownMaster=0)
        {
            if(depth>12)throw new ArgumentException();while(s.Position<end)
            {
                long element=s.Position;ulong id=Vint(true,out _);
                // MediaRecorder leaves cluster sizes open. A following segment-level element closes that cluster.
                if(unknownMaster==0x1F43B675&&id is 0x1F43B675 or 0x1549A966 or 0x1654AE6B or 0x1C53BB6B or 0x114D9B74 or 0x1043A770 or 0x1941A469 or 0x1254C367||unknownMaster==0x18538067&&id==0x18538067){s.Position=element;return;}
                ulong size=Vint(false,out bool unknown);long start=s.Position,stop=unknown?end:checked(start+(long)size);if(stop>end||stop<start)throw new ArgumentException();
                if(id==0x1A45DFA3)header=true;if(id==0x18538067)segment=true;if(id==0x2AD7B1)scale=UInt(checked((int)size));
                if(id==0x4489){int n=checked((int)size);s.ReadExactly(bytes.AsSpan(0,n is 4 or 8?n:throw new ArgumentException()));duration=n==4?BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32BigEndian(bytes))*scale/1_000_000_000:BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64BigEndian(bytes))*scale/1_000_000_000;}
                if(id==0x83)video|=UInt(checked((int)size))==1;if(id==0xE7)cluster=checked((long)UInt(checked((int)size)));
                if(id is 0xA3 or 0xA1){Vint(false,out _);if(s.Position+3>stop)throw new ArgumentException();s.ReadExactly(bytes.AsSpan(0,2));ticks=Math.Max(ticks,cluster+BinaryPrimitives.ReadInt16BigEndian(bytes));block=true;}
                if(id is 0x1A45DFA3 or 0x18538067 or 0x1549A966 or 0x1654AE6B or 0xAE or 0x1F43B675 or 0xA0){Ebml(stop,depth+1,id==0x1F43B675?0:cluster,unknown?id:0);if(unknown)stop=s.Position;}s.Position=stop;
            }
        }
        private void Atoms(long end,int depth)
        {
            if(depth>12)throw new ArgumentException();while(s.Position<end)
            {
                long start=s.Position;if(end-start<8)throw new ArgumentException();s.ReadExactly(bytes.AsSpan(0,8));ulong size=BinaryPrimitives.ReadUInt32BigEndian(bytes);string type=System.Text.Encoding.ASCII.GetString(bytes,4,4);if(size==1)size=UInt(8);long stop=size==0?end:checked(start+(long)size);if(stop>end||stop<s.Position)throw new ArgumentException();
                if(type=="moov")moov=true;if(type=="mdat"&&stop>s.Position)mdat=true;
                if(type=="mvhd")
                {
                    int version=s.ReadByte();if(version is not (0 or 1))throw new ArgumentException();s.Position+=3+(version==1?16:8);if(s.Position+(version==1?12:8)>stop)throw new ArgumentException();double timescale=UInt(4),length=UInt(version==1?8:4);if(timescale<=0)throw new ArgumentException();duration=length/timescale;
                }
                if(type=="hdlr"){if(s.Position+12>stop)throw new ArgumentException();s.Position+=8;s.ReadExactly(bytes.AsSpan(0,4));video|=System.Text.Encoding.ASCII.GetString(bytes,0,4)=="vide";}
                if(type is "moov" or "trak" or "mdia")Atoms(stop,depth+1);s.Position=stop;
            }
        }
    }
}