ファイルサーバでWebサービス提供してみた

# はじめに
ローカルにあるHTML(javaScript)からローカルファイルを操作する方法を調べた記事です。
イントラネット内で簡単なシステムがあると助かるけど、色んな制約でWebサーバが建てられない。ファイルサーバならあるけど、ファイルサーバ上で少人数で利用するWeb構築なんてできるんだっけ？って時に参考になるかもしれない記事。
サンプルで実際に動く日報Webを作成したので、それを使って解説します。

### 使った技術
 - File System Access API[(対応ブラウザはこちら)](URL "https://caniuse.com/?search=file%20system%20access%20api")
※Chrome86から標準機能として搭載された、割と新しい技術

### 完成した画面 
 - HTML+Javascriptを配布して、ファイルサーバ上のテキストファイルをDB代わりに使おうって戦略です。
"開く"でファイルを選択して画面に反映 ⇒ 編集後に"上書き保存"で内容が保存できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2543425/9a96b528-31c2-bcfc-2f91-f83d3bb11779.png)


# ソースコード
``` HTML:fileSystemAccessApiSample.html
<!doctype html>
<html lang="ja">
<head>
<title>fileSystemAccessApiSample</title>
</head>
<body>
<div>
  <button id="lastMonth" onclick="changeMonth(-1)">先月</a></button>
  <button id="nextMonth" onclick="changeMonth(1)">翌月</a></button>
</div>
<br>

<textarea id="editor" style="display:none"></textarea>
<div>
  <button id="open">開く</button>
  <button id="save">上書き保存</button>
</div>

<table id="nippo" border="1">
  <th>日付</th>
  <th id="ichiro">一郎</th>
  <th id="jiro">二郎</th>
  <th id="saburo">三郎</th>
  <th id="shiro">四郎</th>
  <th id="goro">五郎</th>
  <th id="rokuro">六郎</th>
  <th id="nanaro">七郎</th>
</table>
<script type="text/javascript" src="./fileSystemAccessApiSample.js"></script>
</body>
</html>
```
 - HTMLは特に言うことないです・・・。

``` js:fileSystemAccessApiSample.js
let fileName = "fileSystemAccessApiSample.html";
 //=====================================
 //指定の年月を取得(指定無し=>現在)
 //=====================================
let params = (new URL(document.location)).searchParams;
let year = params.get('year');
let month = params.get('month');

if(year === null || month === null){
  let d = new Date();
  year = d.getFullYear();
  month = d.getMonth() + 1;
}

 //=====================================
 //前月または翌月に移動
 //=====================================
function changeMonth(val){
  let d = new Date(year, month - 1);
  if(val == -1){
    d.setMonth(d.getMonth() - 1);
  }
  if(val == 1){
    d.setMonth(d.getMonth() + 1);
  }
  let y = d.getFullYear();
  let m = d.getMonth() + 1
  window.location.href = './' + fileName + '?year=' + y + '&month' + '=' + m;
}

 //=====================================
 //当月の一覧作成
 //=====================================
//行数：tableObject.rows.length
//列数：tableObject.rows[0].cells.length
let tableObject = document.getElementById('nippo');

for (let i = 1; i < 32; i++) {
  var tableEle = tableObject;
  var tbody = document.createElement('tbody');
  tbody.id="tbody1";
  var tr = document.createElement('tr');
  var th = document.createElement('th');
  th.innerHTML = year + "/" + month + "/" + i;
  tr.appendChild(th);

  for (let j = 1; j < tableObject.rows[0].cells.length; j++) {
    var td = document.createElement('td');
    td.innerHTML = "<textarea></textarea>";
    tr.appendChild(td);
  }
  tr.classList.add('table_striped_grey');
  tbody.appendChild(tr);
  tableEle.appendChild(tbody);
}

 //=====================================
 //読み込んだファイルを反映（ファイルはjson形式にしとけばよかった・・・）
 //=====================================
function fileOutput(result){
  let saveDate = result[0].split('\t');
  saveDate = "";
  let lastRowNum;
  for(let i = 0; i < result.length; i++){
    let val = result[i].split('\t');
    try{
      let rowNum = document.getElementById(val[1]).cellIndex;
      for(let j = 0; j < 32; j++){
        if(tableObject.rows[j].cells[0].innerHTML == val[0]){
          if(saveDate == val[0]){
            if(rowNum == lastRowNum){
              tableObject.rows[j].cells[rowNum].children[0].value = tableObject.rows[j].cells[rowNum].children[0].value + "\r\n" + val[2];
            } else{
              tableObject.rows[j].cells[rowNum].children[0].value = val[2];
            }
          } else{
            saveDate = val[0];
            tableObject.rows[j].cells[rowNum].children[0].value = val[2];
          }
          break;
        }
      }
      lastRowNum = rowNum;
    }catch(error){
      console.log("error:" + error);
    }
  }
}
 //=====================================
 //ファイル保存関数
 //=====================================
function fileSave(){
  let saveValue = "";
  for(let j = 1; j < 32; j++){
    for(let rowNum = 1; rowNum < tableObject.rows[0].cells.length; rowNum++){
      try{
        //改行コードでsplit
        let textarea = tableObject.rows[j].cells[rowNum].children[0].value.split(/\r\n|\r|\n/);
        for(i = 0; i < textarea.length; i++){
          saveValue = saveValue + tableObject.rows[j].cells[0].innerHTML + "\t" + tableObject.rows[0].cells[rowNum].id + "\t" + textarea[i] + "\r\n";
          console.log(textarea[i]);
        }
      } catch(error){
      console.log("error:" + error);
      }
    }
  }
  document.getElementById('editor').value = saveValue;
}

 //=====================================
 //ファイルIO関数
 //=====================================
let fileHandle
// テキストファイルだけ開けるオプション
const pickerOpts = {
    types: [
        {
            description: 'Texts',
            accept: {
                'text/*': ['.txt', '.text']
            }
        }
    ],
    multiple: false,
}
document.getElementById('open').onclick = async () => {
    try {
        [ fileHandle ] = await window.showOpenFilePicker(pickerOpts)
        //const root = await navigator.storage.getDirectory('');
        //const fileHandle = await root.getFile('nakayama.txt',{create:true});
        if (fileHandle.kind !== 'file') return
        const fileData = await fileHandle.getFile()
        const content = await fileData.text()
        document.getElementById('editor').value = content
        //結果を格納
        result = content.split('\n');
        fileOutput(result);
    } catch (e) {
        // ファイル選択をキャンセルした時などにここに飛ぶ
        console.error(e)
    }
}

document.getElementById('save').onclick = async () => {
    if (fileHandle.kind !== 'file') return
    fileSave()
    const content = document.getElementById('editor').value
    const writableStream = await fileHandle.createWritable()
    await writableStream.write(content)
    await writableStream.close()
}

```
 - "開く"を押下時、```document.getElementById('open').onclick = async () => {```が呼び出されます。この中のshowOpenFilePicker（←こいつがFile System Access API）がファイル情報をfileHandleに引き渡しています。
※通常、ファイルを開く際のダイアログ画面ではローカルのドキュメントフォルダが表示されるため、実際に使う場合はファイルサーバ上のテキストファイルのリンクをドキュメントフォルダ直下に配置する想定です。

 - 編集後、"上書き保存"押下で```document.getElementById('save').onclick = async () => {```が呼び出されます。保存時にセキュリティの関係で必ず確認ダイアログが表示されますが、一度許可すれば再読み込みするまでは上書き保存し放題です。

# さいごに
欲を言えば、人がファイル選択をせずに、固定値のパス指定で勝手に同じファイルを開くようにしたかったのですが、ローカルファイルの操作はセキュリティ的に非常に多くの制約があり厳しかったです。
より改善できる点があれば、是非教えてください。

# 本記事で使用したコード(GitHub)
https://github.com/NakayamaShota/fileSystemAccessApiSample

# 参考リンク(GoogleChromeLabs)
https://web.dev/file-system-access/