どうも、零細Misskey鯖缶です。
いや、新年早々アップデートチェックしてたらminioがArchの公式から外れてんじゃん。
乗り換えよう
まず、要件。
- ファイルシステムにファイルを直接保存する形式がいい。 ・・・ いちいち開発元の動向に左右されたくない。
- 配信はnginx + cloudflaredにする。
→ s3proxyをdocker-composeで組むことにしました。
docker-compose.yml
services:
s3proxy:
image: andrewgaul/s3proxy
environment:
- LOG_LEVEL=debug
- S3PROXY_ENDPOINT=http://0.0.0.0:9000
- S3PROXY_AUTHORIZATION=aws-v4
- S3PROXY_IDENTITY=admin
- S3PROXY_CREDENTIAL=shimomi-yukino-sekino-to
- JCLOUDS_PROVIDER=filesystem-nio2
- JCLOUDS_FILESYSTEM_BASEDIR=/data
volumes:
- ./volumes/s3proxy:/data
user: "101:101"
network_mode: "service:tailscale"
restart: unless-stopped
tailscale:
image: tailscale/tailscale:latest
restart: unless-stopped
networks:
- s3-net
cap_add:
- NET_ADMIN
- NET_RAW
environment:
- TS_STATE_DIR=/var/lib/tailscale
- TS_EXTRA_ARGS=--advertise-exit-node=false
volumes:
- ./tailscale_state:/var/lib/tailscale
- /dev/net/tun:/dev/net/tun
nginx:
image: nginx:alpine
volumes:
- ./volumes/s3proxy:/data:ro
- ./nginx.conf:/etc/nginx/nginx.conf:ro
networks:
- public-net
restart: unless-stopped
cloudflared:
image: cloudflare/cloudflared:latest
restart: unless-stopped
networks:
- public-net
command: tunnel run --token とてもとーくん
networks:
public-net:
driver: bridge
s3-net:
driver: bridge
ポイント
- Misskeyとの通信はTailscaleでやるので、s3proxyにはtailscaleコンテナのネットワークに便乗させる。
- nginxはroでデータディレクトリをマウントする。
-
aws-v4じゃないとMisskeyから使えないみたい。地味ハマりポイント。 -
filesystemじゃなくてfilesystem-nio2を使えとWarningされたのでそうした。 - nginx.confは
/dataをサーブするように適当に書く。 - Cloudflare Tunnelは
http://nginxをリバースするように適当に設定する。
まとめ
新年から疲れた。
minioからrcloneでファイルを吸い出すのが地味に面倒だったので、やはりデータは汎用的な形式で持つべき。
追補
APIのmime-typeが反映されないじゃんって突っ込まれたので、調べた。
$ getfattr -d /mnt/s3proxy/volumes/s3proxy/nekono/files/4785ccce-f10e-4f58-bc5b-025b60b523f8.webp
getfattr: Removing leading '/' from absolute path names
# file: mnt/s3proxy/volumes/s3proxy/nekono/files/4785ccce-f10e-4f58-bc5b-025b60b523f8.webp
user.user.cache-control="max-age=31536000, immutable"
user.user.content-disposition="inline; filename=\"f7e033fc707168467fcf99f6450c17fb_6022322677774841781.webp\"; filename*=UTF-8''f7e033fc707168467fcf99f6450c17fb_6022322677774841781.webp"
user.user.content-type="image/webp"
user.user.storage-tier="STANDARD"
というわけで、これを読んで適切に返すコードを書けば良さそう。
READMEはGitHub Copilotに書かせて何もチェックしてないのでよろしく。
追補2
s3proxyを通じて上がったファイルにはxattrが書き込まれるけど、rcloneでファイルシステムに引き上げたファイルにはxattrがついていない。
というわけで、xattrを書き込む手順が必要。
$ docker compose exec db psql -U misskey -d mk1 -c "\copy (SELECT name, uri, type FROM drive_file) TO '/dump/drive_file_dump.csv' WITH CSV HEADER"
というわけでまずはこんな感じで雑にdrive_fileテーブルを引き上げる。
import csv
import os
import urllib.parse
from pathlib import Path
# --- 設定項目 ---
DRY_RUN = True
CSV_FILE = "/tmp/drive_file_dump.csv"
STORAGE_ROOT = Path("/mnt/s3proxy/volumes/s3proxy/nekono")
def encode_content_disposition(original_name, is_thumb=False):
"""
S3Proxyの出力に合わせたContent-Dispositionヘッダー値を生成する
"""
name = f"thumb-{original_name}" if is_thumb else original_name
# 日本語などをURLエンコード
encoded_name = urllib.parse.quote(name)
# filename= の部分はASCII以外をアンダースコアに置換(S3Proxyの挙動を再現)
ascii_name = "".join([c if ord(c) < 128 else "_" for c in name])
return f'inline; filename="{ascii_name}"; filename*=UTF-8\'\'{encoded_name}'
def set_xattr_safe(file_path, attrs, storage_label):
if not file_path.exists():
return False
if DRY_RUN:
print(f"[Dry-run] ({storage_label}) Updating: {file_path.relative_to(STORAGE_ROOT)}")
for k, v in attrs.items():
print(f" {k}: {v}")
else:
try:
for k, v in attrs.items():
os.setxattr(str(file_path), k, v.encode('utf-8'))
except Exception as e:
print(f"[Error] ({storage_label}) {file_path.name}: {e}")
return False
return True
def apply_xattr_from_csv():
if not os.path.exists(CSV_FILE):
print(f"Error: CSV file not found at {CSV_FILE}")
return
with open(CSV_FILE, mode='r', encoding='utf-8') as f:
reader = csv.DictReader(f)
stats = {"original": 0, "thumbnail": 0, "webpublic": 0, "nf": 0}
for row in reader:
uri = row.get('uri', '').strip()
stored_internal = row.get('storedInternal', '').lower()
if uri == "" and stored_internal in ['f', 'false']:
original_name = row.get('name', 'file')
mime_type = row.get('type', 'application/octet-stream')
# 1. オリジナル
if key := row.get('accessKey'):
attrs = {
"user.user.content-type": mime_type,
"user.user.cache-control": "max-age=31536000, immutable",
"user.user.content-disposition": encode_content_disposition(original_name),
"user.user.storage-tier": "STANDARD"
}
if set_xattr_safe(STORAGE_ROOT / key, attrs, "Original"): stats["original"] += 1
else: stats["nf"] += 1
# 2. サムネイル
if key := row.get('thumbnailAccessKey'):
attrs = {
"user.user.content-type": "image/webp",
"user.user.cache-control": "max-age=31536000, immutable",
"user.user.content-disposition": encode_content_disposition(original_name, is_thumb=True),
"user.user.storage-tier": "STANDARD"
}
if set_xattr_safe(STORAGE_ROOT / key, attrs, "Thumbnail"): stats["thumbnail"] += 1
else: stats["nf"] += 1
# 3. Webpublic
if key := row.get('webpublicAccessKey'):
webpub_type = row.get('webpublicType') or mime_type
attrs = {
"user.user.content-type": webpub_type,
"user.user.cache-control": "max-age=31536000, immutable",
"user.user.content-disposition": encode_content_disposition(original_name),
"user.user.storage-tier": "STANDARD"
}
if set_xattr_safe(STORAGE_ROOT / key, attrs, "Webpublic"): stats["webpublic"] += 1
else: stats["nf"] += 1
print(f"\n--- 統計 ({'DRY RUN' if DRY_RUN else '実行完了'}) ---")
print(f"Original: {stats['original']} | Thumb: {stats['thumbnail']} | Webpub: {stats['webpublic']} | NotFound: {stats['nf']}")
if __name__ == "__main__":
apply_xattr_from_csv()
こんな感じでxattrを書き込む。
追補3
tarバックアップ取るときの引数
# tar --xattrs --xattrs-include='*' -c -I 'zstd' -v s3proxy/