一個簡單的文件服務器實現方案


引子

最近公司的系統約來越多,基本上每個系統都需要用到“資源”,以前只是簡單的把“資源”放到Web服務器中,但是這樣的話有一個頭痛的問題----如何去管理“資源”?

想法

現在不是很流行API嘛,大家好像都在整什么面向服務、面向資源、RESTful什么的,據說在與復雜性的斗爭中,人們討論表象化狀態轉移(REST)已成為了一種時尚!我對這些概念也就是知道個大概,但是也不能解釋的很清楚,但是用意和優點還是很明確的!說白了就是各式各樣的“API”,可能我的理解有偏差,還望大家海涵,哈哈!

HTTP中有幾個常見謂詞,分別是GET/POST/PUT/DELETE,這也正是對應了我們經常說到的CRUD,含義就是對一個資源的增刪改查!

那咱能不能來一個文件API呢?實現對一個一個文件的CRUD?

說時遲,那時快

既然有了想法,咱就得開始干了!那么接下來的問題又來了,怎么干?

文件的增刪改查很簡單,基本功唄!

數據格式?字節數組吧,不用轉來轉去,

數據傳輸呢?就跟一般的API一樣走HTTP協議,HTTP請求報文中分為兩個部分:請求頭和請求體,既然這樣,正好符合我們的需求,請求體承載文件流的字節數組,請求頭中附加一些額外的信息!

說到這,基本上大概的“形狀”就有了!那咱就開始干!!!

添加一個Web應用程序作為服務端,WebForms或者Mvc的都可以。我這里演示的是Mvc的!

不廢話,先上代碼(只有上傳操作),待會大概解釋一下。

// ***********************************************************************
// Project            : Beimu.Bfs
// Assembly         : Beimu.Bfs.Web
// Author           : iceStone
// Created          : 2014年01月03日 10:23
//
// Last Modified By : iceStone
// Last Modified On : 2014年01月03日 10:23
// ***********************************************************************
// <copyright file="DefaultController.cs" company="Wedn.Net">
//     Copyright (c) Wedn.Net. All rights reserved.
// </copyright>
// <summary></summary>
// ***********************************************************************
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Web;
using System.Web.Mvc;
using Beimu.Bfs.Web.Utilities;

namespace Beimu.Bfs.Web.Controllers
{
    /// <summary>
    /// 默認控制器.
    /// </summary>
    /// <remarks>
    ///  2014年01月03日 10:23 Created By iceStone
    /// </remarks>
    public class DefaultController : Controller
    {
        /// <summary>
        /// 身份驗證
        /// </summary>
        /// <param name="filterContext"></param>
        protected override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var uid = Request.Headers["bfs-uid"];// Request["uid"];
            var pwd = Request.Headers["bfs-pwd"];//Request["pwd"];
            if (!(string.IsNullOrEmpty(uid) || string.IsNullOrEmpty(pwd)))
            {
                var user = new Users();
                if (!user.Exist(uid) || user[uid] != pwd)
                {
                    filterContext.Result = Json(new { Status = 400, Message = "用戶名或密碼不正確" });
                    return;
                }
            }
            base.OnActionExecuting(filterContext);
        }

        /// <summary>
        /// 上傳操作.
        /// </summary>
        /// <remarks>
        ///  2014年01月03日 10:23 Created By iceStone
        /// </remarks>
        /// <returns>ActionResult.</returns>
        public ActionResult Upload()
        {
            #region 批量
            //var files = Request.Files;
            //if (files == null || files.Count == 0) return Json(new { Status = 101, Message = "" });
            //var dict = new Dictionary<int, string>();
            //int index = -1;
            //foreach (HttpPostedFile file in files)
            //{
            //    index++;
            //    if (file == null || file.ContentLength == 0) continue;
            //    var ext = Path.GetExtension(file.FileName);
            //    if (!Config.UploadAllowType.Contains(ext)) continue;
            //    if (file.ContentLength >= (Config.UploadMaxSize * 1024 * 1024)) continue;

            //    string root = AppDomain.CurrentDomain.BaseDirectory;
            //    string path = string.Format("{0}/{1}/{2}/{3}", Config.UploadRoot, dir, DateTime.Now.ToString("yyyy"), DateTime.Now.ToString("MM"));
            //    string filename = GetStreamMd5(file.InputStream) + ext;//Path.GetFileName(file.FileName);
            //    file.SaveAs(Path.Combine(root, path, filename));

            //    dict.Add(index, Config.SiteUrl + path + filename);
            //}
            //return Json(dict); 
            #endregion

            string ext = Request.Headers.AllKeys.Contains("bfs-ext") ? Request.Headers["bfs-ext"] : ".jpg";
            var dir = Request.Headers.AllKeys.Contains("bfs-dir") ? Request.Headers["bfs-dir"] : Request.Headers["bfs-uid"];
            //dir = string.IsNullOrEmpty(dir) ? "common" : dir;

            using (var stream = Request.InputStream)
            {
                //var files = Request.Files;
                if (stream.Length == 0) return SetHeaders(104, "上傳文件為空");

                if (stream.Length >= (Config.UploadMaxSize * 1024 * 1024)) return SetHeaders(101, "上傳文件過大");

                //string root = AppDomain.CurrentDomain.BaseDirectory;
                string path = string.Format("/{0}/{1}/{2}/{3}/", Config.UploadRoot, dir, DateTime.Now.ToString("yyyy"), DateTime.Now.ToString("MM"));
                string filename = GetStreamMd5(stream) + ext;//Path.GetFileName(file.FileName);
                string fullPath = Server.MapPath(path);
                if (!Directory.Exists(fullPath))
                    Directory.CreateDirectory(fullPath);
                //var buffer = new byte[stream.Length];
                //stream.Read(buffer, 0, buffer.Length);  //將流的內容讀到緩沖區
                //using (var fs = new FileStream(fullPath + filename, FileMode.CreateNew, FileAccess.Write))
                //{
                //    fs.Write(buffer, 0, buffer.Length);
                //    fs.Flush();
                //    //fs.Close();
                //} 
                //using (var reader=new StreamReader(stream))
                //{
                //    using (var writer = new StreamWriter(fullPath + filename, false))
                //    {
                //        writer.Write(reader.ReadToEnd());
                //        writer.Flush();
                //    }
                //}
                using (var fs = new FileStream(fullPath + filename, FileMode.Create))
                {
                    byte[] bytes = new byte[stream.Length];
                    int numBytesRead = 0;
                    int numBytesToRead = (int)stream.Length;
                    stream.Position = 0;
                    while (numBytesToRead > 0)
                    {
                        int n = stream.Read(bytes, numBytesRead, Math.Min(numBytesToRead, int.MaxValue));
                        if (n <= 0)
                            break;
                        fs.Write(bytes, numBytesRead, n);
                        numBytesRead += n;
                        numBytesToRead -= n;
                    }
                    fs.Close();
                }
                return SetHeaders(100, Config.SiteUrl + path + filename);
            }

            //if (file == null || file.ContentLength == 0) return SetHeaders(103, "上傳文件為空");

            //var ext = Path.GetExtension(file.FileName);
            //if (!Config.UploadAllowType.Contains(ext)) return SetHeaders(102, "上傳非法文件");
            //if (file.ContentLength >= (Config.UploadMaxSize * 1024 * 1024)) return SetHeaders(101, "上傳文件過大");

            //string root = AppDomain.CurrentDomain.BaseDirectory;
            //string path = string.Format("{0}/{1}/{2}/{3}", Config.UploadRoot, dir, DateTime.Now.ToString("yyyy"), DateTime.Now.ToString("MM"));
            //string filename = GetStreamMd5(file.InputStream) + ext;//Path.GetFileName(file.FileName);
            //string fullPath = Path.Combine(root, path);
            //if (!Directory.Exists(fullPath))
            //    Directory.CreateDirectory(fullPath);
            //file.SaveAs(Path.Combine(root, path, filename));
            //return SetHeaders(100, Config.SiteUrl + path + filename);
        }

        [NonAction]
        public ContentResult SetHeaders(int status, string resault)
        {
            Response.Headers.Add("bfs-status", status.ToString());
            Response.Headers.Add("bfs-result", resault);
            return Content(string.Empty);
        }

        /// <summary>
        /// 獲取文件的MD5值
        /// </summary>
        /// <remarks>
        ///  2013年11月28日 19:24 Created By 汪磊
        /// </remarks>
        /// <param name="stream">文件流</param>
        /// <returns>該文件的MD5值</returns>
        [NonAction]
        public String GetStreamMd5(Stream stream)
        {
            var oMd5Hasher = new MD5CryptoServiceProvider();
            byte[] arrbytHashValue = oMd5Hasher.ComputeHash(stream);
            //由以連字符分隔的十六進制對構成的String,其中每一對表示value 中對應的元素;例如“F-2C-4A”
            string strHashData = BitConverter.ToString(arrbytHashValue);
            //替換-
            strHashData = strHashData.Replace("-", "");
            string strResult = strHashData;
            return strResult;
        }

    }
}

看完代碼的同學估計已經發現,其實實現的過程也有不順利,其實剛開始就是沒有想好數據放在請求報文的什么位置,最初嘗試了附件的形式,的確可以實現,但是出於某種原因(下面會說到)還是折騰成現在的樣子了!

大概理一下流程:

  1. 首先一個請求過來,校驗請求頭中身份信息是否存在並且合法,很簡單!我就是讀了配置文件中定義的用戶列表。可以做擴展;
  2. 獲取文件類型(請求頭中的信息)和文件存放的目錄(如果不存在目錄參數,默認以當前請求的用戶名作為基本目錄);
  3. 判斷請求體中有沒有內容,沒有內容返回錯誤碼和錯誤消息;
  4. 判斷上傳文件格式是否為允許格式(有個問題“客戶端欺騙服務端”,因為文件格式是客戶端在請求頭中注明的,但是回頭想想好像沒什么大問題,如果客戶端偽裝拓展名,最終按照他偽裝的拓展名保存,當然這里大家可以辦法提出更好更安全的想法),不允許返回錯誤碼和錯誤消息;
  5. 判斷文件大小是否允許,不允許返回錯誤碼和錯誤消息;
  6. 獲取文件流的MD5值作為文件名(防止重復資源占用空間,網盤就是這么干的,所以說所謂的多少T,所謂的秒傳,都是扯淡),
  7. 最后以/{指定目錄}/{年份}/{月份}/{文件名}{拓展名}保存到服務器!(如果有需要做成集群的話,那就需要我們再做一些拓展了,以后可以詳細介紹,實際上就是弄個數據庫弄個表保存一下每個服務器的資源情況,以及資源的保存位置)
  8. 最后返回給客戶端文件的地址

這樣我們最簡單的文件API就完成了,當然了它是很簡陋的,但是我更在乎的是思想上的問題,方向對不對!其實咱們做這一行最關鍵的就是這么點事,技術好不好都是時間的事!

好像還少點啥

怎么覺着好像還是少點東西呢?

一般的API還有什么東西?-------客戶端吶!總不能讓使用者自己做吧,那樣的結果只會有各種各樣的情況產生。

那再來個客戶端唄!

好的!

還是上代碼吧?

using System;
using System.Configuration;
using System.IO;
using System.Net;

namespace Beimu.Bfs.Lib
{
    public class BfsUploader
    {
        public readonly string ApiDomain = ConfigurationManager.AppSettings["bfs_server"];
        public string Directory { get; set; }
        public string UserId { get; set; }
        public string Password { get; set; }

        /// <summary>
        /// 構造函數
        /// </summary>
        /// <param name="uid">用戶名</param>
        /// <param name="pwd">密碼</param>
        /// <param name="dir">操作目錄</param>
        public BfsUploader(string uid, string pwd, string dir = "common")
        {
            Directory = dir;
            UserId = uid;
            Password = pwd;
        }

        /// <summary>
        /// 寫文件
        /// </summary>
        /// <param name="filePath">文件路徑</param>
        /// <param name="result">結果</param>
        /// <returns>是否成功</returns>
        public bool WriteFile(string filePath, out string result)
        {
            var ext = Path.GetExtension(filePath);
            using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
            {
                using (var reader = new BinaryReader(fs))
                {
                    byte[] postData = reader.ReadBytes((int)fs.Length);
                    return Post("upload", ext, postData, out result);
                }
            }
        }
        /// <summary>
        /// 寫文件
        /// </summary>
        /// <param name="ext">拓展名</param>
        /// <param name="postData">請求體</param>
        /// <param name="result">結果</param>
        /// <returns>是否成功</returns>
        public bool WriteFile(string ext, byte[] postData, out string result)
        {
            return Post("upload",ext, postData, out result);
        }

        /// <summary>
        /// 發送請求
        /// </summary>
        /// <param name="url">請求地址</param>
        /// <param name="ext">拓展名</param>
        /// <param name="postData">請求體</param>
        /// <param name="result">結果</param>
        /// <returns>是否成功</returns>
        private bool Post(string url, string ext, byte[] postData, out string result)
        {
            var request = (HttpWebRequest)WebRequest.Create(ApiDomain + url);

            request.Method = "POST";
            request.Headers.Add("bfs-uid", UserId);
            request.Headers.Add("bfs-pwd", Password);
            request.Headers.Add("bfs-dir", Directory);
            request.Headers.Add("bfs-ext", ext);


            if (postData != null)
            {
                request.ContentLength = postData.Length;
                request.KeepAlive = true;

                Stream dataStream = request.GetRequestStream();
                dataStream.Write(postData, 0, postData.Length);
                dataStream.Close();
            }
            try
            {
                var response = (HttpWebResponse)request.GetResponse();
                var status = response.Headers["bfs-status"];
                result = response.Headers["bfs-result"];
                return response.StatusCode == HttpStatusCode.OK && status == "100";
            }
            catch (Exception e)
            {
                throw e;
            }
            //string boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x");
            //byte[] boundarybytes = Encoding.UTF8.GetBytes("\r\n--" + boundary + "\r\n");
            //byte[] endbytes = Encoding.UTF8.GetBytes("\r\n--" + boundary + "--\r\n");

            ////1.HttpWebRequest
            //HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
            //request.ContentType = "multipart/form-data; boundary=" + boundary;
            //request.Method = "POST";
            //request.KeepAlive = true;
            //request.Credentials = CredentialCache.DefaultCredentials;

            //using (Stream stream = request.GetRequestStream())
            //{
            //    //1.1 key/value
            //    string formdataTemplate = "Content-Disposition: form-data; name=\"{0}\"\r\n\r\n{1}";
            //    if (data != null)
            //    {
            //        foreach (string key in data.Keys)
            //        {
            //            stream.Write(boundarybytes, 0, boundarybytes.Length);
            //            string formitem = string.Format(formdataTemplate, key, data[key]);
            //            byte[] formitembytes = encoding.GetBytes(formitem);
            //            stream.Write(formitembytes, 0, formitembytes.Length);
            //        }
            //    }

            //    //1.2 file
            //    string headerTemplate = "Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"\r\nContent-Type: application/octet-stream\r\n\r\n";
            //    byte[] buffer = new byte[4096];
            //    int bytesRead = 0;
            //    for (int i = 0; i < files.Length; i++)
            //    {
            //        stream.Write(boundarybytes, 0, boundarybytes.Length);
            //        string header = string.Format(headerTemplate, "file" + i, Path.GetFileName(files[i]));
            //        byte[] headerbytes = encoding.GetBytes(header);
            //        stream.Write(headerbytes, 0, headerbytes.Length);
            //        using (FileStream fileStream = new FileStream(files[i], FileMode.Open, FileAccess.Read))
            //        {
            //            while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0)
            //            {
            //                stream.Write(buffer, 0, bytesRead);
            //            }
            //        }
            //    }

            //    //1.3 form end
            //    stream.Write(endbytes, 0, endbytes.Length);
            //}
            ////2.WebResponse
            //HttpWebResponse response = (HttpWebResponse)request.GetResponse();
            //using (StreamReader stream = new StreamReader(response.GetResponseStream()))
            //{
            //    return stream.ReadToEnd();
            //}
        }
    }
}

代碼實現也很簡單,就是封裝請求操作罷了!

沒什么邏輯,今天就不分析了!

The End~

其實用單獨的文件服務器的好處有很多;

其中比較關鍵的兩點:

第一、促使瀏覽器並行下載多個請求、提高加載速度。

對於瀏覽器而言,同時對同域名下的請求是有限的,IE應該是五個左右,具體可以監控請求查看,而單獨部署靜態資源文件可以解決一點問題;

第二、cookie-free,暨避免不必要的請求頭。

稍微說一點:瀏覽器請求服務器的時候會帶着該域名下的Cookie等請求頭的信息,而這些信息對於資源文件似乎是沒有任何意義的,只是增加負擔。

順便提醒一下,外部鏈接也不要太多,會適得其反!對SEO有影響。據權威(YAHOO)研究表明,應該是5個左右!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM