# /etc/nginx/sites-available/ephemeral-media
server {
listen 80;
server_name media.yourdomain.com;
# セキュリティヘッダー
add_header X-Content-Type-Options "nosniff";
add_header X-Frame-Options "DENY";
add_header X-XSS-Protection "1; mode=block";
add_header Content-Security-Policy "default-src 'none'; img-src 'self'; media-src 'self';";
# ファイルサイズ制限
client_max_body_size 10M;
# メディアルート
root /var/www/media;
index index.html;
# 一時ファイル配信
location /temp/ {
alias /var/www/media/temp/;
# キャッシュヘッダー(短期間)
expires 1h;
add_header Cache-Control "public, must-revalidate";
# セキュリティ
add_header X-Ephemeral "true";
add_header X-Expires-After "7-days";
# ファイル存在確認
try_files $uri @not_found;
# ログ記録
access_log /var/log/nginx/media-access.log combined;
}
# アップロード処理
location /upload {
# POST のみ許可
limit_except POST { deny all; }
# レート制限
limit_req zone=upload_zone burst=5 nodelay;
# PHP-FPM等へプロキシ
proxy_pass http://127.0.0.1:8080/upload;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# アップロードログ
access_log /var/log/nginx/upload-access.log upload_format;
}
# 直接ファイル配信
location ~* \.(jpg|jpeg|png|gif|webp|mp4|webm)$ {
# セキュリティチェック
if ($request_uri ~* "\.\./") {
return 403;
}
# MIME設定
location ~* \.jpg$ { add_header Content-Type "image/jpeg"; }
location ~* \.png$ { add_header Content-Type "image/png"; }
location ~* \.gif$ { add_header Content-Type "image/gif"; }
location ~* \.webp$ { add_header Content-Type "image/webp"; }
location ~* \.mp4$ { add_header Content-Type "video/mp4"; }
location ~* \.webm$ { add_header Content-Type "video/webm"; }
# レスポンシブ画像対応
expires 1d;
add_header Vary "Accept-Encoding";
}
# エラーページ
location @not_found {
return 404 "ファイルが見つからないか、有効期限が切れています";
}
# メインページ(アップロードフォーム等)
location = / {
try_files /index.html @upload_form;
}
location @upload_form {
return 200 '<!DOCTYPE html>
<html><head><title>一時メディア共有</title></head>
<body>
<h1>一時ファイル共有</h1>
<p>7日間で自動削除されます。</p>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="media" accept="image/*,video/*" required>
<button type="submit">アップロード</button>
</form>
</body></html>';
add_header Content-Type "text/html; charset=utf-8";
}
}
# レート制限設定
http {
limit_req_zone $binary_remote_addr zone=upload_zone:10m rate=10r/m;
# カスタムログフォーマット
log_format upload_format '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'upload_size:$content_length';
}
#!/bin/bash
validate_file_mime() {
local file_path="$1"
local allowed_mimes="$2"
local detected_mime="$(file --mime-type -b "$file_path")"
local file_extension="${file_path##*.}"
echo "=== MIME検証: $(basename "$file_path") ==="
echo "検出MIME: $detected_mime"
echo "拡張子: $file_extension"
if echo "$allowed_mimes" | grep -q "$detected_mime"; then
echo "✓ MIME許可済み"
else
echo "✗ MIME不許可"
return 1
fi
case "$detected_mime" in
"image/jpeg")
[[ "$file_extension" =~ ^(jpg|jpeg)$ ]] || { echo "✗ 拡張子不整合"; return 1; } ;;
"image/png")
[[ "$file_extension" == "png" ]] || { echo "✗ 拡張子不整合"; return 1; } ;;
"image/gif")
[[ "$file_extension" == "gif" ]] || { echo "✗ 拡張子不整合"; return 1; } ;;
"image/webp")
[[ "$file_extension" == "webp" ]] || { echo "✗ 拡張子不整合"; return 1; } ;;
"video/mp4")
[[ "$file_extension" == "mp4" ]] || { echo "✗ 拡張子不整合"; return 1; } ;;
"video/webm")
[[ "$file_extension" == "webm" ]] || { echo "✗ 拡張子不整合"; return 1; } ;;
*)
echo "✗ 未対応MIME"; return 1 ;;
esac
echo "✓ 拡張子整合性OK"
local file_size="$(stat -c%s "$file_path")"
local max_size="$((10 * 1024 * 1024))"
if [[ "$file_size" -gt "$max_size" ]]; then
echo "✗ ファイルサイズ超過 ($file_size > $max_size)"
return 1
fi
echo "✓ ファイルサイズOK ($file_size bytes)"
return 0
}
scan_for_malware() {
local file_path="$1"
if command -v clamscan >/dev/null; then
echo "ウイルススキャン実行中..."
if clamscan --quiet "$file_path"; then
echo "✓ スキャン正常"
return 0
else
echo "✗ 脅威検出またはスキャンエラー"
return 1
fi
else
echo "⚠ ClamAV未インストール。スキャンスキップ。"
return 0
fi
}
validate_file_mime "/tmp/uploaded-image.jpg" "image/jpeg,image/png,image/gif,image/webp,video/mp4,video/webm"
#!/bin/bash
fetch_and_cache_image() {
local url="$1"
local media_root="/var/www/media"
local temp_dir="$media_root/temp"
if ! echo "$url" | grep -qE '^https?://'; then
echo "エラー: 無効なURL"
return 1
fi
local url_hash="$(echo -n "$url" | sha256sum | cut -d' ' -f1)"
local timestamp="$(date +%s)"
local suggested_ext="jpg"
case "$url" in
*.png*) suggested_ext="png" ;;
*.gif*) suggested_ext="gif" ;;
*.webp*) suggested_ext="webp" ;;
*.jpeg*|*.jpg*) suggested_ext="jpg" ;;
esac
local output_file="$temp_dir/${timestamp}_${url_hash:0:12}.$suggested_ext"
echo "=== 画像取得: $url ==="
echo "出力先: $(basename "$output_file")"
if curl -L --max-time 30 --max-filesize 10485760 \
-H "User-Agent: EphemeralMediaFetcher/1.0" \
-o "$output_file" \
"$url"; then
echo "✓ ダウンロード完了"
else
echo "✗ ダウンロード失敗"
[[ -f "$output_file" ]] && rm "$output_file"
return 1
fi
if ! validate_file_mime "$output_file" "image/jpeg,image/png,image/gif,image/webp"; then
echo "✗ MIME検証失敗"
rm "$output_file"
return 1
fi
local file_size="$(stat -c%s "$output_file")"
echo "ファイルサイズ: $file_size bytes"
local public_url="https://media.yourdomain.com/temp/$(basename "$output_file")"
echo "公開URL: $public_url"
cat > "${output_file}.meta" << EOF
{
"source_url": "$url",
"fetch_time": "$timestamp",
"expires_at": "$((timestamp + 7 * 86400))",
"mime_type": "$(file --mime-type -b "$output_file")",
"file_size": $file_size,
"public_url": "$public_url"
}
EOF
echo "$public_url"
return 0
}
resize_image() {
local input_file="$1"
local max_width="${2:-800}"
local quality="${3:-85}"
if ! command -v convert >/dev/null; then
echo "ImageMagick未インストール。リサイズスキップ。"
return 0
fi
local output_file="${input_file%.*}_resized.${input_file##*.}"
convert "$input_file" \
-resize "${max_width}x${max_width}>" \
-quality "$quality" \
-strip \
"$output_file"
if [[ -f "$output_file" ]]; then
mv "$output_file" "$input_file"
echo "✓ リサイズ完了: ${max_width}px以下, 品質${quality}%"
fi
}
batch_fetch() {
local url_list_file="$1"
while IFS= read -r url; do
[[ -z "$url" || "$url" =~ ^# ]] && continue
echo "--- 処理中: $url ---"
if fetch_and_cache_image "$url"; then
echo "✓ 成功"
else
echo "✗ 失敗"
fi
echo ""
sleep 1
done < "$url_list_file"
}
<?php
header('Content-Type: application/json; charset=utf-8');
function sendError($message, $code = 400) {
http_response_code($code);
echo json_encode(['error' => $message]);
exit;
}
$config = [
'upload_dir' => '/var/www/media/temp/',
'max_size' => 10 * 1024 * 1024,
'allowed_types' => ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'video/mp4', 'video/webm'],
'retention_days' => 7
];
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
sendError('POST method required', 405);
}
if (!isset($_FILES['media']) || $_FILES['media']['error'] !== UPLOAD_ERR_OK) {
sendError('アップロードエラー: ' . ($_FILES['media']['error'] ?? 'ファイルなし'));
}
$uploaded = $_FILES['media'];
if ($uploaded['size'] > $config['max_size']) {
sendError('ファイルサイズ制限超過: ' . round($uploaded['size'] / 1024 / 1024, 2) . 'MB');
}
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$detected_mime = finfo_file($finfo, $uploaded['tmp_name']);
finfo_close($finfo);
if (!in_array($detected_mime, $config['allowed_types'])) {
sendError('サポートされていないファイル形式: ' . $detected_mime);
}
$extension_map = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
'video/mp4' => 'mp4',
'video/webm' => 'webm'
];
$extension = $extension_map[$detected_mime];
$timestamp = time();
$random = bin2hex(random_bytes(6));
$filename = sprintf('%d_%s.%s', $timestamp, $random, $extension);
$filepath = $config['upload_dir'] . $filename;
if (!move_uploaded_file($uploaded['tmp_name'], $filepath)) {
sendError('ファイル保存エラー');
}
chmod($filepath, 0644);
$metadata = [
'filename' => $filename,
'original_name' => $uploaded['name'],
'mime_type' => $detected_mime,
'size' => $uploaded['size'],
'upload_time' => $timestamp,
'expires_at' => $timestamp + ($config['retention_days'] * 86400),
'upload_ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown'
];
file_put_contents($filepath . '.meta', json_encode($metadata, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
$public_url = 'https://media.yourdomain.com/temp/' . $filename;
$log_entry = sprintf(
"[%s] アップロード成功: %s (%s, %d bytes) from %s\n",
date('Y-m-d H:i:s'),
$filename,
$detected_mime,
$uploaded['size'],
$_SERVER['REMOTE_ADDR'] ?? 'unknown'
);
file_put_contents('/var/www/media/logs/uploads.log', $log_entry, FILE_APPEND | LOCK_EX);
echo json_encode([
'success' => true,
'filename' => $filename,
'url' => $public_url,
'size' => $uploaded['size'],
'type' => $detected_mime,
'expires_in' => $config['retention_days'] . ' days',
'expires_at' => date('Y-m-d H:i:s', $metadata['expires_at'])
]);
?>
#!/bin/bash
generate_media_stats() {
local media_root="/var/www/media"
local stats_file="$media_root/logs/daily-stats.json"
local date="$(date -I)"
echo "=== メディア統計生成: $date ==="
local temp_files="$(find "$media_root/temp" -type f -name "*.jpg" -o -name "*.png" -o -name "*.gif" -o -name "*.webp" -o -name "*.mp4" -o -name "*.webm" | wc -l)"
local temp_size="$(du -sb "$media_root/temp" 2>/dev/null | cut -f1)"
local today_uploads="$(grep "$(date '+%Y-%m-%d')" "$media_root/logs/uploads.log" 2>/dev/null | wc -l)"
local today_requests="$(grep "$(date '+%d/%b/%Y')" /var/log/nginx/media-access.log 2>/dev/null | wc -l)"
local unique_ips="$(grep "$(date '+%d/%b/%Y')" /var/log/nginx/media-access.log 2>/dev/null | awk '{print $1}' | sort -u | wc -l)"
cat > "$stats_file" << EOF
{
"date": "$date",
"files": {
"total_count": $temp_files,
"total_size_bytes": $temp_size,
"total_size_mb": $(echo "scale=2; $temp_size / 1024 / 1024" | bc -l)
},
"daily": {
"uploads": $today_uploads,
"requests": $today_requests,
"unique_visitors": $unique_ips
},
"retention": {
"cleanup_files": $(find "$media_root/temp" -mtime +7 | wc -l),
"oldest_file": "$(find "$media_root/temp" -type f -printf '%T@ %p\n' | sort -n | head -1 | cut -d' ' -f2- | xargs -r basename)"
}
}
EOF
echo "統計ファイル更新: $stats_file"
echo ""
echo "=== 本日の概要 ==="
echo "現在のファイル数: $temp_files"
echo "使用容量: $(echo "scale=2; $temp_size / 1024 / 1024" | bc -l) MB"
echo "今日のアップロード: $today_uploads"
echo "今日のアクセス: $today_requests"
echo "ユニーク訪問者: $unique_ips"
}
check_alerts() {
local media_root="/var/www/media"
local temp_size="$(du -sb "$media_root/temp" 2>/dev/null | cut -f1)"
local max_size="$((1024 * 1024 * 1024))"
if [[ "$temp_size" -gt "$max_size" ]]; then
echo "⚠️ 警告: 使用容量が1GBを超えています ($temp_size bytes)"
find "$media_root/temp" -type f -mtime +3 -delete
echo "3日以上経過したファイルを削除しました"
fi
find "$media_root/temp" -type f \( -name "*.jpg" -o -name "*.png" \) | while read file; do
if ! file "$file" | grep -q "image data"; then
echo "⚠️ 破損ファイル検出: $(basename "$file")"
rm "$file"
fi
done
}
generate_media_stats
check_alerts