Compare commits
2 Commits
a05bcb8a8d
...
c1f2cea77a
| Author | SHA1 | Date |
|---|---|---|
|
|
c1f2cea77a | 1 week ago |
|
|
a750545367 | 1 week ago |
4 changed files with 1206 additions and 0 deletions
@ -0,0 +1,13 @@ |
||||
node_modules/ |
||||
|
||||
uploads/ |
||||
!uploads/.gitkeep |
||||
|
||||
.env |
||||
.env.local |
||||
|
||||
npm-debug.log* |
||||
|
||||
.DS_Store |
||||
Thumbs.db |
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,21 @@ |
||||
{ |
||||
"name": "drop_site", |
||||
"version": "1.0.0", |
||||
"description": "", |
||||
"main": "index.js", |
||||
"scripts": { |
||||
"test": "echo \"Error: no test specified\" && exit 1" |
||||
}, |
||||
"repository": { |
||||
"type": "git", |
||||
"url": "https://gitea.kanawo.net/agomizi1cm/drop_site.git" |
||||
}, |
||||
"keywords": [], |
||||
"author": "", |
||||
"license": "ISC", |
||||
"type": "commonjs", |
||||
"dependencies": { |
||||
"express": "^5.2.1", |
||||
"multer": "^2.2.0" |
||||
} |
||||
} |
||||
@ -0,0 +1,146 @@ |
||||
const express = require('express'); |
||||
const multer = require('multer'); |
||||
const fs = require('fs'); |
||||
const path = require('path'); |
||||
|
||||
const app = express(); |
||||
const PORT = 5000; |
||||
|
||||
// ファイルを一時保存するディレクトリ(存在しない場合は自動作成)
|
||||
const UPLOAD_DIR = path.join(__dirname, 'uploads'); |
||||
if (!fs.existsSync(UPLOAD_DIR)) { |
||||
fs.mkdirSync(UPLOAD_DIR); |
||||
} |
||||
|
||||
// 単一スロットのチェスト状態を管理するオブジェクト
|
||||
let currentChestFile = null; |
||||
let activeDeleteTimer = null; |
||||
|
||||
// 3分(180,000ミリ秒)の有効期限
|
||||
const FILE_LIFETIME = 3 * 60 * 1000; |
||||
|
||||
// 物理ファイルとメタデータを完全に削除する関数
|
||||
const clearChest = () => { |
||||
if (currentChestFile) { |
||||
const filePath = path.join(UPLOAD_DIR, currentChestFile.savedName); |
||||
fs.unlink(filePath, (err) => { |
||||
if (err && err.code !== 'ENOENT') { |
||||
console.error('ファイル削除エラー:', err); |
||||
} |
||||
}); |
||||
currentChestFile = null; |
||||
} |
||||
if (activeDeleteTimer) { |
||||
clearTimeout(activeDeleteTimer); |
||||
activeDeleteTimer = null; |
||||
} |
||||
console.log('チェストが空になりました(有効期限切れ、または手動上書き)。'); |
||||
}; |
||||
|
||||
// Multerのストレージ設定(ファイル名サニタイズのため、ランダムな名前に変更して保存)
|
||||
const storage = multer.diskStorage({ |
||||
destination: (req, file, cb) => { |
||||
cb(null, UPLOAD_DIR); |
||||
}, |
||||
filename: (req, file, cb) => { |
||||
// ディレクトリトラバーサル防止のため、拡張子だけ維持してファイル名はタイムスタンプに変更
|
||||
const ext = path.extname(file.originalname); |
||||
cb(null, `${Date.now()}${ext}`); |
||||
} |
||||
}); |
||||
|
||||
const upload = multer({ |
||||
storage: storage, |
||||
limits: { fileSize: 100 * 1024 * 1024 } // Nginx側と合わせて100MB制限
|
||||
}); |
||||
|
||||
// メイン画面(状態に応じてアップロードかダウンロードのUIを出し分ける)
|
||||
app.get('/', (req, res) => { |
||||
if (!currentChestFile) { |
||||
// チェストが空の場合:アップロード用フォームを返す
|
||||
res.send(` |
||||
<!DOCTYPE html> |
||||
<html> |
||||
<head><title>File Chest</title><meta name="viewport" content="width=device-width, initial-scale=1.0"></head> |
||||
<body> |
||||
<h1>File Chest (空)</h1> |
||||
<form action="/upload" method="post" enctype="multipart/form-data"> |
||||
<input type="file" name="chestFile" required> |
||||
<button type="submit">アップロード (有効期限3分)</button> |
||||
</form> |
||||
</body> |
||||
</html> |
||||
`);
|
||||
} else { |
||||
// チェストにファイルがある場合:ダウンロード用リンクを返す
|
||||
const remainingTime = Math.max(0, Math.規劃((currentChestFile.expiresAt - Date.now()) / 1000)); |
||||
res.send(` |
||||
<!DOCTYPE html> |
||||
<html> |
||||
<head><title>File Chest</title><meta name="viewport" content="width=device-width, initial-scale=1.0"></head> |
||||
<body> |
||||
<h1>File Chest (保管中)</h1> |
||||
<p>ファイル名: <strong>${currentChestFile.originalName}</strong></p> |
||||
<p>サイズ: ${(currentChestFile.size / 1024 / 1024).toFixed(2)} MB</p> |
||||
<p>残り時間: 約 ${remainingTime} 秒</p> |
||||
<a href="/download"><button style="font-size: 1.2em; padding: 10px 20px;">ダウンロード</button></a> |
||||
<hr> |
||||
<p>※新しいファイルをアップロードすると、現在のファイルは即座に上書き削除されます。</p> |
||||
<form action="/upload" method="post" enctype="multipart/form-data"> |
||||
<input type="file" name="chestFile" required> |
||||
<button type="submit">新しく上書きアップロード</button> |
||||
</form> |
||||
</body> |
||||
</html> |
||||
`);
|
||||
} |
||||
}); |
||||
|
||||
// ファイルアップロードAPI
|
||||
app.post('/upload', upload.single('chestFile'), (req, res) => { |
||||
if (!req.file) { |
||||
return res.status(400).send('ファイルがありません。'); |
||||
} |
||||
|
||||
// 既存のファイルがあれば、タイマーをクリアして物理ファイルを即時削除(1スロット制限の維持)
|
||||
clearChest(); |
||||
|
||||
// 新しいファイル情報をチェストに格納
|
||||
currentChestFile = { |
||||
savedName: req.file.filename, |
||||
originalName: req.file.originalname, |
||||
mimetype: req.file.mimetype, |
||||
size: req.file.size, |
||||
expiresAt: Date.now() + FILE_LIFETIME |
||||
}; |
||||
|
||||
// 3分後の自動削除タイマーを起動
|
||||
activeDeleteTimer = setTimeout(() => { |
||||
clearChest(); |
||||
}, FILE_LIFETIME); |
||||
|
||||
res.redirect('/'); |
||||
}); |
||||
|
||||
// ファイルダウンロードAPI
|
||||
app.get('/download', (req, res) => { |
||||
if (!currentChestFile) { |
||||
return res.status(404).send('チェストは空か、有効期限が切れています。'); |
||||
} |
||||
|
||||
const filePath = path.join(UPLOAD_DIR, currentChestFile.savedName); |
||||
|
||||
// 安全な強制ダウンロードの実行
|
||||
res.download(filePath, currentChestFile.originalName, (err) => { |
||||
if (err) { |
||||
console.error('ダウンロードエラー:', err); |
||||
if (!res.headersSent) { |
||||
res.status(500).send('ダウンロード中にエラーが発生しました。'); |
||||
} |
||||
} |
||||
}); |
||||
}); |
||||
|
||||
app.listen(PORT, () => { |
||||
console.log(`Server is running on http://localhost:${PORT}`); |
||||
}); |
||||
Loading…
Reference in new issue