V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
lock522
V2EX  ›  程序员

分享一个 node.js “公众档所”项目

  •  4
     
  •   lock522 · 2015-09-14 16:09:46 +08:00 · 2507 次点击
    这是一个创建于 3351 天前的主题,其中的信息可能已经有所发展或是发生改变。

    本文作者:小鳥遊死月 ,完整文章地址http://mp.weixin.qq.com/s?__biz=MjM5ODc5ODgyMw==&mid=210563292&idx=1&sn=d8a8ce1554fa1c510b357738a46f97a6#rd


    所谓“公众档所”,其实就是一个公共的临时网盘了。这个东西是一个老物了,在我刚接触 Expressjs 的时候写的。当时还随便搞了一下 backbone.js ,但是没有深入,勿笑。关于深入构架 Expressjs 方面也没做,只是粗粗写了下最基础的路由,所以整个文件结构也不是很规范。但是应该能比较适合刚学 Node.js 以及刚接触 Expressjs 的人吧。

    Repo 地址在 我的 Github (https://github.com/XadillaX/public-file-house)上。 Demo 地址在 http://dang.kacaka.ca/,由于个人电脑的不稳定性,所以不保证你们随时可以访问,保不定哪天就失效了,所以最好的办法还是自己 clone 下来啪啪啪。

    它所需要的东西大致就是 Expressjs + Redis + Backbone 了。不过都是最最基础的代码。

    部署

    把部署写在最前面是爲了能让你们自己电脑上有一个能跑的环境啦。公衆档所在我自己这边的环境里面是由三台电脑组成的。

    • 网关“服务器”。这是我这边环境一致对外的机器。实际上是一片树莓派,装了 nginx ,然后对内部做反向代理。
    • 本体“服务器”。跑了 公众档所 本体。
    • 数据库“服务器”。我们用的数据库实际上不是严格意义上的数据库,只是 redis 罢了,也没做与其它数据库的持久化,只是用了他内部自带的持久化。

    如果你们装一台机子上,那么就是:

    将 repo 给 clone 到自己的机子上。

    shell
    
    1  $ git clone https://github.com/XadillaX/public-file-house
    

    装好 redis ,并根据需要修改 redis.conf 文件。

    执行 redis.sh 文件开启数据库。如果你自己本身已经开启数据库或者用其它方法开啓了,请忽略上面数据库相关步骤。

    然后打开 commonConst.js 文件进行编辑,把相关的一些信息改成自己所需要的。

    哦对了,还有一个“洁癖相关”的步骤。我以前年轻不懂事,把 node_modules 文件夹也给加到版本库中了,而且也在里面居然自己加了两个没有弄到 nmp 去的模块(而且这两个模块本来就不应该放在这个文件夹下,但是不要在意这些细节,反正我现在肯定不会做这么傻的事了)。

    至于爲什麽不要这麽做,就跟 node_modules 文件夹的意义相关了。而且里面有可能有一些在我本机编译好的模块,所以最好还是清理下自己重新装一遍爲佳。

    具体呢大致就是把 node_modules 文件夹里面的 alphaRandomer.js 文件和 smpEncoder.js 文件拷贝出来备份到任意文件夹,然后删除整个 node _module 文件夹。接下去跑到项目根目录执行:

    shell
    
    1  $ npm install
    

    把三方模塊重新裝好之後,把剛纔拷出去的倆文件放回這個目錄下。(但是以後你們自己寫別的項目的話千萬別學我這個壞樣子啊,以前年輕不懂事 QAQ )

    最後跑起來就行啦:

    shell
    
    1  $ node pfh.js
    

    解析

    接下去就是要剖析這小破東西了。

    基礎文件

    pfh.js

    這個文件其實是 Expressjs 自動生成的,以前不是很懂他,所以也沒怎麼動,基本上是保持原封不動的。

    router.js

    這個是路由定義的文件。比較醜陋的一種方法,把需要定義的所有路由都寫進兩個 json 對象中,一個 POST 和一個 GET 。

    看過 Expressjs 文檔的人或者教程的人都知道,最基礎的路由註冊寫法其實就是:

    javascript
    
    1  app.get (KEY, FUNCTION );
    

    或者:

    javascript
    
    1  app.post (KEY, FUNCTION );
    

    所以我下面有一個函數:

    javascript
    
    1  exports.setRouter = function (app ) {
    2      for (var key in this.getRouter ) {
    3          app.get (key, this.getRouter[key]); 
    4      }
    5
    6      for (var key in this.postRouter ) {
    7          app.post (key, this.postRouter[key]);
    8      }
    9  };
    

    其大致意思就是把之前我們定義好的兩個路由對象裏的內容一一給註冊到系統的路由當中去。這個是我最初最簡陋的思想,不過後來我把它稍稍完善了一下寫到別的地方去了。

    模型

    model/fileModel.js

    這個就是模型層了,主要就是 redis 的一些操作了。在這裏我用的是 redis 這個模塊,具體的用法大家可以看它 repo 的 README.md 文件。

    大致就三個函數:

    • fileModel.prototype.keyExists: 判斷某個提取碼存在與否。
    • fileModel.prototype.get: 獲取某個驗證碼的文件信息。
    • fileModel.prototype.addFile: 添加一個文件信息。

    不過有個壞樣子大家不要學, Node.js 大家都約定俗成的回調函數參數一般都是 callback (err, data, blahblah...) 的,第一個參數都是錯誤,如果沒錯誤都是 null 或者是 undefined 的。但是以前也沒這種意識,所以回調函數的參數也都是比較亂的。

    控制器

    action/index.js

    這是一些基礎控制器。

    exports.index

    純粹的首頁顯示。

    exports.download

    文件下載控制器。由代碼可知,首先獲取 token 和 code 。 token 是驗證 URL 的有效性而 code 即提取碼了。

    期間我們驗證了下 token :

    javascript
    
    1   if (!functions.verifyBlahblah (token )) {
    2       resp.redirect (baseConfig.webroot );
    3   }
    

    而這個 verifyBlahblah 函數就在這個文件(https://github.com/XadillaX/public-file-house/blob/master/plugin/functions.js#L21)裏面。

    javascript
    
    1    exports.verifyBlahblah = function (blahblah ) {
    2        var array = blahblah.split ("^");
    3        var time = array[array.length - 1];
    4        array.pop ();
    5
    6        var encoder = require ("smpEncoder");
    7
    8        try {
    9            var text = encoder.norBack (array, time.toString ());
    10           text = encoder.decode (text );
    11       } catch (e ) {
    12           return false;
    13       }
    14
    15       var now = text.substr (0, 10 );
    16       var token = text.substr (10 );
    17
    18       if (parseInt (Date.now () / 1000 ) - parseInt (now ) > 300 ) return false;
    19       if (token !== require ("../commonConst").token ) return false;
    20
    21       return true;
    22  };
    

    大體意思就是把其打散到數組裏面,其中時間戳是最後一位。然後解密。最後驗證解密後的 token 是否等於系統的 token 以及時間戳有沒有過期。

    大家通過截取 Chrome 或者 Firefox 的請求信息,不難發現有這麼個地址:

    1  Request URL:http://localhost/download?file=662ZE&token=65^97^74^68^106^125^88^115^65^96^66^105^127^114^87^123^123^114^84^124^114^125^120^121^99^116^100^118^116^98^124^120^109^98^120^100^80^119^120^87^119^105^116^8^1395904110
    2  Request Method:GET
    3  Status Code:200 OK
    

    而這一坨 65^97^74^68^106^125^88^115^65^96^66^105^127^114^87^123^123^114^84...^1395904110 便是所謂的 token 了。而且本來就是個 demo ,這個 token 也就是隨便做做樣子罷了。

    接下去通過驗證之後,便可以從數據庫中讀取文件信息了。如果有文件,那麼通過 resp.download 函數呈現給用戶。

    javascript
    
    1     var fileModel = new FileModel ();
    2     fileModel.get (code, function (status, error, obj ) {
    3         if (error ) resp.redirect (baseConfig.webroot );
    4         else {
    5             if (obj === null ) {
    6                resp.redirect (baseConfig.webroot + "/get/" + code + "/not-exist");
    7             } else {
    8                 resp.download (baseConfig.uploadDir + code, require ("urlencode")(obj.filename ));
    9             }
    10        }
    11  });
    

    exports.getToken

    這個函數就是生產一個有效的 token 用的。在前端是通過 ajax 來獲取的。

    javascript
    
    1  var encoder = require ("smpEncoder");
    2  var token = baseConfig.token;
    3  var now = parseInt (Date.now () / 1000 );
    4  var result = encoder.encode (now + token );
    5  result = encoder.norGo (result, now.toString ());
    6  var resultString = "";
    7  for (var i = 0; i < result.length; i++) resultString += (result[i] + "^");
    

    大體呢就是根據目前的時間戳和系統 token 一起加密生產一個有效的 token 。

    exports.send2fetion

    通過自己的飛信給自己發送提取碼以備忘。

    這裏的話用了一個 fetion-sender 的模塊。 Repo 在這裏(https://github.com/XadillaX/fetion-sender)。

    action/upload.js

    這個文件裏面其實就一個 exports.upload 函數,另一個是生成提取碼用的。

    function genAlphaKey (time, callback )

    生成提取碼。我們假設最多嘗試 10 次,若嘗試 10 次還沒有生成唯一的驗證碼就輸出錯誤讓用戶重試。所以就有了:

    javascript
    
    1    function genAlphaKey (time, callback ) {
    2        var keyLength = config.uploadLen;
    3        var filename = alphaRandomer.rand (keyLength );
    4        var fileModel = new FileModel ();
    5
    6        fileModel.keyExists (filename, function (status, result ) {
    7            if (!status ) {
    8                if (time < maxTryTime ) {
    9                    genAlphaKey (time + 1, callback );
    10               }
    11               else {
    12                   callback (false, result, "");
    13               }
    14
    15               return;
    16           } else {
    17               if (result ) genAlphaKey (time, callback );
    18               else {
    19                   callback (true, "", filename );
    20               }
    21           }
    22       });
    23  }
    

    不斷地生成定長的提取碼,然後通過模型的 keyExists 函數來確定這個提取碼是否存在,如果存在了就遞歸調用重新生成,否則就直接回調。

    exports.upload

    上傳文件的頁面了。

    javascript
    
    1    if (req.files.files.length !== 1 ) {
    2        result.status = false;
    3        result.msg = "請用正確的姿勢餵我文件。";
    4        resp.send (200, result );
    5        return;
    6    }
    7
    8    var fileInfo = req.files.files[0];
    9    if (fileInfo.size > config.maxUploadSize ) {
    10       result.status = false;
    11       result.msg = "文件太大啦,公衆檔所一次只能吃 10M 的文件哦。";
    12       resp.send (200, result );
    13       return;
    14   }
    

    前面一堆話大致就是做下有效性判斷而已。然後調用函數來生成有效的提取碼:

    javascript
    
    1  genAlphaKey (1, function (status, msg, filename ) {
    2      ...
    3  });
    

    如果生成成功的話就往數據庫中添加文件信息:

    javascript
    
    1  var fileModel = new FileModel ();
    2  fileModel.addFile (filename,
    3      fileInfo.name,
    4      fileInfo.headers["content-type"],
    5      function (status, msg ) {
    6          ...
    7      }
    8  );
    

    如果添加也成功了的話,那麼把剛上傳到臨時文件夾的文件給移動到上傳文件儲存目錄中,以便以後可以被下載:

    javascript
    
    1  fs.rename (fileInfo.path, uploadDir + filename, function (err ) {
    2      ...
    3  });
    

    如果移動也成功了的話,那麼返回一個成功的 json 信息:

    javascript
    
    1  result.status = true;
    2  result.code = filename;
    3  resp.send (200, result );
    

    視圖

    這裏視圖就一個 index.ejs 。然後通過 backbone.js 來調用不同的頁內模板和邏輯來實現的類似於 SPA (Solus Par Agula ) (Single Page Application ) 的效果。

    views/index/index.ejs

    像類似於下面的這種就是 backbone.js 的模板概唸了:

    --- 未完 ---

    查看完整文章请访问:http://t.cn/RytTUeA

    更多技术类专题请关注 UPYUN 微信公众号

    欢迎转载,但请标明作者并保留原文出处,谢谢。

    4 条回复    2015-09-15 10:00:04 +08:00
    Septembers
        1
    Septembers  
       2015-09-14 16:34:55 +08:00 via Android
    請勿全文轉載 CC @Livid
    Livid
        2
    Livid  
    MOD
       2015-09-14 16:37:11 +08:00
    @lock522 你是 UPYUN 的工作人员么?
    lock522
        3
    lock522  
    OP
       2015-09-14 16:38:44 +08:00
    @Livid 是的,我是负责线上这一块的,这篇稿件是内部投稿。
    Darkholme
        4
    Darkholme  
       2015-09-15 10:00:04 +08:00
    繁体看着有点累...
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1140 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 19:03 · PVG 03:03 · LAX 11:03 · JFK 14:03
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.