SambaでSpotlightを使えるようにした

Sambaで公開しているボリュームをSpotlight検索できるようにしたので、備忘録を書いておく。

**なぜSpotlightを動かそうと思ったのか**
TimeMachineをsamba上で動かそうとしていた所、うまく動作しない理由にSpotlightが関係しているらしいと思われた。これを検証する為に動かしてみた。下のリンク先はそのドタバタ記である。

https://qiita.com/ftns/items/13aa5783610d9dd2b84f

### 前提
* **「Spotlightが動作した」と言うだけ**[^1]であり、それ以上の検証は行っていない。
* 筆者はJavaやelasticsearchでは **ど素人** なので、elasticsearchやfscrawlerの設定には **ほぼ確実に** 不適切な所があると思われる[^2]。ご指摘頂ければ幸いである。
* elasticsearchへのアクセスにはパスワードすら設定できない。これはsamba4.1.13でパスワード等を指定する手段が無い為。おそらく近い将来のバージョンでサポートされると思われる。
* 以下の例では、sambaサーバ上の`/share1, /share2`と言うディレクトリを、それぞれ`Share1, Share2`と言う名前で公開している。

[^1]: 実際Spotlightの動作がMac本体に比べてやや遅いような...
[^2]: elasticsearchの用語はデータベース分野の物？のせいか、理解しづらいのが一因である。

### 環境
* OS: FreeBSD 12.2-RELEASE-p11 amd64
* samba: 4.13.14

### samba
#### sambaのコンパイル
ports/pkg の sambaは、Spotlight非対応である。よってportsをSpotlight対応に設定した上で、コンパイル＆インストールする必要がある。

```
# cd /usr/ports/net/samba413
# make config
      （SPOTLIGHTにチェックを入れる）
# make clean all deinstall reinstall
```

#### smb4.conf
最低限必要な設定は、`[global]`セクションに

```conf:[global]
    # Spotlight
    spotlight backend = elasticsearch 
    elasticsearch:address = 127.0.0.1
    elasticsearch:port = 9200
```

を追加し、Spotlightをサポートするボリュームに、

```conf:[Share]
    spotlight=yes
```
を加えれば良い。
参考までに筆者が運用しているsmb4.confの抜粋を以下の折りたたみ部分に示す。

<details><summary>`smb4.conf`の例</summary><div>

``` conf:smb4.conf
[global]
    workgroup = WORKGROUP
    security = USER
    server min protocol = SMB2
    server string = <your server>
    server role = standalone server
    dos filemode = yes
    store dos attributes = yes
    map archive = no
    unix extensions = no

    # veto files
    delete veto files = yes
    veto files = /._*/.DS_Store/

    # Spotlight
    spotlight backend = elasticsearch 
    elasticsearch:address = 127.0.0.1
    elasticsearch:port = 9200

    # vfs
    # ZFSの場合
    vfs objects = zfsacl catia fruit streams_xattr
    # UFSの場合
    # vfs objects = acl_xattr catia fruit streams_xattr

    # vfs_zfsacl
    nfs4:chown = yes

    # vfs_fruit
    fruit:zero_file_id = yes
    fruit:metadata = stream
    fruit:veto_appledouble = no
    fruit:resource = xattr
    fruit:model = PowerMac

    # vfs_streams_xattr
    streams_xattr:store_stream_type = no
    streams_xattr:prefix = user.

[Share1]
    path = /share1
    read only = no
    valid users = <yourname>
    browseable = yes
    writable = yes
    spotlight = yes

[Share2]
    path = /share2
    read only = no
    valid users = <yourname>
    browseable = yes
    writable = yes
    spotlight = yes
```
</div></details>

### elasticsearch 
#### インストール
    # pkg install elasticsearch7 openjdk15

#### 設定

##### elasticsearch本体の設定
```conf:/usr/local/etc/elasticsearch/elasticsearch.yml
cluster.name: samba
node.name: node1
path.data: /var/db/elasticsearch
path.logs: /var/log/elasticsearch
network.host: 127.0.0.1
http.port: 9200
xpack.ml.enabled: false
discovery.type: single-node
xpack.security.enabled: false
```

##### JVMの設定
```conf:/usr/local/etc/elasticsearch/jvm.options
####################################
## IMPORTANT: JVM heap size
####################################
       :
     （略）
       :
##
## -Xms4g
## -Xmx4g
##
```
を、以下のように変更（heap sizeを2GByteにする場合）

```
-Xms2g
-Xmx2g
```

:::note warn
elasticsearchは、デフォルトでは物理メモリの半分を確保してしまう。Spotlightのみに使うなら、2Gbyte程度で十分である。
以下のscript(esmem.py)でelasticsearchが消費しているheapの割合を調べられるので、割り当て量が妥当か確認できる。
:::

<details><summary>スクリプト： `esmem.py`</summary><div>

```python:esmem.py
#!/usr/bin/env python3

import requests
import json
import time

r = requests.get('http://localhost:9200/_nodes/stats/jvm')
j = json.loads(r.text)

for node in j['nodes'].items():
    id, properties = node
    timestamp = time.strftime('%Y-%m-%d %H:%M:%S %z',
                              time.localtime(properties['timestamp']/1000))
    name = properties['name']
    total = properties['jvm']['mem']['heap_used_percent']
    percent = properties['jvm']['mem']['heap_used_percent']
    print(timestamp, name, percent, sep=',')
    break

```
</div></details>

##### `/etc/rc.conf`
以下を追加

```conf:
# elasticsearch
elasticsearch_enable="YES"
elasticsearch_java_home="/usr/local/openjdk15"
```

##### 起動

```shell:
$ sudo service elasticsearch start
```

##### 起動後の設定

以下の設定[^4] により、[クラスタヘルス](https://www.elastic.co/guide/jp/elasticsearch/reference/current/gs-cluster-health.html)が yellow → greenとなった。

```shell: 
# 新しく作られるインデックスのレプリカ数を 0 に設定する。
$ curl  -H "Content-Type: application/json" -XPUT localhost:9200/_template/template_1 -d '
{
    "template" : "*",
    "settings" : {
        "number_of_shards" : 1, "number_of_replicas" : 0
    }
}'

# 既にインデックスが存在する場合、レプリカ数を 0 に変更する。
$ curl -H "Content-Type: application/json" -XPUT localhost:9200/*/_settings -d '{"number_of_replicas":0}'
```

[^4]: 「[Elasticsearchが1ノードでConditionがYellowな時の直し方](https://zenn.dev/yakumo/articles/55d2c075c7394afdb24b2fbeaf92f9e7)」を参考にした。


### fscrawler
#### インストール
[こちら](https://fscrawler.readthedocs.io/en/latest/installation.html)から適切なパッケージをダウンロードし、適当なディレクトリに展開する。
ここでは例として、`fscrawler-es7-2.9.zip`をダウンロードし、`/usr/local/fscrawler-es7-2.9/`に展開、`/usr/local/fscrawler -> fscrawler-es7-2.9`と言うシンボリックリンクを張った。

```shell:
$ cd /usr/local
$ sudo unzip <somewhere>/fscrawler-es7-2.9.zip
$ sudo ln -s fscrawler-es7-2.9 fscrawler
```

#### 設定

##### 実行時の設定

fscrawlerの実行時の設定ファイルは、defaultでは実行ユーザの`$HOME/.fscrawler/`以下に置かれるが、ここでは `/usr/local/fscrawler/config/`以下に置く事とする。

以下を実行する。
```shell:
$ sudo JAVA_HOME=/usr/local/openjdk15 /usr/local/fscrawler/bin/fscrawler job_share1 --config_dir /usr/local/fscrawler/config/ 
```

その結果、`/usr/local/fscrawler/config/job_share1/_settings.yaml` が生成されるので、これを編集する。

```conf:/usr/local/fscrawler/config/job_share1/_settings.yaml 
---
name: "job_share1"
fs:
  url: "/share1"
  update_rate: "15m"
  excludes:
  - "*/~*"
  - "*/*~"
  - "*/[.]*"
  json_support: false
  filename_as_id: false
  add_filesize: true
  remove_deleted: true
  add_as_inner_object: false
  store_source: false
  index_content: true
  attributes_support: false
  raw_metadata: false
  xml_support: false
  index_folders: true
  lang_detect: false
  continue_on_error: true
  ocr:
    language: "eng"
    enabled: true
    pdf_strategy: "ocr_and_text"
  follow_symlinks: false
elasticsearch:
  nodes:
  - url: "http://127.0.0.1:9200"
  bulk_size: 100
  flush_interval: "5s"
  byte_size: "10mb"
  ssl_verification: false
```
変更したのは、`name`（セッション名）, `url`（インデックスを作る対象のpath）, `excludes`（インデックス対象から除外するpath）, `continue_on_error`（エラー発生時に続けるか）, `ssl_verification`（SSL証明書の検証を行うか）のみである。

:::note warn
最初`excludes:`でdotで始まるファイルを除外するために、`- "*/.*"`としていたが、これでは正常動作しない。
`- "*/[.]*"`とするのが正しい。
:::

上記と同様に、
```shell:
$ sudo JAVA_HOME=/usr/local/openjdk15 /usr/local/fscrawler/bin/fscrawler job_share2 --config_dir /usr/local/fscrawler/config/ 
```

を実行し、`/usr/local/fscrawler/config/job_share2/_settings.yaml`を適宜編集する。

##### cron
fscrawlerはsambaサーバ上に常駐させる事もできるが、ここではcronで定期的に動作させる事にする。

``` shell:/usr/local/sbin/fscrawler.sh
#!/bin/sh
export JAVA_HOME=/usr/local/openjdk15
export FS_JAVA_OPTS="-DLOG_DIR=/var/log/fscrawler"

args="--loop 1 --config_dir /usr/local/fscrawler/config"

OPTS=""
JOBS=""

for OPT in "$@"; do
    case $OPT in
        --*)
            OPTS="$OPTS $OPT"
            ;;
        *)
            JOBS="$JOBS $OPT"
            ;;
    esac
done

for job in $JOBS; do
    /usr/local/fscrawler/bin/fscrawler $job $OPTS $args
done
```

:::note warn
`fscrawler.sh`では環境変数`FS_JAVA_OPTS`によりログを`/var/log/fscrawler`以下に保存するように設定しているが、`fscrawler-es7-2.7`ではcronから実行した場合ログが正常に保存されなかった。なお`fscrawler-es7-2.9`にバージョンアップする事で解決した。
:::

```shell:/etc/cron.d/fscrawler.cron
# fscrawler
#%M %H  %d  %m  %w  user  command
0   4	*   *   1-6 root  /usr/local/sbin/fscrawler.sh --silent job_share1 job_share2
0   4	*   *   0   root  /usr/local/sbin/fscrawler.sh --silent --restart job_share1 job_share2
```

##### その他
fscrawlerにインデックスを作らせたくないディレクトリには、`.fscrawlerignore` と言うファイルを作っておく。

```shell:
$ cd somewhere-you-want-to-hide-from-spotlight
$ touch .fscrawlerignore
```
なお[上記設定](#実行時の設定)では、.(dot)で始まるか、~(tilde)が先頭か最後に付いているファイル・ディレクトリを無視するように設定している。

### ログ[^3]の整理
上記設定では、elasticsearchは`/var/log/elasticsearch`以下にログファイルを作る。特に`samba-YYYY-MM-DD-1.{json,log}.gz`と言うファイルが毎日作成される。
同じようにfscrawlerは`/var/log/fscrawler`以下に`documents-YYYY-MM-DD.log.gz`と`fscrawler-YYYY-MM-DD-[0-9]+.log.gz`と言うファイルが作成される。

これらの古い物を消したいが、UNIXで使われる `newsyslog`や`logrotate`では無理である。その為Pythonで簡単なスクリプトを書いて、`cron`で動かしている。

[^3]: ここで言うログとは、UNIX文化でのログ（`/var/log/` 以下に作られるファイル）を指している。データベース文化での[（トランザクション）ログ](https://e-words.jp/w/トランザクションログ.html)とは異なるので注意。

<details><summary>スクリプト: /usr/local/sbin/elasticlog.py</summary><div>

```python:/usr/local/sbin/elasticlog.py
#!/usr/bin/env python3
#
# log cleaner for elasticsearch
# usage:
#       elasticlog.py [-d <days>] <path>
#
# log file name
#   samba-%Y-%m-%d-1.{log,json}.gz

from datetime import datetime, date, timedelta
import argparse
import os
import re
import sys

parser = argparse.ArgumentParser()
parser.add_argument("-d", "--days", help="expire if file is older than days", ty
pe=int, default=7)
parser.add_argument("path", help="path to log files", type=str, default='.')
parser.add_argument("-n", "--dry_run", help="check files but do not remove", act
ion="store_true")
args = parser.parse_args()

if not os.path.isdir(args.path):
    print(args.path, "is not a directory", file=sys.stderr)
    exit(1)

if re.match(r'.*/$', args.path):
    p = args.path
else:
    p = args.path+'/'

reg=re.compile('(^[A-Za-z][A-Za-z0-9]*)\-([0-9]{4}\-[0-9]{2}\-[0-9]{2})')
lastdays=timedelta(days=args.days)
today=date.today()

for f in os.scandir(path=p):
    m=reg.match(f.name)
    if m:
        fdate=date.fromisoformat((m.group(2)))

        if (today-fdate) > lastdays:
            if args.dry_run:
                print("rm", p+f.name, file=sys.stderr)
            else:
                try:
                    os.unlink(p+f.name)
                except (IsADirectoryError,FileNotFoundError,PermissionError) as　f:
                    print(f, file=sys.stderr)
        else:
            if args.dry_run:
                print(f.name, "is newer than", args.days, "days",
                      file=sys.stderr)

exit(0)
```

cron設定

```conf:/etc/cron.d/elasticlog.cron
# elasticlog.cron

#%M  %H %d %m %U user command
0    11  *  *  * root /usr/local/sbin/elasticlog.py /var/log/elasticsearch
5    11  *  *  * root /usr/local/sbin/elasticlog.py /var/log/fscrawler
```

</div></details>

:::note
なおelasticsearch本体を使って行う方法もあるようだが、
よく分からなかったので使っていない。
:::


### 参考

* [Samba with macOS Spotlight on FreeBSD with Elasticsearch](https://www.davidschlachter.com/misc/freebsd-samba-macos-spotlight-elasticsearch) 
このページを元に調べた
* [Spotlight - SambaWiki](https://wiki.samba.org/index.php/Spotlight) 
Samba projectによるSpotlight解説
* [Elasticsearchリファレンス [5.4] | Elastic](https://www.elastic.co/guide/jp/elasticsearch/reference/current/index.html) 
古いバージョン向けの日本語ドキュメント
* [Elasticsearch Guide [7.16] | Elastic](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) 
英語ドキュメントは、最新版が公開されている
* [Welcome to FSCrawler’s documentation! — FSCrawler 2.9-SNAPSHOT documentation](https://fscrawler.readthedocs.io/en/latest/index.html)
FSCrawlerのドキュメント