Skip to content

Latest commit

 

History

History
569 lines (450 loc) · 20.7 KB

File metadata and controls

569 lines (450 loc) · 20.7 KB

ファイルストレージガイド

Moch BBSシステムにおける、データベース以外のすべてのファイル入出力の配置場所・意味・説明をまとめたドキュメントです。


ディレクトリ全体像

backend/storage/
├── app/
│   ├── threads/              # アクティブスレッドJSON
│   │   └── {boardId}/
│   │       └── {threadKey}.json
│   ├── kako/                 # 過去ログJSON
│   │   └── {boardId}/
│   │       └── {threadKey}.json
│   ├── dat/                  # DATファイル(レガシー互換用、未使用)
│   └── public/
│       └── banners/          # バナー画像
├── framework/
│   ├── cache/data/           # ファイルキャッシュ
│   ├── sessions/             # セッション(DBドライバ使用時は未使用)
│   └── views/                # Bladeテンプレートコンパイル済みファイル
└── logs/
    ├── laravel.log           # Laravel汎用ログ
    ├── admin.log             # 管理操作ログ
    ├── error.log             # エラーログ
    ├── access.log            # アクセスログ
    ├── post.log              # 投稿ログ
    └── host.log              # ホスト情報ログ

1. アクティブスレッドJSON

概要

アクティブな(過去ログ化されていない)スレッドのレス内容を、1スレッド1ファイルのJSON形式で保存します。
スレッドのメタ情報(タイトル、レス数等)はデータベースのthreadsテーブルで管理し、レス本文のみをJSONファイルで管理します。

パス

storage/app/threads/{boardId}/{threadKey}.json
  • {boardId} — 掲示板のディレクトリ名(例: news, game
  • {threadKey} — スレッド作成時のUNIXタイムスタンプ(例: 1772927750

設定

// config/bbs.php
'thread_json_path' => env('BBS_THREAD_JSON_PATH', storage_path('app/threads'))

JSONファイル構造

{
  "thread_key": 1772927750,
  "title": "スレッドタイトル",
  "responses": [
    {
      "number": 1,
      "name": "名無しさん",
      "trip": null,
      "email": null,
      "body": "本文テキスト",
      "posted_at": "2025-03-05T12:34:56+09:00",
      "author_id": "a1b2c3d4",
      "host_hash": "abc123def456",
      "pass_id": "ninja_pass_id",
      "cap_name": null,
      "is_owner": false,
      "ninja_info": null,
      "is_deleted": false,
      "deleted_at": null,
      "deleted_by": null,
      "command_messages": []
    }
  ]
}
フィールド 説明
thread_key int スレッドキー(UNIXタイムスタンプ)
title string スレッドタイトル
responses array レス配列
responses[].number int レス番号(1始まり)
responses[].name string 投稿者名
responses[].trip string|null トリップ
responses[].email string|null メールアドレス(sage等含む)
responses[].body string 本文(生テキスト、HTMLエスケープなし)
responses[].posted_at string 投稿日時(ISO 8601形式)
responses[].author_id string 投稿者ID(8文字hex、BBS_NO_ID時は除去)
responses[].host_hash string ホスト情報ハッシュ(内部用)
responses[].pass_id string 忍法帖パスID(内部用、公開APIではフィルタ)
responses[].cap_name string|null キャップ名(CAP投稿時のみ)
responses[].is_owner bool スレ主フラグ
responses[].ninja_info string|null 忍法帖レベル表示(Lv5未満: L数値-8文字hex、Lv5以上: 8文字hexのみ)
responses[].is_deleted bool ソフトデリート済みフラグ
responses[].deleted_at string|null 削除日時
responses[].deleted_by string|null 削除者名
responses[].command_messages array コマンド実行結果メッセージ
responses[].watcohoi string|null ワッチョイ表示文字列(BBS_SLIP設定有効時)
responses[].icon_url string|null アイコン画像URL(!ico:コマンド使用時)

実例1: 基本的なスレッド

標準的なフィールド構成のスレッドです。author_idhost_hashpass_idninja_infowatcohoiis_owner など主要フィールドがすべて含まれています。

{
    "thread_key": 1772927750,
    "title": "テストスレッド",
    "responses": [
        {
            "number": 1,
            "name": "名無しさん",
            "trip": null,
            "email": null,
            "body": "テストスレッドです",
            "posted_at": "2025-03-05T12:34:56+09:00",
            "author_id": "a1b2c3d4",
            "host_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
            "pass_id": "AbCdEfGhIjKlMnOpQrStUvWx",
            "ninja_info": "f0e1d2c3",
            "watcohoi": "ワッチョイW x4Rp-7Kw2",
            "is_owner": true
        }
    ]
}

フィールド解説:

フィールド 値の例 解説
author_id "a1b2c3d4" 8文字hexのID。IPアドレスと日付から生成され、IDが同じ=同一人物と判別可能
host_hash "e3b0c442..." IPアドレス/ホスト名のSHA-256ハッシュ。管理用途の内部フィールドで、公開APIでは除外される
pass_id "AbCdEfGh..." 忍法帖パスID。Cookie/パスワードで識別されるユーザー固有値。内部用フィールドで公開APIでは除外される
ninja_info "f0e1d2c3" 忍法帖レベル表示。Lv5以上は8文字hexのみ(例: f0e1d2c3)、Lv5未満は L3-f0e1d2c3 のようにレベル付き
watcohoi "ワッチョイW x4Rp-7Kw2" ワッチョイ文字列。ISP名(半角カナ)+ ランダム文字列で構成。BBS_SLIP設定で有効/無効を制御
is_owner true スレッド作成者(スレ主)フラグ。creator_pass_idとレスのpass_idが一致する場合にtrue

実例2: アイコンコマンド・トリップ・コマンドメッセージを含むスレッド

!ico:コマンドによるアイコン画像指定、トリップ生成、管理コマンド実行結果など、多様なフィールドが含まれるスレッドの実例です。

{
    "thread_key": 1772931280,
    "title": "テストスレッド",
    "responses": [
        {
            "number": 1,
            "name": "名無しさん",
            "trip": null,
            "email": null,
            "body": "書き込みテストスレッドです",
            "posted_at": "2025-03-05T13:34:40+09:00",
            "author_id": "a1b2c3d4",
            "host_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
            "icon_url": "https:\/\/example.com\/images\/icon_sample_a.gif",
            "pass_id": "AbCdEfGhIjKlMnOpQrStUvWx",
            "ninja_info": "f0e1d2c3",
            "watcohoi": "ワッチョイW x4Rp-7Kw2",
            "is_owner": true
        },
        {
            "number": 2,
            "name": "名無しさん",
            "trip": "Xz9pQ4rY2.",
            "email": null,
            "body": "トリップ付きでアイコン設定",
            "posted_at": "2025-03-05T13:36:47+09:00",
            "author_id": "a1b2c3d4",
            "host_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
            "icon_url": "https:\/\/example.com\/images\/icon_sample_b.jpg",
            "pass_id": "AbCdEfGhIjKlMnOpQrStUvWx",
            "ninja_info": "f0e1d2c3",
            "watcohoi": "ワッチョイW x4Rp-7Kw2",
            "is_owner": true
        },
        {
            "number": 3,
            "name": "名無しさん",
            "trip": "Xz9pQ4rY2.",
            "email": null,
            "body": "アイコンが前回の設定を維持するケース",
            "posted_at": "2025-03-05T13:38:03+09:00",
            "author_id": "a1b2c3d4",
            "host_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
            "icon_url": "https:\/\/example.com\/images\/icon_sample_b.jpg",
            "pass_id": "AbCdEfGhIjKlMnOpQrStUvWx",
            "ninja_info": "f0e1d2c3",
            "watcohoi": "ワッチョイW x4Rp-7Kw2",
            "is_owner": true
        },
        {
            "number": 4,
            "name": "名無しさん",
            "trip": "Xz9pQ4rY2.",
            "email": null,
            "body": "!ico:https:\/\/example.com\/images\/icon_sample_b.jpg\nアイコンコマンドが無効の場合、コマンドが本文に残る",
            "posted_at": "2025-03-05T13:41:53+09:00",
            "author_id": "a1b2c3d4",
            "host_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
            "pass_id": "AbCdEfGhIjKlMnOpQrStUvWx",
            "ninja_info": "f0e1d2c3",
            "watcohoi": "ワッチョイW x4Rp-7Kw2",
            "is_owner": true
        },
        {
            "number": 5,
            "name": "名無しさん",
            "trip": "Xz9pQ4rY2.",
            "email": null,
            "body": "アイコンコマンド有効の場合、コマンドが除去される",
            "posted_at": "2025-03-05T13:42:31+09:00",
            "author_id": "a1b2c3d4",
            "host_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
            "icon_url": "https:\/\/example.com\/images\/icon_sample_b.jpg",
            "pass_id": "AbCdEfGhIjKlMnOpQrStUvWx",
            "ninja_info": "f0e1d2c3",
            "watcohoi": "ワッチョイW x4Rp-7Kw2",
            "is_owner": true
        },
        {
            "number": 6,
            "name": "名無しさん",
            "trip": "Xz9pQ4rY2.",
            "email": null,
            "body": "!ico:https:\/\/example.com\/images\/icon_sample_a.gif",
            "posted_at": "2025-03-05T13:45:09+09:00",
            "author_id": "a1b2c3d4",
            "host_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
            "pass_id": "AbCdEfGhIjKlMnOpQrStUvWx",
            "ninja_info": "f0e1d2c3",
            "watcohoi": "ワッチョイW x4Rp-7Kw2",
            "is_owner": true
        },
        {
            "number": 7,
            "name": "名無しさん",
            "trip": "Xz9pQ4rY2.",
            "email": null,
            "body": "",
            "posted_at": "2025-03-05T13:45:26+09:00",
            "author_id": "a1b2c3d4",
            "host_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
            "pass_id": "AbCdEfGhIjKlMnOpQrStUvWx",
            "ninja_info": "f0e1d2c3",
            "watcohoi": "ワッチョイW x4Rp-7Kw2",
            "is_owner": true,
            "command_messages": [
                "★スレッドを過去ログに移動します。"
            ]
        }
    ]
}

レスごとの解説:

レス番号 ポイント
No.1 icon_urlあり。!ico:コマンドが処理され、URLがicon_urlに格納される
No.2 tripあり(名前欄に#キー入力)。icon_urlも別URLで設定
No.3 icon_urlが前回と同じURLを維持(セッション内で記憶)
No.4 icon_urlなし。アイコンコマンド無効時は!ico:が本文にそのまま残る
No.5 icon_urlあり。アイコンコマンド有効時は本文からコマンド行が除去される
No.6 icon_urlなし。本文が!ico:のみの場合、コマンド無効時は本文に残る
No.7 body空・command_messagesあり。管理コマンド(!pool等)の実行結果

担当サービス

サービス 操作
ThreadJsonService 読み書き・削除・レス追加/更新/ソフトデリート/復元
ThreadService スレッド作成・レス投稿時にThreadJsonServiceを呼び出し
SearchService 横断検索時にJSONファイルを読み取り
KeywordService トレンドキーワード集計時にJSONファイルを読み取り

文字コード

UTF-8(JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINTで出力)


2. 過去ログJSON

概要

アーカイブ(過去ログ化)されたスレッドのレス内容を保存します。
ファイル形式はアクティブスレッドJSONと同一です。

アーカイブ時にアクティブ側のJSONファイルが削除され、過去ログ側に新たに書き出されます。

パス

storage/app/kako/{boardId}/{threadKey}.json

設定

// config/bbs.php
'kako_json_path' => env('BBS_KAKO_JSON_PATH', storage_path('app/kako'))

担当サービス

サービス 操作
KakoService アーカイブ・復元・読み取り・削除

アーカイブの流れ

  1. KakoService::archiveThread() が呼ばれる
  2. アクティブJSON(threads/{boardId}/{threadKey}.json)からレスデータを読み取り
  3. 過去ログJSON(kako/{boardId}/{threadKey}.json)として書き出し
  4. アクティブJSON を削除
  5. threadsテーブルの is_archived=true, archived_at=now() を更新

復元の流れ

  1. KakoService::unarchiveThread() が呼ばれる
  2. 過去ログJSONからレスデータを読み取り
  3. アクティブJSONとして書き出し
  4. 過去ログJSONを削除
  5. threadsテーブルの is_archived=false, archived_at=null を更新

トリガー

  • 管理者手動操作(管理画面 or API)
  • php artisan bbs:auto-archive コマンド(自動アーカイブ、5条件: by-date/position/res/title/stopped)

3. DATファイル(レガシー互換用)

概要

旧2ch互換DAT形式のファイル保存先です。
本システムでは使用されていませんが、将来的な2ch互換出力のために設定のみ残しています。

パス

storage/app/dat/

設定

// config/bbs.php
'dat_path' => env('BBS_DAT_PATH', storage_path('app/dat'))

現在の状態

未使用です。
DatFileService が定義されていますが、本システムの運用では呼び出されません。


4. バナー画像

概要

管理画面からアップロードされるバナー画像ファイルです。
公開ディスク(storage/app/public)に保存され、シンボリックリンク経由でWebからアクセス可能です。

パス

storage/app/public/banners/{timestamp}_{filename}
  • ファイル名はアップロード時のタイムスタンプ+元ファイル名で生成

公開URL

/storage/banners/{filename}

php artisan storage:link でシンボリックリンク作成が必要となります。

対応形式

jpg, jpeg, png, gif, webp, svg(最大2MB)

担当コントローラ

コントローラ 操作
BannerImageController アップロード・一覧取得・削除

5. ファイルキャッシュ

概要

Redisを使用しない方針のため、キャッシュはすべてファイルベースです。
Laravelの Cache::store('file') を使用し、2階層ディレクトリハッシュで管理されます。

パス

storage/framework/cache/data/{hash1}/{hash2}/{hash_full}

主な用途

用途 キー 保存内容 担当
アクティブユーザ数 bbs:active_users 24時間以内の投稿ユーザ数 CollectActiveUsersCommand
トレンドキーワード bbs:trending_keywords 頻出キーワード一覧 KeywordService
Samba規制 samba:{boardId}:{ip} 最終投稿時刻 SambaService
DNSBL結果 dnsbl:{ip} DNSBL問い合わせ結果 DnsblService
timecount/timeclose post_times:{boardId}:{ip} 投稿時刻配列 PostRateLimitMiddleware
KAKIKO(二重投稿防止) kakiko:{ip}:{bodyHash} 投稿済みフラグ PostRateLimitMiddleware
IPレートリミット rate_limit:{ip} 1分間の投稿回数 PostRateLimitMiddleware

注意

  • セッションはデフォルトでデータベースドライバを使用(config/session.php
  • SambaServiceKeywordService等は明示的にCache::store('file')を指定

6. ログファイル

概要

掲示板システムの各種操作ログです。
Laravelのログチャンネル機能(Monolog)で管理されます。

管理画面からの閲覧・エクスポート・削除が可能。

パスと用途

ファイル パス 内容 ローテーション上限
admin.log storage/logs/admin.log 管理者操作(ログイン/ログアウト、CRUD操作) ADMMAX(デフォルト500行)
error.log storage/logs/error.log システムエラー、警告、例外 ERRMAX(デフォルト500行)
access.log storage/logs/access.log HTTPリクエスト/レスポンス記録 ERRMAX(デフォルト500行)
post.log storage/logs/post.log スレッド作成、レス投稿の記録 ERRMAX(デフォルト500行)
host.log storage/logs/host.log IP/ホスト名情報 HSTMAX(デフォルト500行)
laravel.log storage/logs/laravel.log Laravelフレームワーク汎用ログ ローテーションなし

設定

// config/logging.phpで各チャンネルを定義
'admin' => [
    'driver' => 'single',
    'path' => storage_path('logs/admin.log'),
],
// ... 同様に bbs-error, access, post, host

担当サービス

サービス 操作
LogService ログ書き込み(admin(), error(), access(), post(), host())、ローテーション
LogController 管理画面でのログ閲覧・エクスポート・削除

ログローテーション

  • LogService::rotateLogIfNeeded() で管理
  • 10%の確率でローテーションチェックが実行される(パフォーマンス対策)
  • システム設定(ERRMAX, HSTMAX, ADMMAX)で各ログの最大行数を制御
  • 上限超過時は古い行を削除し、最新N行のみ保持
  • file_put_contents()LOCK_EX フラグで排他書き込み

7. フレームワーク生成ファイル

Bladeテンプレートキャッシュ

storage/framework/views/

LaravelのBladeテンプレートエンジンがコンパイルしたPHPファイルです。
自動生成され、手動管理不要です。

セッションファイル

storage/framework/sessions/

現在はデータベースドライバを使用しているため、未使用です。
SESSION_DRIVER=file に変更した場合のみ使用されます。


8. プラグインファイル

概要

プラグインはPHPクラスとしてplugins/ディレクトリに配置され、PluginLoaderrequire_onceで動的に読み込まれます。
ファイルI/Oとしては、プラグインコード自体の読み込みのみです。

パス

plugins/
├── Dice/
│   └── DicePlugin.php
├── Omikuji/
│   └── OmikujiPlugin.php
├── RemoveUtm/
│   └── RemoveUtmPlugin.php
└── Sannan/
    ├── SannanPlugin.php
    └── Services/
        ├── NinjaLevelService.php
        ├── NinjaSessionService.php
        ├── ThreadOwnerCommandService.php
        └── ThreadOwnerService.php

注意

プラグインの設定値はデータベース(pluginsテーブル)で管理され、ファイルには保存されません。


環境変数によるパス変更

以下の環境変数で各ストレージのパスを変更可能です。

環境変数 デフォルト値 説明
BBS_THREAD_JSON_PATH storage/app/threads アクティブスレッドJSONの保存先
BBS_KAKO_JSON_PATH storage/app/kako 過去ログJSONの保存先
BBS_DAT_PATH storage/app/dat DATファイルの保存先(未使用)

ディレクトリの初期作成

setup.sh(初期セットアップスクリプト)で以下のディレクトリが作成されます。

mkdir -p storage/app/threads
mkdir -p storage/app/kako
mkdir -p storage/app/dat
mkdir -p storage/app/public/banners
mkdir -p storage/framework/cache/data
mkdir -p storage/framework/sessions
mkdir -p storage/framework/views
mkdir -p storage/logs

各サービスもensureDirectory()メソッドで、書き込み前にディレクトリが存在しない場合は自動作成されます。(パーミッション 0755)