FTP2?使用msgfiles不断发展的文件传输

介绍

早在2000年代初,当云只是天空中那些蓬松的白色东西时,我为一家旨在改变文件存储方式的初创公司工作。我们简单地称之为在线文件存储。您将上传文件,然后您可以在任何地方访问它们......并将它们发送给您的朋友。它不像Napster那样的文件共享;你没有与全世界分享你的CD收藏。但是您可以轻松地将CD收藏发送给您的姐姐。

从那时起,技术不断发展和改进,您可以提出这样的论点,即没有理由共享或发送文件,因为有一个繁忙的市场可以在线购买和租用文件访问权限。支付访问文件城堡的月费,并将您想要的任何东西发送给支付月费的其他人到同一个城堡,问题解决了。都是合法的,都是光明正大的。像Napster这样的东西永远不会被允许再存在。

那么这个msgfiles业务是怎么回事呢?msgfiles是关于局部性和简单性的。在您要与之传输文件的所有人都可以访问的地方运行服务器软件。然后让他们运行客户端,您可以轻松地将文件发送给彼此。将其视为改进的基于消息的FTP系统,重点是首字母缩略词的传输部分。它达到了电子邮件和FTP之间的最佳平衡点。电子邮件不适合处理大文件或大量文件。FTP不是基于消息的。我很想知道这对你有什么影响。

FTP2是一个煽动性的,宏伟的文章标题,但深入挖掘,你可能会明白为什么它不是一个太疯狂的命题。好吧,也许有点疯狂...

顺便说一句...

我已经有一段时间没有写太多C#了。

我回来后第一个脸红的是C#.NET库和LINQ上已经非常高效了。我认为新的人工智能东西太过分了,无法使用......它只是碍事。我编码是因为我想编码,而不是一直点击代码建议。自动完成是一个不费吹灰之力的切片面包,但人工智能必须去,我无法足够快地禁用它。

从阳光的角度来看,可空性业务起初很烦人,但它做得很好,并且是有目的的。它让我有点像Rust,编译器在你身边帮助你制作正确的程序。我喜欢。

所以有好有坏。

我仍然不认为C#是大型项目的好语言。世界正在远离这些,所以也许没关系。

msgfiles客户端应用程序

可以安装客户端并在 msgfiles.io 上查看屏幕截图演练。我不会在这里重复所有这些,足以说...

您启动客户端...

  1. 输入显示名称和您的电子邮件地址。
  2. 输入服务器地址,如FTP服务器。
  3. 服务器会向您的电子邮件地址发送登录代码。
  4. 你把它打进去,然后你就可以发送和接收消息。

要发送文件...

  1. 发送文件按钮。
  2. 选择要将文件发送给谁。
  3. 输入您的小消息。
  4. 选择要发送的文件。
  5. 然后客户端压缩文件...
  6. ...并将ZIP和消息发送到服务器。
  7. 服务器存储ZIP和消息...
  8. ...并使用访问令牌向收件人发送电子邮件。

当您收到一封电子邮件说您有文件时...

  1. 启动客户端。
  2. 连接到服务器。
  3. 接收文件按钮。
  4. 从电子邮件中复制访问令牌并将其粘贴到客户端中。
  5. 客户端向您显示消息来自谁以及小消息。
  6. 你盯着这个,要么打它,要么继续。
  7. 客户端下载ZIP并显示内容的清单。
  8. 你盯着这个,要么打它,要么继续。
  9. 然后,选择放置文件的位置,并在那里提取ZIP
  10. 任务完成!

这就是整个应用程序。两个大按钮和一些简单的对话框。简单易行!

它可能更漂亮。有人很快要去毛伊岛吗?

msgfiles服务器应用程序

您可以安装服务器并获取有关 msgfiles.io 的安装步骤和维护提示。

代码

msgfilesGitHub上的开源软件,具有Apache 2.0许可证。它都是.NET 6 C#在一个解决方案中,包括单元测试。

下面是解决方案中项目的概要。

有两个应用程序项目,clientserver。这些项目中的代码很少,只有顶级编排。

securenet

这个低级库包装了第三方依赖项,包括ZIPAESJSON。它还包括核心TLS代码,包括自签名证书生成和SslStream包装器函数。许多核心构建块,如SMTP包装类EmailClient和会话管理类SessionStore也在这里。

msglib

此库在客户端MsgClient和服务器端 MsgRequestHandler.cs 类中实现消息处理。核心类MessageStore也在这里。

client

一个基本的概念证明WinForms应用程序,它响应MsgClient事件以显示进度并提示用户输入令牌和确认。我想更性感的应用程序来取代这个程序;希望它们能像这个不起眼的开始一样简单易用。

server

用于在服务器端运行节目的命令行应用程序。服务器依赖于设置INI文件以及允许和阻止列表文件。命令行提示符使您可以轻松访问这些文件,服务器会选取一些更改并立即生效。

从理论上讲,您可以只采用securenet项目并开发自己的客户端-服务器应用程序。这有点像http2,它可以带你走得更远。

所以这是代码项目...

足够的民间传说和项目,让我们看看一些代码!

安全网络

msgfiles核心是基本的自签名安全网络,通过SslStream

/// Create a self-signed cert...in six lines of code
public static X509Certificate GenCert()
{
    using (RSA rsa = RSA.Create(4096))
    {
        var distinguishedName = new X500DistinguishedName($"CN=msgfiles.io");
        var request = new CertificateRequest(distinguishedName, rsa, 
                      HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), 
                          DateTimeOffset.UtcNow.AddDays(3650));
        return new X509Certificate2
               (certificate.Export(X509ContentType.Pfx, "password"), 
               "password", X509KeyStorageFlags.MachineKeySet);
    }
}

/// Given a client TCP connection, secure communications with the server
public static Stream SecureConnectionToServer(TcpClient client)
{
    var client_stream = client.GetStream();
    var ssl_stream = 
        new SslStream
        (
            client_stream,
            false,
            (object obj, X509Certificate? cert, X509Chain? chain, 
                         SslPolicyErrors errors) => true
        );
    ssl_stream.AuthenticateAsClient("msgfiles.io");
    if (!ssl_stream.IsAuthenticated)
        throw new NetworkException("Connection to server not authenticated");
    return ssl_stream;
}

/// Given a server TCP connection, secure communications with the client
public async static Task<Stream> SecureConnectionFromClient
             (TcpClient client, X509Certificate cert)
{
    var client_stream = client.GetStream();
    var ssl_stream = new SslStream(client_stream, false, 
    (object obj, X509Certificate? cert2, X509Chain? chain, 
                 SslPolicyErrors errors) => true);
    await ssl_stream.AuthenticateAsServerAsync(cert).ConfigureAwait(false);
    if (!ssl_stream.IsAuthenticated)
        throw new NetworkException("Connection from client not authenticated");
    return ssl_stream;
}

网络有效负载序列化

拥有安全网络后,您需要一种通过网络来回发送有效负载的机制。我选择让有效负载像HTTP一样,并压缩标头,以便可以将消息文本和收件人等重要内容放入其中。如果您的小消息和收件人列表加起来,压缩到超过64 KB......您可能更喜欢电子邮件!

public static int MaxObjectByteCount = 64 * 1024;

/// Given pretty much anything, 
/// turn it into JSON,
/// get the UTF-8 bytes,
/// compress the bytes,
/// make sure it isn't too big,
/// then send it over the stream, length-prefixed
public static void SendObject<T>(Stream stream, T headers)
{
    string json = JsonConvert.SerializeObject(headers);

    byte[] json_bytes = Utils.Compress(Encoding.UTF8.GetBytes(json));
    if (json_bytes.Length > MaxObjectByteCount)
        throw new InputException("Too much to send");

    byte[] num_bytes = BitConverter.GetBytes
           (IPAddress.HostToNetworkOrder(json_bytes.Length));

    using (var buffer = Utils.CombineArrays(num_bytes, json_bytes))
        stream.Write(buffer.GetBuffer(), 0, (int)buffer.Length);
}
public static async Task SendObjectAsync<T>(Stream stream, T headers)
...

/// Receive pretty much anything from a stream
/// Read the length, ensure it's not too much
/// Read the bytes, decompress, turn into a string, JSON parse,
/// and out comes an object
public static T ReadObject<T>(Stream stream)
{
    byte[] num_bytes = new byte[4];
    if (stream.Read(num_bytes, 0, num_bytes.Length) != num_bytes.Length)
        throw new SocketException();

    int bytes_length = 
        IPAddress.NetworkToHostOrder(BitConverter.ToInt32(num_bytes, 0));
    if (bytes_length > MaxObjectByteCount)
        throw new InputException("Too much to read");

    byte[] header_bytes = new byte[bytes_length];
    int read_yet = 0;
    while (read_yet < bytes_length)
    {
        int to_read = bytes_length - read_yet;
        int new_read = stream.Read(header_bytes, read_yet, to_read);
        if (new_read <= 0)
            throw new NetworkException("Connection closed");
        else
            read_yet += new_read;
    }

    string json = Encoding.UTF8.GetString
                  (Utils.Decompress(header_bytes, bytes_length));
    var obj = JsonConvert.DeserializeObject<T>(json);
    if (obj == null)
        throw new InputException("Input did not parse");
    else
        return obj;
}
public static async Task<T> ReadObjectAsync<T>(Stream stream)
...

存取控制

运行任何类型的服务器的一大主题是访问控制。我没有看到把这个服务器放在互联网上;这是一个内联网重头戏。也许您希望一个部门的服务器,而不希望其他部门对此胡思乱想。

因此,服务器有两个用于访问控制的文件,allow.txtblock.txt。您可以将完整的电子邮件地址或域名及其前缀@放在一起。如果你的电子邮件地址不在允许列表中,或者你被阻止,则无法连接,也无法向你发送任何内容。服务器已锁定。

以下是用于强制访问控制的代码:

/// Manage allow and block lists of email addresses
/// to validate that a given email address is allowed access
public class AllowBlock
{
    /// Swap in new lists
    public void SetLists(HashSet<string> allow, HashSet<string> block)
    {
        try
        {
            m_rwLock.EnterWriteLock();

            m_allowList = allow;
            m_blockList = block;
        }
        finally
        {
            m_rwLock.ExitWriteLock();
        }
    }

    /// Ensure that an email address or its domain is allowed,
    /// or at least not blocked
    public void EnsureEmailAllowed(string email)
    {
        try
        {
            m_rwLock.EnterReadLock();

            // Normalize the email address
            email = Utils.GetValidEmail(email).ToLower();
            if (email.Length == 0)
                throw new InputException($"Invalid email: {email}");

            // Include the leading @, list files use this to allow/block entire domains
            string domain = email.Substring(email.IndexOf('@')).ToLower();

            // Look for specific email address blocks first, that trumps all
            if (m_blockList.Contains(email))
                throw new InputException($"Blocked email: {email}");

            // Check for specific email address being allowed, this trumps domains
            if (m_allowList.Contains(email))
                return;

            // Check for a whole blocked domain
            if (m_blockList.Contains(domain))
                throw new InputException($"Blocked domain: {email}");

            // Allow a whole domain
            if (m_allowList.Contains(domain))
                return;

            // Failing all of that, if there is an allow list,
            // the email is not on any of them, so they're blocked by default
            if (m_allowList.Count > 0)
                throw new InputException($"Not allowed: {email}");

            // no allow list, not blocked -> allowed
        }
        finally
        {
            m_rwLock.ExitReadLock();
        }
    }

    private HashSet<string> m_allowList = new HashSet<string>();
    private HashSet<string> m_blockList = new HashSet<string>();

    private ReaderWriterLockSlim m_rwLock = new ReaderWriterLockSlim();
}

发送电子邮件

msgfiles另一个主题是发送电子邮件。System.Net.SmtpClient很有趣。它不是线程安全的。它有一个非async/await SendAsync函数。文档建议在后台有网络连接池,所以让我们为每个消息创建一个SmtpClient,并使用SendAsync函数:

/// SmtpClient wrapper class
public class EmailClient
{
    public EmailClient(string server, int port, string username, string password)
    {
        m_server = server;
        m_port = port;
        m_credential = new NetworkCredential(username, password);
    }

    public void SendEmail
    (
        string from, // display <email> or just email
        Dictionary<string, string> toAddrs, // email -> display
        string subject,
        string body
    )
    {
        var fromKvp = Utils.ParseEmail(from);

        var mail_message = new MailMessage();
        mail_message.From = new MailAddress(fromKvp.Key, fromKvp.Value);

        foreach (var toKvp in toAddrs)
            mail_message.To.Add(new MailAddress(toKvp.Key, toKvp.Value));

        mail_message.Subject = subject;
        mail_message.Body = body;

        SmtpClient client = new SmtpClient(m_server, m_port);
        client.Credentials = m_credential;
        client.DeliveryMethod = SmtpDeliveryMethod.Network;
        client.EnableSsl = true;
        client.SendAsync(mail_message, null);
    }

    private string m_server;
    private int m_port;
    private NetworkCredential m_credential;
}

压缩文件处理

ZIP文件是此应用程序的核心。我围绕DotNetZip NuGet包创建了几个包装函数:

/// Create a ZIP file from files and folders to include
public static void CreateZip(IClientApp app, string zipFilePath, IEnumerable<string> paths)
{
    using (var zip = new Ionic.Zip.ZipFile(zipFilePath))
    {
        zip.CompressionLevel = Ionic.Zlib.CompressionLevel.BestSpeed;
        string lastZipCurrentFilename = "";
        zip.SaveProgress +=
            (object? sender, Ionic.Zip.SaveProgressEventArgs e) =>
            {
                if (e.CurrentEntry != null && 
                    e.CurrentEntry.FileName != lastZipCurrentFilename)
                {
                    lastZipCurrentFilename = e.CurrentEntry.FileName;
                    app.Log(lastZipCurrentFilename);
                }

                if (e.TotalBytesToTransfer > 0)
                    app.Progress((double)e.BytesTransferred / e.TotalBytesToTransfer);
            };
        foreach (var path in paths)
        {
            if (File.Exists(path))
                zip.AddFile(path, "");
            else if (Directory.Exists(path))
                zip.AddDirectory(path, Path.GetFileName(path));
            else
                throw new InputException($"Item to send not found: {path}");
        }

        zip.Save();
    }
}

/// Summarize the contents of a ZIP file for the benefit of having an idea
/// whether they are what is expected, and safe
public static string ManifestZip(string zipFilePath)
{
    int file_count = 0;
    long total_byte_count = 0;

    StringBuilder entry_lines = new StringBuilder();
            
    Dictionary<string, int> ext_counts = new Dictionary<string, int>();
            
    using (var zip_file = new Ionic.Zip.ZipFile(zipFilePath))
    {
        foreach (var zip_entry in zip_file.Entries)
        {
            if (zip_entry.IsDirectory)
                continue;

            string size_str = 
                Utils.ByteCountToStr(zip_entry.UncompressedSize);
            entry_lines.AppendLine($"{zip_entry.FileName} ({size_str})");

            string ext = Path.GetExtension(zip_entry.FileName).ToUpper();
            if (ext_counts.ContainsKey(ext))
                ++ext_counts[ext];
            else
                ext_counts[ext] = 1;

            ++file_count;
            total_byte_count += zip_entry.UncompressedSize;
        }
    }

    string ext_summary =
        "File Types:\r\n" +
        string.Join
        (
            "\r\n",
            ext_counts
                .Select(kvp => $"{kvp.Key.Trim('.')}: {kvp.Value}")
                .OrderBy(str => str)
        );

    return
        $"Files: {file_count}" +
        $" - " +
        $"Total: {Utils.ByteCountToStr(total_byte_count)}" +
        $"\r\n\r\n" +
        $"{ext_summary}" +
        $"\r\n\r\n" +
        $"{entry_lines}";
}

/// Extract a ZIP file's contents into an output directory
public static void ExtractZip
       (IClientApp app, string zipFilePath, string extractionDirPath)
{
    using (var zip = new Ionic.Zip.ZipFile(zipFilePath))
    {
        string lastZipCurrentFilename = "";
        zip.ExtractProgress +=
            (object? sender, Ionic.Zip.ExtractProgressEventArgs e) =>
            {
                if (e.CurrentEntry != null && 
                    e.CurrentEntry.FileName != lastZipCurrentFilename)
                {
                    lastZipCurrentFilename = e.CurrentEntry.FileName;
                    app.Log(lastZipCurrentFilename);
                }

                if (e.TotalBytesToTransfer > 0)
                    app.Progress((double)e.BytesTransferred / e.TotalBytesToTransfer);
            };
        zip.ExtractAll(extractionDirPath);
    }
}

客户端消息发送

食物链的上游是用于发送消息的客户端代码:

/// Send a message with files to recipients
public bool SendMsg
(
    IEnumerable<string> to, 
    string message, 
    IEnumerable<string> paths
)
{
    using (var temp_file_use = new TempFileUse(".zip"))
    {
        string zip_file_path = temp_file_use.FilePath;

        App.Log("Adding files to package...");
        Utils.CreateZip(App, zip_file_path, paths);

        App.Log("Scanning package...");
        string zip_hash;
        using (var fs = File.OpenRead(zip_file_path))
            zip_hash = Utils.HashStream(fs);

        App.Log("Sending message...");
        long zip_file_size_bytes = new FileInfo(zip_file_path).Length;
        var send_request =
            new ClientRequest()
            {
                version = 1,
                verb = "POST",
                contentLength = zip_file_size_bytes,
                headers = new Dictionary<string, string>()
                {
                    { "to", string.Join("; ", to) },
                    { "message", message },
                    { "hash", zip_hash }
                }
            };
        if (ServerStream == null)
            return false;
        SecureNet.SendObject(ServerStream, send_request);

        App.Log("Sending package...");
        using (var zip_file_stream = File.OpenRead(zip_file_path))
        {
            long sent_yet = 0;
            byte[] buffer = new byte[64 * 1024];
            while (sent_yet < zip_file_size_bytes)
            {
                int to_read = 
                (int)Math.Min(zip_file_size_bytes - sent_yet, buffer.Length);
                int read = zip_file_stream.Read(buffer, 0, to_read);
                if (App.Cancelled)
                    return false;

                if (ServerStream == null)
                    return false;
                ServerStream.Write(buffer, 0, read);

                sent_yet += read;

                App.Progress((double)sent_yet / zip_file_size_bytes);
                if (App.Cancelled)
                    return false;
            }
        }
        if (App.Cancelled)
            return false;

        App.Log("Receiving response...");
        using (var send_response = SecureNet.ReadObject<ServerResponse>(ServerStream))
        {
            App.Log($"Server Response: {send_response.ResponseSummary}");
            if (send_response.statusCode / 100 != 2)
                throw send_response.CreateException();
        }

        return true;
    }
}

客户端消息接收

下面是用于接收消息的客户端代码:

/// Given a message token, get a message for the current user
/// Returns true if getting the message succeeded
/// Sets shouldDelete to true if the user canceled the operation
public bool GetMessage(string msgToken, out bool shouldDelete)
{
    shouldDelete = false;

    {
        App.Log("Sending GET msg request...");
        var request =
            new ClientRequest()
            {
                version = 1,
                verb = "GET",
                headers =
                    new Dictionary<string, string>()
                    { { "token", msgToken }, { "part", "msg"} }
            };
        if (ServerStream == null)
            return false;
        SecureNet.SendObject(ServerStream, request);
        if (App.Cancelled)
            return false;

        App.Log("Receiving GET msg response...");
        if (ServerStream == null)
            return false;
        using (var response = SecureNet.ReadObject<ServerResponse>(ServerStream))
        {
            App.Log($"Server Response: {response.ResponseSummary}");
            if (response.statusCode / 100 != 2)
                throw response.CreateException();

            msg? m = JsonConvert.DeserializeObject<msg>(response.headers["msg"]);
            string status = m == null ? "(null)" : m.from;
            App.Log($"Message: {status}");
            if (m == null)
                return false;
            else
                msgToken = m.token;

            if (!App.ConfirmDownload(m.from, m.message, out shouldDelete))
                return false;
        }
    }

    {
        App.Log("Sending GET file request...");
        var request =
            new ClientRequest()
            {
                version = 1,
                verb = "GET",
                headers =
                    new Dictionary<string, string>()
                    { { "token", msgToken }, { "part", "file"} }
            };
        if (ServerStream == null)
            return false;
        SecureNet.SendObject(ServerStream, request);
        if (App.Cancelled)
            return false;

        App.Log("Receiving GET file response...");
        if (ServerStream == null)
            return false;
        using (var response = SecureNet.ReadObject<ServerResponse>(ServerStream))
        {
            App.Log($"Server Response: {response.ResponseSummary}");
            if (response.statusCode / 100 != 2)
                throw response.CreateException();

            using (var temp_file_use = new TempFileUse(".zip"))
            {
                string temp_file_path = temp_file_use.FilePath;

                App.Log($"Downloading files...");
                if (App.Cancelled)
                    return false;
                using (var fs = File.OpenWrite(temp_file_path))
                {
                    long total_to_read = response.contentLength;
                    long read_yet = 0;
                    byte[] buffer = new byte[64 * 1024];
                    while (read_yet < total_to_read)
                    {
                        int to_read = (int)Math.Min(total_to_read - read_yet, 
                                       buffer.Length);
                        if (ServerStream == null)
                            return false;
                        int read = ServerStream.Read(buffer, 0, to_read);
                        if (App.Cancelled)
                            return false;

                        if (read == 0)
                            throw new NetworkException("Connection lost");
                        fs.Write(buffer, 0, read);
                        if (App.Cancelled)
                            return false;

                        read_yet += read;

                        App.Progress((double)read_yet / total_to_read);
                    }
                }

                App.Log($"Scanning downloaded files...");
                if (App.Cancelled)
                    return false;
                string local_hash;
                using (var fs = File.OpenRead(temp_file_path))
                    local_hash = Utils.HashStream(fs);
                if (App.Cancelled)
                    return false;
                if (local_hash != response.headers["hash"])
                    throw new NetworkException("File transmission error");

                App.Log($"Examining downloaded files...");
                string manifest = Utils.ManifestZip(temp_file_path);
                if (App.Cancelled)
                    return false;

                string extraction_dir_path = "";
                if (!App.ConfirmExtraction(manifest, out shouldDelete, 
                                           out extraction_dir_path))
                    return false;

                App.Log($"Saving downloaded files...");
                Utils.ExtractZip(App, temp_file_path, extraction_dir_path);

                App.Log($"All done.");
                return true;
            }
        }
    }
}

服务器消息发送

在服务器端,以下是处理用户发送消息的代码:

private async Task<ServerResponse> HandleSendRequestAsync
        (ClientRequest request, HandlerContext ctxt)
{
    // Unpack the message
    Utils.NormalizeDict
    (
        request.headers,
        new[]
        { "to", "message", "packageHash" }
    );

    string to = request.headers["to"];
    if (to == "")
        throw new InputException("Header missing: to");

    string message = request.headers["message"];
    if (message == "")
        throw new InputException("Header missing: message");

    long package_size_bytes = request.contentLength;
    if
    (
        MaxSendPayloadMB > 0
        &&
        package_size_bytes / 1024 / 1024 > MaxSendPayloadMB
    )
    {
        throw new InputException("Header invalid: package too big");
    }

    string sent_zip_hash = request.headers["hash"];
    if (sent_zip_hash == "")
        throw new InputException("Header missing: hash");

    Log(ctxt, $"Sending: To: {to}");

    using (var temp_file_use = new TempFileUse(".zip"))
    {
        string stored_file_path = "";
        string temp_zip_file_path = temp_file_use.FilePath;
        try
        {
            Log(ctxt, $"Saving ZIP: {temp_zip_file_path}");
            using (var zip_file_stream = File.OpenWrite(temp_zip_file_path))
            {
                long written_yet = 0;
                byte[] buffer = new byte[64 * 1024];
                while (written_yet < package_size_bytes)
                {
                    int to_read = (int)Math.Min
                                  (package_size_bytes - written_yet, buffer.Length);
                    int read = await ctxt.ConnectionStream.ReadAsync
                               (buffer, 0, to_read).ConfigureAwait(false);
                    if (read == 0)
                        throw new NetworkException("Connection lost");
                    await zip_file_stream.WriteAsync
                          (buffer, 0, read).ConfigureAwait(false);
                    written_yet += read;
                }
            }

            Log(ctxt, $"Hashing ZIP");
            string local_zip_hash;
            using (var zip_file_stream = File.OpenRead(temp_zip_file_path))
                local_zip_hash = await Utils.HashStreamAsync
                                 (zip_file_stream).ConfigureAwait(false);
            if (local_zip_hash != sent_zip_hash)
                throw new InputException("Received file contents do not match 
                                          what was sent");

            Log(ctxt, $"Storing ZIP");
            stored_file_path = m_fileStore.StoreFile(temp_zip_file_path);
            File.Delete(temp_zip_file_path);
            temp_zip_file_path = "";
            temp_file_use.Clear();

            Log(ctxt, $"Storing messages");
            string email_from = $"{ctxt.Auth["display"]} <{ctxt.Auth["email"]}>";
            var toos = to.Split(';').Select(t => t.Trim()).Where(t => t.Length > 0);
            foreach (var too in toos)
            {
                string token = 
                    m_msgStore.StoreMessage
                    (
                        new msg()
                        {
                            from = email_from,
                            to = too,
                            message = message
                        },
                        stored_file_path,
                        local_zip_hash
                    );

                    Log(ctxt, $"Sending email");
                    ctxt.App.SendDeliveryMessage
                    (
                        email_from,
                        too,
                        message,
                        token
                    );
            }
            stored_file_path = "";

            return
                new ServerResponse()
                {
                    version = 1,
                    statusCode = 200,
                    statusMessage = "OK"
                };
        }
        finally
        {
            if (stored_file_path != "" && File.Exists(stored_file_path))
                File.Delete(stored_file_path);
        }
    }
}

服务器消息接收

以下是用于处理接收消息的用户的服务器代码:

private async Task<ServerResponse> HandleGetRequestAsync
              (ClientRequest request, HandlerContext ctxt)
{
    string to = ctxt.Auth["email"];
    m_allowBlock.EnsureEmailAllowed(to);

    Utils.NormalizeDict(request.headers, new[] { "token", "part" });

    string token = request.headers["token"];
    if (token.Length == 0)
        throw new InputException("Header missing: token");

    string part_to_get = request.headers["part"];
    if (part_to_get.Length == 0)
        throw new InputException("Header missing: part");

    bool get_msg = false, get_file = false;
    if (part_to_get == "msg")
        get_msg = true;
    else if (part_to_get == "file")
        get_file = true;
    else
        throw new InputException("Invalid header: part");

    Log(ctxt, $"Get Message: {to} - {token} - {part_to_get}");

    string package_file_path, package_file_hash;
    var msg = 
        m_msgStore.GetMessage
        (to, token, out package_file_path, out package_file_hash);

    if (get_msg)
    {
        if (msg == null)
        {
            var response_404 =
                new ServerResponse()
                {
                    version = 1,
                    statusCode = 404,
                    statusMessage = "Message Not Found"
                };
            return response_404;
        }

        var response =
            new ServerResponse()
            {
                version = 1,
                statusCode = 200,
                statusMessage = "OK",
                headers =
                    new Dictionary<string, string>()
                    { { "msg", JsonConvert.SerializeObject(msg) } },
            };
        await Task.FromResult(0);
        return response;
    }
    else if (get_file)
    {
        if (!File.Exists(package_file_path))
        {
            var response_404 =
                new ServerResponse()
                {
                    version = 1,
                    statusCode = 404,
                    statusMessage = "File Not Found"
                };
            return response_404;
        }

        var response =
            new ServerResponse()
            {
                version = 1,
                statusCode = 200,
                statusMessage = "OK",
                contentLength = new FileInfo(package_file_path).Length,
                headers =
                    new Dictionary<string, string>()
                    { { "hash", package_file_hash } },
                streamToSend = File.OpenRead(package_file_path)
            };
        await Task.FromResult(0);
        return response;
    }
    else
        throw new InputException("Invalid header: part");
}

服务器应用程序启动

仍然在服务器端,这里是服务器的启动代码,您可以看到它们是如何组合在一起的:

public ServerApp()
{
    string settings_file_path = Path.Combine(AppDocsDirPath, "settings.ini");
    if (!File.Exists(settings_file_path))
        throw new InputException
              ($"settings.ini file does not exist in {AppDocsDirPath}");
            
    m_settings = new Settings(settings_file_path);

    if (!int.TryParse
    (
        m_settings.Get("application", "MaxSendPayloadMB"),
        out MsgRequestHandler.MaxSendPayloadMB
    ))
    {
        throw new InputException("Invalid setting: MaxSendPayloadMB");
    }

    if (!int.TryParse
    (
        m_settings.Get("application", "ReceiveTimeoutSeconds"),
        out Server.ReceiveTimeoutSeconds
    ))
    {
        throw new InputException("Invalid setting: ReceiveTimeoutSeconds");
    }

    if (!int.TryParse
    (
        m_settings.Get("application", "ServerPort"),
        out ServerPort
    ))
    {
        throw new InputException("Invalid setting: ServerPort");
    }

    m_settingsWatcher = new FileSystemWatcher(AppDocsDirPath, "*.ini");
    m_settingsWatcher.Changed += SettingsWatcher_Changed;
    m_settingsWatcher.Created += SettingsWatcher_Changed;
    m_settingsWatcher.Deleted += SettingsWatcher_Changed;
    SettingsWatcher_Changed(new object(), 
            new FileSystemEventArgs(WatcherChangeTypes.All, "", null));

    m_txtFilesWatcher = new FileSystemWatcher(AppDocsDirPath, "*.txt");
    m_txtFilesWatcher.Changed += TextWatcher_Changed;
    m_txtFilesWatcher.Created += TextWatcher_Changed;
    m_txtFilesWatcher.Deleted += TextWatcher_Changed;
    TextWatcher_Changed(new object(), 
                new FileSystemEventArgs(WatcherChangeTypes.All, "", null));

    m_sessions = new SessionStore(Path.Combine(AppDocsDirPath, "sessions.db"));

    m_messageStore = new MessageStore(Path.Combine(AppDocsDirPath, "messages.db"));

    m_fileStore = new FileStore(m_settings.Get("application", "FileStoreDir"));

    m_logStore = new LogStore(Path.Combine(AppDocsDirPath, "logs"), "raw");
    m_accessStore = new LogStore(Path.Combine(AppDocsDirPath, "logs"), "access");

    string mail_server = m_settings.Get("application", "MailServer");
    if (string.IsNullOrWhiteSpace(mail_server))
        throw new InputException("Invalid setting: MailServer");

    int mail_port;
    if (!int.TryParse
    (
        m_settings.Get("application", "MailPort"),
        out mail_port
    ))
    {
        throw new InputException("Invalid setting: MailPort");
    }

    m_emailClient =
        new EmailClient
        (
            mail_server,
            mail_port,
            m_settings.Get("application", "MailUsername"),
            m_settings.Get("application", "MailPassword")
        );

    m_maintenanceTimer = new Timer(MaintenanceTimer, null, 0, 60 * 1000);

    var to_kvp = Utils.ParseEmail(m_settings.Get("application", "MailAdminAddress"));
    m_emailClient.SendEmail
    (
        m_settings.Get("application", "MailFromAddress"),
        new Dictionary<string, string>() { { to_kvp.Key, to_kvp.Value } },
        "Server Started Up",
        "So far so good..."
    );
}

允许阻止列表文件加载程序

最后,这是用于加载允许和阻止列表文本文件的函数。LINQ可能很慢,但它确实很漂亮,所以如果速度不是首要问题,那就去做吧!

private HashSet<string> LoadFileList(string fileName)
{
    string file_path = Path.Combine(AppDocsDirPath, fileName);
    if (File.Exists(file_path))
    {
        return
            new HashSet<string>
            (
                File.ReadAllLines(file_path)
                .Select(e => e.Trim().ToLower())
                .Where(e => e.Length > 0 && e[0] != '#')
            );
    }
    else
        return new HashSet<string>();
}

结论

嗯,这是对.NET客户端-服务器Intranet应用程序的旋风之旅,我希望您喜欢它。

我最近没有写太多C#/.NET,所以请用新奇的做事方式启发我和我的恐龙同胞。

如果在阅读了所有这些内容后,您认为这是您想尝试的东西,请向我发送一个电子邮件地址或几个您想访问演示服务器的电子邮件地址,并 contact@msgfiles.io 一条简短的我不是机器人消息,我会为您设置。只发送一个地址是可以的,您可以像电子邮件一样将文件发送给自己。您发送的地址应来自与地址具有相同域的地址。

https://www.codeproject.com/Articles/5341953/ftp2-Evolving-File-Transfer-with-msgfiles

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值