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スレッド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')){
"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:コマンド使用時) |
標準的なフィールド構成のスレッドです。author_id、host_hash、pass_id、ninja_info、watcohoi、is_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 |
!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で出力)
アーカイブ(過去ログ化)されたスレッドのレス内容を保存します。
ファイル形式はアクティブスレッドJSONと同一です。
アーカイブ時にアクティブ側のJSONファイルが削除され、過去ログ側に新たに書き出されます。
storage/app/kako/{boardId}/{threadKey}.json
// config/bbs.php
'kako_json_path' => env('BBS_KAKO_JSON_PATH', storage_path('app/kako'))| サービス | 操作 |
|---|---|
KakoService |
アーカイブ・復元・読み取り・削除 |
KakoService::archiveThread()が呼ばれる- アクティブJSON(
threads/{boardId}/{threadKey}.json)からレスデータを読み取り - 過去ログJSON(
kako/{boardId}/{threadKey}.json)として書き出し - アクティブJSON を削除
threadsテーブルのis_archived=true,archived_at=now()を更新
KakoService::unarchiveThread()が呼ばれる- 過去ログJSONからレスデータを読み取り
- アクティブJSONとして書き出し
- 過去ログJSONを削除
threadsテーブルのis_archived=false,archived_at=nullを更新
- 管理者手動操作(管理画面 or API)
php artisan bbs:auto-archiveコマンド(自動アーカイブ、5条件: by-date/position/res/title/stopped)
旧2ch互換DAT形式のファイル保存先です。
本システムでは使用されていませんが、将来的な2ch互換出力のために設定のみ残しています。
storage/app/dat/
// config/bbs.php
'dat_path' => env('BBS_DAT_PATH', storage_path('app/dat'))未使用です。
DatFileService が定義されていますが、本システムの運用では呼び出されません。
管理画面からアップロードされるバナー画像ファイルです。
公開ディスク(storage/app/public)に保存され、シンボリックリンク経由でWebからアクセス可能です。
storage/app/public/banners/{timestamp}_{filename}
- ファイル名はアップロード時のタイムスタンプ+元ファイル名で生成
/storage/banners/{filename}
php artisan storage:link でシンボリックリンク作成が必要となります。
jpg, jpeg, png, gif, webp, svg(最大2MB)
| コントローラ | 操作 |
|---|---|
BannerImageController |
アップロード・一覧取得・削除 |
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) SambaService、KeywordService等は明示的にCache::store('file')を指定
掲示板システムの各種操作ログです。
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フラグで排他書き込み
storage/framework/views/
LaravelのBladeテンプレートエンジンがコンパイルしたPHPファイルです。
自動生成され、手動管理不要です。
storage/framework/sessions/
現在はデータベースドライバを使用しているため、未使用です。
SESSION_DRIVER=file に変更した場合のみ使用されます。
プラグインはPHPクラスとしてplugins/ディレクトリに配置され、PluginLoaderがrequire_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)