2018年3月27日 星期二

Node.js 學習筆記 (三) : 檔案模組 fs 測試

過去這一周把圖書館借來的幾本 Node.js 書籍囫圇吞棗地看了一些, 迫不及待要來測試一番. 其中以核心套件中的 fs 模組最為重要, 因為在建立網頁伺服器時會用到, 而且這是 Javascript 跨出瀏覽器禁錮的最重要功能-存取本機檔案, 此功能以前只能用微軟的 ActiveX 元件 FSO 才辦得到.

Node.js 將檔案讀取與寫入, 刪除檔案, 更改檔名, 查詢目錄或連結等操作封裝在核心模組 fs 中, 使用標準 POSIX 呼叫進行檔案操作. 此模組較特別之處是每個函數都提供了同步 (Sync) 與非同步版本, 例如讀取檔案同步版是呼叫 fs.redFileSync(), 而非同步則是 fs.readFile().

本系列之前的測試紀錄參考 :

關於 Node.js
如何更新樹莓派的 Node.js
Node.js 學習筆記 (一) : 在 Windows 安裝 Node.js

Node.js 8.x 版檔案系統 API 文件參考 :

https://nodejs.org/dist/latest-v8.x/docs/api/fs.html
# w3schools : Node.js Tutorial

以下僅就常用的 fs 模組函數進行測試, 除了官網範例外, 還參考了下面兩本書 :

# 你不能錯過的 Node.js 指南
# 不一樣的 Node.js

使用 fs 模組前需先用 require() 載入 :

var fs=require("fs");

然後就可以呼叫它的函數進行檔案操作. 與其他模組不同之處是, fs 模組的函數有非同步或同步之分, 同步版的函數以 Sync 結尾, 其差異如下 :
  1. 同步 : 無回呼函數 (call back), 執行時程序會阻塞 (blocked) 或停住等待 
  2. 非同步 : 有回呼函數 (call back), 執行時程序不會阻塞 (blocked) 或停住等待
除了同步非同步之分外, fs 模組部分函數如 fs.stat(), fs.chown(), fs.chmod(), fs.utimes() 等還可用 open() 開啟後取得檔案描述表 file descriptor 來操作. 

常見的檔案操作測試如下 :


1. 讀取檔案 :

非同步 : fs.readFile(filename, [options,] callback)
同步 :     fs.readFileSync(filename, [options,])

此函數用來讀取指定之檔案內容. 第一參數 filename 為包含如 "./" 或 "../" 等路徑之檔案名稱, 第二參數則是一個可有可無之選項物件, 例如指定檔案編碼格式則傳入 {encoding : "utf-8"} 或 {encoding : "ascii"}, 也可以傳入字串如 "utf-8" 或 "ascii". 非同步操作須傳入一個回呼函數來處理檔案操作結束後的傳回值, 它預設為最後一個參數. 以讀取檔案為例, fs.readFile() 處理結束後會傳回 err(錯誤訊息) 與 data(讀取到的檔案內容) 兩個參數, 須將其傳入回呼函數中處理, 例如 :

//fs_test_1.js
var fs=require("fs");
fs.readFile("./test.txt", function(err, data) {
  if (err) {console.error(err);}
  else {console.log(data);}
  })
console.log("檔案讀取操作");

上例是讀取程式所在目錄底下的 test.txt 檔案, 若讀取成功則傳回之 err 為 null, 就會執行 else 輸出讀取到的檔案內容, 否則 err 物件不為 null, 表示檔案讀取失敗, 就輸出 err 物件內容, 例如 :

D:\Node.js\test>node fs_test_1.js
{ Error: ENOENT: no such file or directory, open 'D:\Node.js\test\test.txt'
  errno: -4058,
  code: 'ENOENT',
  syscall: 'open',
  path: 'D:\\Node.js\\test\\test.txt' }

如果在程式所在目錄下準備了一個 test.txt 檔, 裡面就只有 Hello 與 World 兩行字 :

Hello
World

將 test.txt 以 ANSI (ASCII) 格式存檔, 執行結果如下 :

D:\Node.js\test>node fs_test_1.js
<Buffer 48 65 6c 6c 6f 0d 0a 57 6f 72 6c 64>

可見傳回的 data 是存在緩衝區物件 Buffer 內的未解碼 ASCII 碼, 其中 0d 0a 分別為跳行字元 CR LF. 如果將 test.txt 以 UTF-8 格式存檔, 則執行結果如下 :

D:\Node.js\test>node fs_test_1.js
檔案讀取操作
<Buffer ef bb bf 48 65 6c 6c 6f 0d 0a 57 6f 72 6c 64>

可見開頭多了 3 個 byte 的 UTF-8 位元組順序記號 ef bb bf.

如果要得到解碼後的文字, 必須在 fs.readFile() 方法中傳入 encoding 參數 (第二參數), 如果 test.txt 是以 ANSI (ASCII) 存檔, 則 encoding 為 "ASCII" 或 "ascii" :

//fs_test_2.js
var fs=require("fs");
fs.readFile("./test.txt", "ASCII", function(err, data) {
  if (err) {console.error(err);}
  else {console.log(data);}
  })
console.log("檔案讀取操作");

可見執行結果不再是 ASCII 碼, 而是已解碼之可讀文字 :

D:\Node.js\test>node fs_test_1.js
檔案讀取操作
Hello
World

或者呼叫 toString() 函數也可以, 例如 :

//fs_test_2.js
var fs=require("fs");
fs.readFile("./test.txt", function(err, data) {
  if (err) {console.error(err);}
  else {console.log(data.toString());}
  })
console.log("檔案讀取操作");

注意, Node.js 非同步函數設計慣例最後一個參數為 callback 函數, 一個函數只有一個 callback. 回呼函數的第一個參數固定為 error 物件, 其餘參數為回呼函數的其他傳回值.

如果將 test.txt 存成 UTF-8 格式, 但卻用 fs_test_2.js 的 ASCII 格式讀取, 則前面三個位元組順序記號將無法解碼為可讀文字 :

D:\Node.js\test>node fs_test_2.js
檔案讀取操作
o;?Hello
World

這時應該用 UTF-8 格式讀取 :

//fs_test_3.js
var fs=require("fs");
fs.readFile("./test.txt", "UTF-8", function(err, data) {
  if (err) {console.error(err);}
  else {console.log(data);}
  console.log("檔案讀取操作完成");
  })
console.log("檔案讀取操作中 ... ");

執行結果如下 :

D:\Node.js\test>node fs_test_3.js
檔案讀取操作中 ...
Hello
Tony
檔案讀取操作完成

上面程式均為檔案讀取的非同步操作, 當呼叫 fs.readFile() 之後, 執行緒不會停下來等候檔案讀取動作結束, 而是繼續往下執行, 因此會先印出 "檔案讀取操作", 然後才出現讀取到的檔案內容. 然而 fs.readFile() 的同步版函數 fs.readFileSync() 則非如此, 執行緒會停住等待檔案 I/O 完成. 由於同步版沒有回呼函數, 因此必須用 try catch 來捕捉例外, 例如 :

//fs_test_4.fs
var fs=require("fs");
try {
  var data=fs.readFileSync("./test.txt","UTF-8");
  console.log(data);
  }
catch (err){
  console.error(err);
  }
console.log("檔案讀取操作");

執行結果如下 :

D:\Node.js\test>node fs_test_4.js
Hello
World
檔案讀取操作

可見與上面非同步操作不同的是, 輸出 "檔案讀取操作" 是在檔案讀取完畢之後才會執行. 以下測試將僅針對非同步.


2. 寫入檔案 :

非同步 : fs.writeFile(file, data[, options], callback)
同步 :     fs.writeFileSync(file, data[, options])

第一參數 file 為檔案名稱, 前面可帶 "./" (目前目錄), "../" (上一層目錄) 或 "html/" 等路徑, 不帶時表示目前目錄. 第二參數 data 為欲寫入之資料, 可以是字串或緩衝區物件 buffer. 寫入字串若要跳行在 Linux 中只要用 "\n" 即可, 但在 Windows 中則必須使用 "\r\n". 備選參數 options 可以是物件 (可有 encoding, mode, flag 等三屬性) 或字串 (僅表示 encoding, 預設為 'utf8'), 若寫入資料為 buffer 物件, 則 encoding 會被忽略.

注意, 使用寫入函數前不需要用 fs.exists() 檢查檔案是否已存在, 如果檔案不存在會自動建立一個空檔案; 如果檔案已存在則會覆蓋裡面的內容. 此外, writeFile() 的回呼函數只有 err 一個參數而已, 亦即只在發生錯誤時才會傳回 Error 物件, 否則不傳回任何值. 例如 :

//fs_test_6.js
var fs=require("fs");
var data="Hello\r\nTony";     //在 Windows 中跳行須用 \r\n
fs.writeFile("./test.txt", data, "UTF8", function(err) {
  if (err) throw err;
  console.log("檔案寫入操作完成!");
  })
console.log("檔案寫入操作中 ... ");

開啟 test.txt 內容如下 :

Hello
Tony

可見確實有跳行. 寫入資料也可以使用 Buffer 物件, 例如 :

//fs_test_7.js
var fs=require("fs");
var data=new Buffer("Hello\r\nTony");  //使用 Buffer 物件
fs.writeFile("./test.txt", data, "UTF8", function(err) {
  if (err) throw err;
  console.log("檔案寫入操作完成!");
  })
console.log("檔案寫入操作中 ... ");

上面的 writeFile() 與 writeFileSync() 函數會覆蓋檔案內原來的內容, 如果不希望覆蓋, 而是希望將內容附加在原來內容後面就要使用下面的 appendFile() 與 appendFileSync() 函數.


3. 追加寫入檔案 (Append) :

非同步 : fs.appendFile(file, data[, options], callback)
同步 :     fs.appendFileync(file, data[, options])

參數用法同 writeFile() 與 writeFileSync(). 例如 :

//fs_test_8.js
var fs=require("fs");
var data="Hello\r\nWorld";
fs.appendFile("./test.txt", data, "UTF8", function(err) {   //只有一個參數 err
  if (err) throw err;
  console.log("檔案附加寫入操作完成!");
  })
console.log("檔案附加寫入操作中 ... ");

執行結果  :

D:\Node.js\test>node fs_test_8.js
檔案附加寫入操作中 ...
檔案附加寫入操作完成!

開啟 test.txt 內容如下 :

Hello
TonyHello
World

因為上面寫入 "Hello\r\nTony" 結尾沒跳行, 因此 "Hello\r\nWorld" 就黏在 Tony 後面了.


4. 讀取檔案目錄 :

非同步 : fs.readdir(path[, options], callback)
同步 :     fs.readdirSync(path[, options])

此函數會讀取指定目錄下的所有檔案名稱 (只限檔案, 不包含子目錄名稱), 然後放在 files 陣列中傳回. 第一參數 path 為包含目錄名稱的路徑, 例如程式所在位置的子目錄 "tmp", 根目錄 "/", 上一層目錄 "../" 等. 備選參數 options 可以是物件 (僅 encoding) 或字串 (僅表示 encoding, 預設為 'utf8'). 非同步回呼函數 callback 有兩個傳入參數 : err 與 files, 讀取到的檔案列表會放在 files 陣列中, 例如 :

//fs_test_9.js
var fs=require("fs");
fs.readdir("/Node.js", function(err, files) {  //讀取根目錄下子目錄 Node.js 的檔案列表
  if (err) throw err;
  console.log(files);
  })
console.log("讀取目錄操作中 ... ");

執行結果如下 :

D:\Node.js\test>node fs_test_9.js
讀取目錄操作中 ...
[ 'app.nw',
  'arduino 當機事件.txt',
  'duration.js',
  'ebook',
  'hello.js',
  'http_1.js',
  'index.html',
  'node.exe',
  'Node.js.zip',
  'nw.exe',
  'package.json',
  'process_argv.js',
  'serial.js',
  'serialport.js',
  'server.js',
  'test',
  '露天.txt' ]



5. 建立目錄 :

非同步 : fs.mkdir(path[, mode], callback)
同步 :     fs.mkdirSync(path[, mode])

此函數用來建立目錄. 備選參數 mode 為一整數, 用來設定新建目錄之存取權限, 預設為 0o777. 注意, 非同步之回呼函數只有一個參數 err. 例如 :

//fs_test_10.js
var fs=require("fs");
fs.mkdir("./tmp", function(err) {
  if (err) throw err;
  console.log("新建目錄操作完成");
  })
console.log("新建目錄操作中 ... ");

D:\Node.js\test>node fs_test_10.js
新建目錄操作中 ...
新建目錄操作完成

檢查目前目錄下確實多了一個 tmp 子目錄.



6. 更改檔案或目錄名稱 :

非同步 : fs.rename(oldPath, newPath, callback)
同步 :     fs.renameSync(oldPath, newPath)

此函數用來更改檔案或名稱, 前兩個參數為包含路徑之舊與新檔案或目錄名稱. 注意, 非同步之回呼函數只有一個參數 err. 例如 :

//fs_test_11.js
var fs=require("fs");
fs.rename("test.txt", "test_new.txt", function(err) {
  if (err) throw err;
  console.log("檔案更名操作完成");
  })
console.log("檔案更名操作中 ... ");

執行結果如下 :

D:\Node.js\test>node fs_test_11.js
檔案更名操作中 ...
檔案更名操作完成

檢查確定 test.txt 已經被改成 test_new.txt 了.

函數 rename() 也可以更改目錄名稱 (但目前程式所在目錄因為被鎖定無法更名), 例如在上面範例中用 fs.mkdir() 所建立的子目錄 tmp 可以利用 rename() 將其改為 tmp_new, 例如 :

//fs_test_12.js
var fs=require("fs");
fs.rename("./tmp", "./tmp_new", function(err) {   //更改目錄名稱
  if (err) throw err;
  console.log("目錄更名操作完成");
  })
console.log("目錄更名操作中 ... ");

執行結果如下 :

D:\Node.js\test>node fs_test_12.js
目錄更名操作中 ...
目錄更名操作完成

檢查目前目錄下之子目錄 tmp 確實被改為 tmp_new 了.


7. 刪除目錄 :

非同步 : fs.rmdir(path, callback)
同步 :     fs.rmdirSync(path)

此函數用來建立目錄. 備選參數 mode 為一整數, 用來設定新建目錄之存取權限, 預設為 0o777. 注意, 非同步之回呼函數只有一個參數 err.

在上面範例中我們用 fs.rename() 將子目錄 tmp 更名為 tmp_new, 此處可用 fs.rmdir() 將其刪除, 例如 :

//fs_test_13.js
var fs=require("fs");
fs.rmdir("./tmp_new", function(err) {
  if (err) throw err;
  console.log("刪除目錄操作完成");
  })
console.log("刪除目錄操作中 ... ");

D:\Node.js\test>node fs_test_13.js
刪除目錄操作中 ...
刪除目錄操作完成

檢查目前目錄下確實已無 tmp_new 這個子目錄了.


7. 修改檔案長度 :

非同步 : fs.truncate(path[, len], callback)
同步 :     fs.truncateSync(path[, len])

此函數用來削減檔案長度. 第一參數 path 為包含路徑之檔案名稱, 備選參數 len 為新檔案內容之長度 (bytes), 預設為 0 (即全部內容刪除), 第三參數 callback 為回呼函數, 只有一個 err 參數. 

我先在目前目錄下編輯 test.txt 檔案, 內容為 "你好啊!Tony". 然後執行下列程式 : 

//fs_test_14.js
var fs=require("fs");
fs.truncate("./test.txt", function(err) {
  if (err) throw err;
  console.log("檔案長度修改操作完成!");
  })
console.log("檔案長度修改操作中 ... ");
執行後開啟 test.txt 檔, 果然裡面的內容都沒了, 可見預設 len=0 表示新檔案長度為 0 byte. 現在重新將 test.txt 內容改回 "你好啊!Tony", 進行下面 len 參數測試, 擷取原檔案前 8 個 bytes 內容 :

//fs_test_15.js
var fs=require("fs");
fs.truncate("./test.txt", 8, function(err) {
  if (err) throw err;
  console.log("檔案長度修改操作完成!");
  })
console.log("檔案長度修改操作中 ... ");


執行後開啟 test.txt, 發現裡面內容只剩下 "你好啊!T", 這是因為中文字型佔 2 個 byte, 前面三個中文字  "你好啊" 佔了 6 個 bytes, 後面再取 2 個 bytes "!T" 就成為 "你好啊!T" 了.

另外若檔案內容有跳行, fs.truncate() 還是可以運作, 但要注意, 在 Windows 中跳行字元 "\r\n" 佔 2 個 bytes, 例如將 test.txt 內容改為如下兩行內容的話 :

Hello
World

執行上面的程式擷取前面 8 個 bytes 會得到如下結果 :

Hello
W

這是因為第一行 "Hello\r\n" 佔了 7 個字元, 接著到下一行取 "W" 即 8 個 bytes 了. 將上面的測試寫成自動檢視檔案內容的程式如下, 使用非同步寫法必須將下一個要做的動作放在回呼函數, 因此會形成所謂的 "回呼地獄", 比較不好閱讀 :

//fs_test_16.js
var fs=require("fs");
var data="Hello\r\nWorld";      //檔案初始內容
fs.writeFile("./test.txt", data, "UTF8", function(err) {     //寫入檔案
  if (err) throw err;
  console.log("檔案寫入操作完成!");
  fs.readFile("./test.txt", "UTF8", function(err, data) {    //讀取檔案內容
    if (err) throw err;
    console.log("檔案讀取操作完成!");
    console.log("檔案內容 : \n" + data);
    fs.truncate("./test.txt", 8, function(err) {     //擷取檔案前 8 個 bytes
      if (err) throw err;
      console.log("檔案長度修改操作完成!");
      fs.readFile("./test.txt", "UTF8", function(err, data) {     //讀取檔案內容
        if (err) throw err;
        console.log("檔案讀取操作完成!");
        console.log("檔案內容 : \n" + data);
        });
      console.log("檔案讀取操作中 ... ");
      });
    console.log("檔案長度修改操作中 ... ");
    });
  console.log("檔案讀取操作中 ... ");
  });
console.log("檔案寫入操作中 ... ");

執行結果如下 :

D:\Node.js\test>node fs_test_16.js
檔案寫入操作中 ...
檔案寫入操作完成!
檔案讀取操作中 ...
檔案讀取操作完成!
檔案內容 :
Hello
World
檔案長度修改操作中 ...
檔案長度修改操作完成!
檔案讀取操作中 ...
檔案讀取操作完成!
檔案內容 :
Hello
W

上面程式的同步版如下 :

//fs_test_17.js
var fs=require("fs");
var data="Hello\r\nWorld";
try {  //寫入檔案
  fs.writeFileSync("./test.txt", data);
  console.log("檔案寫入操作完成!");
  }
catch (err) {console.error(err);}
try {  //讀取檔案
  var file=fs.readFileSync("./test.txt", 'utf8', data);
  console.log("檔案讀取操作完成!");
  console.log("檔案內容 : \n" + file);
  }
catch (err) {console.error(err);}
try {  //變更檔案長度
  var data=fs.truncateSync("./test.txt", 8);
  console.log("檔案長度修改完成!");
  }
catch (err) {console.error(err);}
try {  //讀取檔案
  var file=fs.readFileSync("./test.txt", 'utf8', data);
  console.log("檔案讀取操作完成!");
  console.log("檔案內容 : \n" + file);
  }
catch (err) {console.error(err);}

執行結果是一樣的 :

D:\Node.js\test>node fs_test_17.js
檔案寫入操作完成!
檔案讀取操作完成!
檔案內容 :
Hello
World
檔案長度修改完成!
檔案讀取操作完成!
檔案內容 :
Hello
W


8. 取得檔案或目錄資訊 :

非同步 : fs.stat(path, callback)
同步 :     fs.statSync(path)

此函數用來取得檔案或目錄之資訊. 第一參數 path 為包含路徑之檔案或目錄名稱, 第二參數 callback 為回呼函數, 有 err 與 ststs 兩個參數, 其中 stats 為查詢所得之檔案或目錄資訊, 為一 fs.Stats 物件, 可用迴圈列舉其 key, value 以檢視物件內容, 例如 :

//fs_test_18.js
var fs=require("fs"); 
fs.stat("./test.txt", function(err, stats) {
  if (err) throw err;
  console.log("檔案資訊取得操作完成!");
  console.log("檔案資訊 :\n");
  for(var k in stats) {
console.log(k + ":" + stats[k]);
    }
  });
console.log("檔案資訊取得操作中 ... ");

執行結果如下 :

D:\Node.js\test>node fs_test_18.js
檔案資訊取得操作中 ...
檔案資訊取得操作完成!
檔案資訊 :

dev:2089136718
mode:33206 
nlink:1
uid:0
gid:0
rdev:0
blksize:undefined
ino:562949954052903 
size:138 
blocks:undefined
atimeMs:1522111655025.0005
mtimeMs:1522202247354.802
ctimeMs:1522202247354.802
birthtimeMs:1521426043495.4006
atime:Tue Mar 27 2018 08:47:35 GMT+0800 (台北標準時間)
mtime:Wed Mar 28 2018 09:57:27 GMT+0800 (台北標準時間)
ctime:Wed Mar 28 2018 09:57:27 GMT+0800 (台北標準時間)
birthtime:Mon Mar 19 2018 10:20:43 GMT+0800 (台北標準時間)
_checkModeProperty:function (property) {
  return ((this.mode & S_IFMT) === property);
}
isDirectory:function () {
  return this._checkModeProperty(constants.S_IFDIR);
}
isFile:function () {
  return this._checkModeProperty(S_IFREG);
}
isBlockDevice:function () {
  return this._checkModeProperty(constants.S_IFBLK);
}
isCharacterDevice:function () {
  return this._checkModeProperty(constants.S_IFCHR);
}
isSymbolicLink:function () {
  return this._checkModeProperty(S_IFLNK);
}
isFIFO:function () {
  return this._checkModeProperty(S_IFIFO);
}
isSocket:function () {
  return this._checkModeProperty(S_IFSOCK);
}

這裡面 uid (使用者 ID), gid (群組 ID) 等資訊在 Linux 較常用. fs.stat() 也可以查詢目錄資訊, 例如目前目錄底下的空子目錄 tmp :

//fs_test_19.js
var fs=require("fs"); 
fs.stat("./tmp", function(err, stats) {    //空的子目錄
  if (err) throw err;
  console.log("檔案資訊取得操作完成!");
  console.log("檔案資訊 :\n");
  for(var k in stats) {
console.log(k + ":" + stats[k]);
    }
  });
console.log("檔案資訊取得操作中 ... ");

執行結果如下 :

D:\Node.js\test>node fs_test.js
檔案資訊取得操作中 ...
檔案資訊取得操作完成!
檔案資訊 :

dev:2089136718
mode:16822   
nlink:1
uid:0
gid:0
rdev:0
blksize:undefined
ino:281474977342527   
size:0   
blocks:undefined
atimeMs:1522135068643.0005
mtimeMs:1522135068643.0005
ctimeMs:1522135068643.0005
birthtimeMs:1522135068643.0005
atime:Tue Mar 27 2018 15:17:48 GMT+0800 (台北標準時間)
mtime:Tue Mar 27 2018 15:17:48 GMT+0800 (台北標準時間)
ctime:Tue Mar 27 2018 15:17:48 GMT+0800 (台北標準時間)
birthtime:Tue Mar 27 2018 15:17:48 GMT+0800 (台北標準時間)
_checkModeProperty:function (property) {
  return ((this.mode & S_IFMT) === property);
}
isDirectory:function () {
  return this._checkModeProperty(constants.S_IFDIR);
}
isFile:function () {
  return this._checkModeProperty(S_IFREG);
}
isBlockDevice:function () {
  return this._checkModeProperty(constants.S_IFBLK);
}
isCharacterDevice:function () {
  return this._checkModeProperty(constants.S_IFCHR);
}
isSymbolicLink:function () {
  return this._checkModeProperty(S_IFLNK);
}
isFIFO:function () {
  return this._checkModeProperty(S_IFIFO);
}
isSocket:function () {
  return this._checkModeProperty(S_IFSOCK);
}


9. 取得絕對路徑 :

非同步 : fs.realpath(path[, options], callback)

此函數用來取得檔案或目錄之絕對路徑. 第一參數 path 為包含路徑之檔案或目錄名稱, 備選參數 options 為一字串或物件, 用來指定檔案內容之編碼 encoding, 預設為 utf8. 第三參數 callback 為回呼函數, 只有一個 err 參數. 例如 :

//fs_test_20.js
var fs=require("fs"); 
fs.realpath("./test.txt", function(err, resolvedPath) {
  if (err) throw err;
  console.log("絕對路徑取得操作完成!");
  console.log("相對路徑 : ./test.txt");
  console.log("絕對路徑 : " + resolvedPath);
  });
console.log("絕對路徑取得操作中 ... ");

執行結果 :

D:\Node.js\test>node fs_test_20.js
絕對路徑取得操作中 ...
絕對路徑取得操作完成!
相對路徑 : ./test.txt
絕對路徑 : D:\Node.js\test\test.txt

查詢目錄之絕對路徑 :

//fs_test_21.js
var fs=require("fs"); 
fs.realpath("./tmp", function(err, resolvedPath) {
  if (err) throw err;
  console.log("絕對路徑取得操作完成!");
  console.log("相對路徑 : ./tmp");
  console.log("絕對路徑 : " + resolvedPath);
  });
console.log("絕對路徑取得操作中 ... ");

執行結果 :

D:\Node.js\test>node fs_test_21.js
絕對路徑取得操作中 ...
絕對路徑取得操作完成!
相對路徑 : ./tmp
絕對路徑 : D:\Node.js\test\tmp


10. 開啟與關閉檔案 :

除了上述的高階檔案操作外, 有時需進行低階操作, 這時需要用到 fs.open() 所傳回之檔案描述子 (file descriptor), 這是一個參照或索引, 作業系統內核用它來指向所開啟檔案之檔案紀錄表. Linux 的系統呼叫大都依賴檔案描述子. 使用 fs.open() 操作檔案完畢後, 須使用 fs.close() 予以關閉. 關於檔案描述子參考 :

https://zh.wikipedia.org/wiki/文件描述符

開啟檔案 :
非同步 : fs.open(path, flags[, mode], callback)
同步 :     fs.openSync(path, flags[, mode])

此函數用來開啟檔案, 第一參數 path 為包含路徑之檔案或目錄名稱, 第二參數 flags 旗標字串設定檔案操作模式 :

 flag 說明
 r 讀取, 若檔案不存在則拋出例外
 r+ 讀取 + 寫入, 若檔案不存在則拋出例外
 rs+ 同步模式之讀取 + 寫入
 w 寫入, 若檔案不存在則建立, 否則覆蓋
 wx 寫入, 若檔案不存在則拋出例外
 w+ 讀取 + 寫入, 若檔案不存在則建立, 否則覆蓋
 wx+ 讀取 + 寫入, 若檔案不存在則拋出例外
 a 附加寫入, 若檔案不存在則建立
 ax  附加寫入, 若檔案不存在則拋出例外
 a+ 讀取 + 附加寫入, 若檔案不存在則建立
 ax+  讀取 + 附加寫入, 若檔案不存在則拋出例外

備選參數 mode 為表示權限之整數, 預設為 0o666. 最後一個參數 callback 為回呼函數, 有兩個參數 err 與 fd, 其中 fd 即為傳回之檔案描述子 (整數).

關閉檔案 :
非同步 : fs.close(fd, callback)
同步 :     fs.closeSync(fd)

此函數用來關閉檔案, 第一參數 fd 為檔案描述子 (整數), 參數 callback 為回呼函數, 只有一個參數 err. 例如 :

//fs_test_22.js
var fs=require("fs");
fs.open("./test.txt", 'a', function(err, fd) {     //開啟檔案
  if (err) throw err;
  console.log("檔案開啟操作完成!");
  console.log("檔案描述表 : " + fd);
  fs.close(fd, function(err) {                          //關閉檔案
    if (err) throw err;
    console.log("檔案關閉操作完成!");
    });
  console.log("檔案關閉操作中 ... ");
  })
console.log("檔案開啟操作中 ... ");

在 Windows 執行結果如下 :

D:\Node.js\test>node fs_test.js
檔案開啟操作中 ...
檔案開啟操作完成!
檔案描述表 : 3
檔案關閉操作中 ...
檔案關閉操作完成!

在 Windows 不管是哪個檔案都是傳回 3.

上面提到 fs.stat(), fs.truncate(), fs.chown(), fs.chmod(), fs.utimes() 等函數有 fd 版本之函數, 即函數名稱前面冠 'f' : fs.fstat(), fs.truncate(), fs.fchown(), fs.fchmod(), fs.futimes() 等. 例如可使用檔案描述子與 fs.fstat() 顯示檔案資訊 :

非同步 : fs.fstat(fd, callback)
同步 :     fs.fstatSync(fd)

其中第一參數 fd 為檔案描述子, 參數 callback 為有 err 與 stats 參數之回呼函數. stats 為傳回之 fs.Stats 物件.

將上面 fs.stat() 的範例修改為如下的 fs.fstat() 版 :

//fs_test_23.js
var fs=require("fs");
fs.open("./test.txt", 'a', function(err, fd) {
  if (err) throw err;
  console.log("檔案開啟操作完成!");
  console.log("檔案描述表 : " + fd);
  fs.fstat(fd, function(err, stats) {
    if (err) throw err;
    console.log("檔案資訊取得操作完成!");
    console.log("檔案資訊 :\n");
    for(var k in stats) {
      console.log(k + ":" + stats[k]);
      }
    fs.close(fd, function(err) {
      if (err) throw err;
      console.log("檔案關閉操作完成!");
      });
    console.log("檔案關閉操作中 ... ");
    });
  console.log("檔案資訊取得操作中 ... ");
  })
console.log("檔案開啟操作中 ... ");

執行結果如下 :

D:\Node.js\test>node fs_test_23.js
檔案開啟操作中 ...
檔案開啟操作完成!
檔案描述表 : 3
檔案資訊取得操作中 ...
檔案資訊取得操作完成!
檔案資訊 :

dev:2089136718
mode:33206
nlink:1
uid:0
gid:0
rdev:0
blksize:undefined
ino:562949954052903
size:15
blocks:undefined
atimeMs:1522111655025.0005
mtimeMs:1522227853515.802
ctimeMs:1522227853515.802
birthtimeMs:1521426043495.4006
atime:Tue Mar 27 2018 08:47:35 GMT+0800 (台北標準時間)
mtime:Wed Mar 28 2018 17:04:13 GMT+0800 (台北標準時間)
ctime:Wed Mar 28 2018 17:04:13 GMT+0800 (台北標準時間)
birthtime:Mon Mar 19 2018 10:20:43 GMT+0800 (台北標準時間)
_checkModeProperty:function (property) {
  return ((this.mode & S_IFMT) === property);
}
isDirectory:function () {
  return this._checkModeProperty(constants.S_IFDIR);
}
isFile:function () {
  return this._checkModeProperty(S_IFREG);
}
isBlockDevice:function () {
  return this._checkModeProperty(constants.S_IFBLK);
}
isCharacterDevice:function () {
  return this._checkModeProperty(constants.S_IFCHR);
}
isSymbolicLink:function () {
  return this._checkModeProperty(S_IFLNK);
}
isFIFO:function () {
  return this._checkModeProperty(S_IFIFO);
}
isSocket:function () {
  return this._checkModeProperty(S_IFSOCK);
}
檔案關閉操作中 ...
檔案關閉操作完成!


11. 使用 fs.read() 操作低階檔案讀取 :

除了上面的 fs.readFile() 高階檔案讀取操作外, 也可以使用檔案描述子與 Buffer 物件以 fs.read() 進行低階檔案讀取操作, 例如指定讀取位置與讀取多少 bytes.

非同步 : fs.read(fd, buffer, offset, length, position, callback)

第一參數 fd 為 fs.open() 傳回之檔案描述子; 第二參數 buffer 為儲存讀取內容之 Buffer 物件; 第三參數 offset 是讀取後寫入 Buffer 中的偏移位置 (0 起始之整數); 第四參數 length 指定要從檔案中讀取幾個 byte; 第五參數 position 用來指定要從檔案的哪一個位置開始讀取內容, 如果 position=null 表示從檔案指位器目前位置開始讀取; 第六參數 callback 為包含 err, bytesRead, 以及 buffer 三個參數之回呼函數, 其中 bytesRead 為讀取之 Bytes 數, 而 buffer 為 Buffer 物件.

在下面範例中, 我先在檔案 test.txt 中準備好 "Hello\r\nWorld" 這兩行內容供 fs.read() 讀取, 而且以 ASCII 格式存檔 :

//fs_test_24.js
var fs=require("fs");
fs.open("./test.txt", "r", function(err, fd) {   
  if (err) throw err;
  console.log("檔案開啟操作完成!");
  console.log("檔案描述子 : " + fd);
  var buf=new Buffer(8);     //儲存要讀取的 8 個 bytes
  fs.read(fd, buf, 0, 8, 0, function(err, bytesRead, buffer) {    //從檔案頭開始讀 8 個 bytes
    if (err) throw err;
    console.log("檔案讀取操作完成!");
    console.log("讀取 Bytes 數 : " + bytesRead);
    console.log("讀取內容 : " + buffer);
    });
  console.log("檔案讀取操作中 ... ");
  });
console.log("檔案開啟操作中 ... ");

此程式中我們先建立一個長度為 8 的 Buffer 物件來儲存要讀取之 8 個 bytes 資料, 然後將此 Buffer 物件傳入 fs.read() 中, 讀取完成後, 此 Buffer 物件會被傳入回呼函數的參數 buffer, 讀取到的 Bytes 數則傳給 BytesRead 參數, 讀取到的資料為 "Hello\r\nW" :

D:\Node.js\test>node fs_test_24.js
檔案開啟操作中 ...
檔案開啟操作完成!
檔案描述子 : 3
檔案讀取操作中 ...
檔案讀取操作完成!
讀取 Bytes 數 : 8
讀取內容 : Hello
W

如果改為 fs.read(fd, buf, 0, 8, 3, ... 的話, 就會從 test.txt 的第 3 個 byte 開始讀取, 讀到的資料為 "lo\r\nWorld"  共 8 個 Bytes :

D:\Node.js\test>node fs_test_24.js
檔案開啟操作中 ...
檔案開啟操作完成!
檔案描述子 : 3
檔案讀取操作中 ...
檔案讀取操作完成!
讀取 Bytes 數 : 8
讀取內容 : lo
Worl


12. 使用 fs.write() 操作低階檔案寫入 :

除了上面的 fs.writeFile() 高階檔案寫入操作外, 也可以使用檔案描述子與 Buffer 物件以 fs.write() 進行低階檔案寫入操作, 例如指定寫入位置與寫入多少 bytes.

非同步 : fs.write(fd, buffer[, offset[, length[, position]]], callback)
同步 :     fs.writeSync(fd, buffer[, offset[, length[, position]]])

第一參數 fd 為 fs.open() 傳回之檔案描述子; 第二參數 buffer 為儲存欲寫入內容之 Buffer 物件; 第三參數 offset 是要從 Buffer 中的哪一個偏移位置開始寫入 (0 起始之整數); 第四參數 length 指定要從 Buffer 物件中讀取幾個 byte 寫入檔案; 第五參數 position 用來指定要從檔案的哪一個位置開始寫入 Buffer 內容, 如果 position=null 表示從檔案指位器目前位置開始讀取; 第六參數 callback 為包含 err, bytesWritten, 以及 buffer 三個參數之回呼函數, 其中 bytesWritten 為寫入檔案之 Bytes 數, 而 buffer 為 Buffer 物件.

下列範例使用 fs.write() 將儲存於 Buffer 物件中的 "Hello\r\nTony" 字串寫入 test.txt 中 :

//fs_test_25.js
var fs=require("fs");
fs.open("./test.txt", "r+", function(err, fd) {     //以讀取+寫入模式開啟
  if (err) return console.log(err);
  console.log("檔案開啟操作完成!");
  console.log("檔案描述子 : " + fd);
  var buf=new Buffer("Hello\r\nTony");
  fs.write(fd, buf, 0, buf.length, 0, function(err, bytesWritten, buffer) {   
    if (err) return console.log(err);
    console.log("檔案寫入操作完成!");
    console.log("寫入 Bytes 數 : " + bytesWritten);
    console.log("寫入內容 : " + buffer);
    });
  console.log("檔案寫入操作中 ... ");
  });
console.log("檔案開啟操作中 ... ");

執行結果如下 :

D:\Node.js\test>node fs_test_25.js
檔案開啟操作中 ...
檔案開啟操作完成!
檔案描述子 : 3
檔案寫入操作中 ...
檔案寫入操作完成!
寫入 Bytes 數 : 11
寫入內容 : Hello
Tony

注意, 若以 "r" 唯讀模式開啟檔案, 執行時將出現如下 "EPERM: operation not permitted" 錯誤 :

D:\Node.js\test>node fs_test_25.js
檔案開啟操作中 ...
檔案開啟操作完成!
檔案描述子 : 3
檔案寫入操作中 ...
{ Error: EPERM: operation not permitted, write errno: -4048, code: 'EPERM', syscall: 'write' }

fs.write() 還有另一個使用字串而非 Buffer 物件之多型函數 :

非同步 : fs.write(fd, string[, position[, encoding]], callback)
同步 :     fs.writeSync(fd, string[, position[, encoding]])

其中回呼函數 callback 之參數有 3 : err, written (寫入 bytes 數), string (寫入字串).

//fs_test_26.js
var fs=require("fs");
fs.open("./test.txt", "r+", function(err, fd) {   
  if (err) return console.log(err);
  console.log("檔案開啟操作完成!");
  console.log("檔案描述子 : " + fd);
  var str="Hello\r\nTony";
  fs.write(fd, str, "utf8", function(err, written, string) { 
    if (err) return console.log(err);
    console.log("檔案寫入操作完成!");
    console.log("寫入 Bytes 數 : " + written);
    console.log("寫入內容 : " + string);
    });
  console.log("檔案寫入操作中 ... ");
  });
console.log("檔案開啟操作中 ... ");

執行結果同上.


13. 使用 fs.createReadStream() 操作低階檔案讀取 :

除了 fs.read() 外, fs.createReadStream() 也可以操作低階檔案讀取, 此函數沒有回呼函數, 須掛上readable 事件監聽器, 其 API 如下 :

fs.createReadStream(path[, options])

第一參數 path 為包含路徑之檔案或目錄名稱; 備選參數 options 包含如下選項 :

flags <string> : 讀寫旗標
encoding <string> : 編碼
fd <integer> : 檔案描述器
mode <integer> : 模式
autoClose <boolean> : 自動關閉
start <integer> : 讀取開始索引
end <integer> : 讀取結束索引

//fs_test_26.js
var fs=require("fs");
var rs=fs.createReadStream("./test.txt",{encoding:'utf8'});
console.log("檔案讀取操作中 ...");
rs.on("readable", function() {
  var chunk=rs.read();
  console.log(chunk);
  });
rs.on("end", function() {
  console.log("檔案讀取操作完成!");
  });

執行結果如下 :

D:\Node.js\test>node fs_test_stream.js
檔案讀取操作中 ...
Hello
Tony
null
檔案讀取操作完成!

沒有留言 :