【Web總結】資源儲存

JerryCheese發表於2018-07-24

我的原文:www.hijerry.cn/p/17326.htm…

前言

幾乎所有的Web站點都需要儲存檔案資源,如圖片、視訊等。也有像百度網盤這樣的平臺專門做雲端儲存,為使用者提供了極大的便利。

基礎知識

HTTP請求報文分為請求頭、請求體。請求頭中的Content-Type欄位,描述了請求體是什麼型別的內容,它的欄位值也叫做MIME型別,也叫mimetype,在這裡進行查詢:www.w3school.com.cn/media/media…

有關Content-Type、媒體型別等詳細知識可以參考《HTTP權威指南》,PDF版下載(密碼:7u67)。

有關使用HTTP協議上傳檔案的原理,可以參考:https://www.cnblogs.com/cswuyg/p/3185164.html。

版本1

理解了上傳檔案的原理後,我們可以完成一個較為基礎的檔案儲存函式,下面程式碼是基於Laravel5編寫。

/**
 * 儲存上傳的檔案至伺服器,並返回URL
 * @param Illuminate\Http\UploadedFile $file 上傳的檔案
 * @return string 檔案在伺服器中的URL,失敗返回false
 */  
function storeFile(UploadedFile $file) {
    $store_path = 'uploads';
    // 檔名稱 xxx.yy
    $name = time();
    if ($extension = $file->extension()) {
        $name .= $extension;
    }
    if (Storage::putFileAs($store_path, $file, $name)) {
        return '/storage/uploads/' . $name;
    } else {
        return false;
    }
}
複製程式碼

storeFile 函式接受一個上傳檔案例項,儲存成功後返回該檔案對應的URL,失敗返回false。

做法很簡單,把上傳的檔案都放到一個HTTP可訪問的目錄下,得到URL,使用者訪問此URL即可訪問上傳的資源。

  • 第10行。獲取上傳檔案的副檔名。
  • 第13行。儲存上傳的檔案到指定的目錄,並指定的檔名。

但是這種做法有幾個缺點甚至有安全漏洞:

  • 可上傳自定義指令碼。比如使用者可以上傳自己編寫的PHP檔案,這是非常危險的。就算使用Java等編譯語言進行後臺開發,也應該避免指令碼檔案的上傳。
  • 檔案都放在同一個目錄,有的檔案系統會有數量限制。可參考:https://blog.csdn.net/leonwei/article/details/3980179
  • 檔名可能衝突(併發量比較大時)。
  • 可能有重複檔案。比如兩個人在不同時間點上傳了一樣的檔案,系統會儲存兩份。

版本2:校驗和分類

上程式碼:

/**
 * 儲存上傳的檔案至伺服器,並返回URL
 * @param Illuminate\Http\UploadedFile $file 上傳的檔案
 * @return string 檔案在伺服器中的URL,失敗返回false
 */  
function storeFile(UploadedFile $file) {
    $time = time();
    // 檔名稱 xxx.yy
    $name = $time;
    if ($extension = $file->extension()) {
        $name .= $extension;
    }
    // 按日期將檔案分類
    $store_path = 'uploads' . DIRECTORY_SEPARATOR . date('Ymd', $time);
    if (Storage::putFileAs($store_path, $file, $name)) {
        return '/storage/' . str_replace('\\', '/', $store_path) . '/' . $name;
    } else {
        return false;
    }
}
/**
 * 檢查上傳的檔案是否符合要求
 * @param Illuminate\Http\UploadedFile $file 上傳的檔案
 * @param array $rule 含mimetype, max_size(單位B)兩個規則
 * @return int 符合所有要求返回0,不符合mimetype返回1,不符合max_size返回2
 */  
function checkFile(UploadedFile $file, $rule) {
    if (! $rule) $rule = ['mimetype' => [], 'max_size' => 0];
    if ($rule['mimtype'] && ! in_array($file->getMimeType(), $rule['mimetype'])) {
        return 1;
    } else if ($rule['max_size'] && $file->getSize() > $rule['max_size']) {
        return 2;
    } else {
        return 0;
    }
}
複製程式碼

先呼叫 checkFile 檢查上傳檔案的mimetype、檔案大小是否符合要求,函式返回0表示沒有不符合的。再呼叫 storeFile 儲存檔案。這樣可以儘可能的避免版本1的第一、二個問題。

比如在上傳頭像時,可以規定只能上傳jpg、bmp等圖片格式,而且檔案大小不得超過2M。

下面解釋部分程式碼:

  • 第14行。DIRECTORY_SEPARATOR 是指當前作業系統的分隔字元,Linux是 /,windows是\
  • 第16行。URL的分隔符都是 / 所以需要替換一下。

檔名生成函式可以替換成 microtime ,這是微秒級別的時間戳,所以就可以避免檔名衝突了(衝突可能性非常非常非常小,所以可以認為不會衝突)。

此版本仍然不能解決資源重複問題。

版本3:統一資源儲存

上程式碼:

/**
 * 儲存檔案和model
 * @param Illuminate\Http\UploadedFile $file
 * @return ResourceModel
 */
public function storeFile(UploadedFile $file) {
    // 檔案雜湊值
    $this->md = md5_file($file->getRealPath());
    // 查詢已存在的資源
    if ($exist_rs = ResourceModel::where('md', $this->md)->first()) {
        $exist_rs->from_db = true;
        return $exist_rs;
    }

    // 檔名稱    xxx.jpeg
    $this->name = $file->getClientOriginalName();
    // mimetype  image/jpeg
    $this->mime = $file->getMimeType();
    // 字尾      jpeg
    $this->suffix = $file->extension();

    DB::beginTransaction();
    // 儲存資料庫
    if (! $this->save()) {
        DB::rollBack();
        return false;
    }

    // 檔案儲存路徑 data/52/08/06
    $store_path = $this->getStorePath();
    // 520806eb60722ca0d10c89d3b20b370c
    $store_name = $this->getStoreName();

    // 儲存檔案
    if (! Storage::exists($this->getFilename())) {
        // 如果寫入檔案失敗,則不存入資料庫中
        if (!Storage::putFileAs($store_path, $file, $store_name)) {
            DB::rollBack();
            return false;
        }
    }
    DB::commit();
    return $this;
}
/**
 * 顯示檔案
 * @param Illuminate\Http\Request $req
 * @param $md
 * @return mixed
 */
public function showResource(Request $req, $md) {
    $rs = ResourceModel::where('md', $md)->first();
    if (! $rs) {
        return 'resource not found';
    } else {
        if ($rs->refer) return redirect($rs->refer);
        // uploads/7d/s8/7ds87x...
        $filename = $rs->getFilename();
        // 獲取檔案物理路徑
        $full_filename = storage_path("app/${filename}");
        if (!file_exists($full_filename)) {
            return response('file not found', Response::HTTP_NOT_FOUND);
        }
        return response()->file($full_filename,[
            'Content-Type' => $rs->mime
        ]);
    }
}
複製程式碼

storeFile 方法是 ResourceModel 裡的,這裡羅列一下 Resource 的表結構:

mark

showResource 是controller裡的方法,用於處理HTTP請求。

這個版本的核心思想是,先計算檔案的MD5值,同一個檔案具有一樣的MD5值,不同的檔案MD5值不一樣。這個MD5值將作為檔名。檔案的儲存路徑是取MD5值的前四位字元,兩位分為一組,共兩組,分別作為一級目錄和二級目錄名。

此版本仍然使用 checkFile 檢查檔案型別和大小。

程式碼解釋:

  • 第8行:計算檔案的MD5值。
  • 第10~13行:找到系統中已經存在的資源,直接返回該資源。這樣可以避免資源重複。
  • 第60行:storage_path 返回檔案的物理儲存地址。
  • 第64行:響應資原始檔,並指定響應的Content-Type,讓瀏覽器可以正常的顯示資源。

這個版本已經可以滿足絕大部分Web系統的需求了。並且還有可提升空間,可擴充性也比較強。

舉個擴充套件的例子,資原始檔越多時,可以通過目錄劃分來做分散式儲存。

視訊資源

視訊資源在存放於Web系統之前,往往要進行調整解析度、編碼格式、加水印之類的操作。對於大視訊檔案,也應該採取分片儲存的方式。

調整解析度、編碼格式、加水印等,可以使用 ffmpeg 完成。

檔案分片包含兩個話題:上傳時分片,儲存時分片。上傳時分片,儲存時就比較好操作,直接按片劃分儲存就行;上傳不分片,儲存如果要分片的話,只能能採取視訊擷取的方式(使用ffmpeg)把視訊分為幾個部分儲存,一般來說取前1分鐘為第一片,後續每4分鐘一片,是比較好的分片方法。

視訊檔案分片後,播放時也是要分片播放的,如果採取1:4:4..:4的方法分片,前端需要先載入出第一片1分鐘的視訊,後面在依次載入4分鐘的分片檔案。這裡解釋下為什麼第一片1分鐘,因為使用者一般看1分鐘,沒興趣看就溜了,所以為了不浪費流量,第一片就1分鐘比較合適。

分散式和同步

先看一個需要用到同步的場景,不同學校間共享教學視訊,訪問時又是自己內網的伺服器。要實現這個功能有兩個關鍵點:

  1. 學校的伺服器至少有一臺能和外部伺服器通訊。
  2. 採取分散式還是集中式同步。

集中式同步方案:選一箇中心主機,視訊都放在中心主機,並且通過檔案共享手段,讓伺服器在讀取資源時,直接從中心主機讀取。檔案共享手段比如NAS、rsync命令、基於xcopy的指令碼等。需要注意的是,跨校區的同步速度是比較慢的。

分散式同步方案:沒有中心主機,上傳的時候先放在本校伺服器,每一個伺服器定期向其他伺服器詢問是否有新資源,和推送資源給其他伺服器。這種方案,也可以選一箇中心主機,本地伺服器在儲存完資源後,將視訊推送到中心主機,再由中心主機下發到其他學校伺服器(類似git機制)。

他們的核心區別是:集中式,不同伺服器從同一臺伺服器上取視訊資源;分散式,伺服器從本地取視訊資源。

相關文章