Compare commits
2 Commits
a05bcb8a8d
...
c1f2cea77a
| Author | SHA1 | Date |
|---|---|---|
|
|
c1f2cea77a | 2 weeks ago |
|
|
a750545367 | 2 weeks 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