app.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. import os
  2. import sqlite3
  3. import uuid
  4. import hashlib
  5. import threading
  6. import time
  7. import secrets
  8. import string
  9. import argparse
  10. from datetime import datetime, timedelta
  11. from werkzeug.utils import secure_filename
  12. from flask import Flask, request, jsonify, render_template, send_from_directory, abort, make_response
  13. from werkzeug.security import generate_password_hash
  14. import re
  15. import zipfile
  16. import tarfile
  17. try:
  18. import rarfile
  19. RARFILE_AVAILABLE = True
  20. except ImportError:
  21. RARFILE_AVAILABLE = False
  22. def generate_secret_key(length=32):
  23. """Generate a random secret key with specified length"""
  24. alphabet = string.ascii_letters + string.digits + string.punctuation
  25. return ''.join(secrets.choice(alphabet) for _ in range(length))
  26. def get_archive_contents(file_path, mime_type):
  27. """Get contents of archive files (ZIP, TAR, RAR)"""
  28. try:
  29. # Check if it's a ZIP file
  30. if zipfile.is_zipfile(file_path):
  31. with zipfile.ZipFile(file_path, 'r') as zip_ref:
  32. file_list = []
  33. for info in zip_ref.infolist():
  34. if not info.is_dir():
  35. file_list.append({
  36. 'name': info.filename,
  37. 'size': info.file_size,
  38. 'compressed_size': info.compress_size,
  39. 'date_time': info.date_time,
  40. 'type': 'file'
  41. })
  42. else:
  43. file_list.append({
  44. 'name': info.filename,
  45. 'size': 0,
  46. 'type': 'directory'
  47. })
  48. return {'type': 'zip', 'files': file_list}
  49. # Check if it's a TAR file
  50. elif tarfile.is_tarfile(file_path):
  51. with tarfile.open(file_path, 'r:*') as tar_ref:
  52. file_list = []
  53. for member in tar_ref.getmembers():
  54. file_list.append({
  55. 'name': member.name,
  56. 'size': member.size,
  57. 'type': 'file' if member.isfile() else ('directory' if member.isdir() else 'other'),
  58. 'mode': oct(member.mode) if member.mode else None,
  59. 'uid': member.uid,
  60. 'gid': member.gid,
  61. 'mtime': member.mtime
  62. })
  63. return {'type': 'tar', 'files': file_list}
  64. # Check if it's a RAR file (if rarfile is available)
  65. elif RARFILE_AVAILABLE and mime_type and 'rar' in mime_type.lower():
  66. try:
  67. with rarfile.RarFile(file_path) as rar_ref:
  68. file_list = []
  69. for info in rar_ref.infolist():
  70. file_list.append({
  71. 'name': info.filename,
  72. 'size': info.file_size,
  73. 'compressed_size': info.compress_size,
  74. 'date_time': info.date_time,
  75. 'type': 'file'
  76. })
  77. return {'type': 'rar', 'files': file_list}
  78. except:
  79. pass
  80. return None
  81. except Exception as e:
  82. print(f"Error reading archive {file_path}: {e}")
  83. return None
  84. def is_archive_file(file_path, mime_type, filename):
  85. """Check if file is a supported archive format"""
  86. # Check by file extension
  87. lower_filename = filename.lower()
  88. if any(lower_filename.endswith(ext) for ext in ['.zip', '.tar', '.tar.gz', '.tar.bz2', '.tar.xz', '.tgz']):
  89. return True
  90. if RARFILE_AVAILABLE and lower_filename.endswith('.rar'):
  91. return True
  92. # Check by MIME type
  93. if mime_type:
  94. archive_mimes = [
  95. 'application/zip',
  96. 'application/x-zip-compressed',
  97. 'application/x-tar',
  98. 'application/x-gtar',
  99. 'application/x-compressed-tar',
  100. 'application/vnd.rar',
  101. 'application/x-rar-compressed'
  102. ]
  103. if any(mime in mime_type.lower() for mime in archive_mimes):
  104. return True
  105. # Check by actual file content
  106. try:
  107. if zipfile.is_zipfile(file_path) or tarfile.is_tarfile(file_path):
  108. return True
  109. except:
  110. pass
  111. return False
  112. def is_media_file(mime_type, filename):
  113. """Check if file is a supported media format (image, video, audio)"""
  114. if not mime_type:
  115. return False, None
  116. # Image files
  117. if mime_type.startswith('image/'):
  118. supported_images = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml', 'image/bmp']
  119. if mime_type in supported_images:
  120. return True, 'image'
  121. # Video files
  122. if mime_type.startswith('video/'):
  123. supported_videos = ['video/mp4', 'video/webm', 'video/ogg', 'video/avi', 'video/mov', 'video/quicktime']
  124. if mime_type in supported_videos:
  125. return True, 'video'
  126. # Audio files
  127. if mime_type.startswith('audio/'):
  128. supported_audio = ['audio/mpeg', 'audio/mp3', 'audio/wav', 'audio/ogg', 'audio/webm', 'audio/aac', 'audio/flac']
  129. if mime_type in supported_audio:
  130. return True, 'audio'
  131. return False, None
  132. def parse_arguments():
  133. """Parse command line arguments"""
  134. parser = argparse.ArgumentParser(description='Flask Temporary File Upload Server')
  135. parser.add_argument('--host', default='127.0.0.1', help='Host to bind to (default: 127.0.0.1)')
  136. parser.add_argument('--port', type=int, default=5000, help='Port to bind to (default: 5000)')
  137. parser.add_argument('--debug', action='store_true', help='Enable debug mode')
  138. parser.add_argument('--base-url', default=None, help='Base URL for download links (e.g., https://mysite.com)')
  139. parser.add_argument('--network', action='store_true', help='Bind to all network interfaces (0.0.0.0)')
  140. parser.add_argument('--max-file-size', type=int, default=16, help='Maximum file size in MB (default: 16)')
  141. return parser.parse_args()
  142. # Parse command line arguments
  143. args = parse_arguments()
  144. # Set host based on arguments
  145. if args.network:
  146. HOST = '0.0.0.0'
  147. else:
  148. HOST = args.host
  149. # Set base URL for download links
  150. BASE_URL = args.base_url or os.environ.get('BASE_URL', f'http://{HOST}:{args.port}')
  151. # Validate max file size
  152. if args.max_file_size <= 0:
  153. print("❌ Error: Maximum file size must be a positive integer")
  154. exit(1)
  155. app = Flask(__name__)
  156. app.config['SECRET_KEY'] = generate_secret_key(32)
  157. app.config['UPLOAD_FOLDER'] = 'uploads'
  158. app.config['MAX_CONTENT_LENGTH'] = args.max_file_size * 1024 * 1024 # Convert MB to bytes
  159. app.config['BASE_URL'] = BASE_URL
  160. # Ensure upload directory exists
  161. os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
  162. # Database initialization
  163. def init_db():
  164. conn = sqlite3.connect('files.db')
  165. cursor = conn.cursor()
  166. cursor.execute('''
  167. CREATE TABLE IF NOT EXISTS files (
  168. id TEXT PRIMARY KEY,
  169. original_filename TEXT NOT NULL,
  170. sanitized_filename TEXT NOT NULL,
  171. file_path TEXT NOT NULL,
  172. user_session TEXT NOT NULL,
  173. upload_date TEXT NOT NULL,
  174. expiration_date TEXT NOT NULL,
  175. duration_hours INTEGER NOT NULL,
  176. file_size INTEGER NOT NULL,
  177. mime_type TEXT
  178. )
  179. ''')
  180. conn.commit()
  181. conn.close()
  182. def sanitize_filename(filename):
  183. """Sanitize filename for safe storage"""
  184. # Remove special characters and spaces
  185. name, ext = os.path.splitext(filename)
  186. sanitized = re.sub(r'[^a-zA-Z0-9_-]', '_', name)
  187. # Generate unique identifier
  188. unique_id = str(uuid.uuid4())[:8]
  189. return f"{sanitized}_{unique_id}{ext}"
  190. def cleanup_expired_files():
  191. """Remove expired files from database and filesystem"""
  192. conn = sqlite3.connect('files.db')
  193. cursor = conn.cursor()
  194. # Get expired files
  195. cursor.execute('''
  196. SELECT file_path FROM files
  197. WHERE expiration_date < ?
  198. ''', (datetime.now().isoformat(),))
  199. expired_files = cursor.fetchall()
  200. # Delete files from filesystem
  201. for (file_path,) in expired_files:
  202. full_path = os.path.join(app.config['UPLOAD_FOLDER'], file_path)
  203. try:
  204. if os.path.exists(full_path):
  205. os.remove(full_path)
  206. print(f"Deleted expired file: {file_path}")
  207. except Exception as e:
  208. print(f"Error deleting file {file_path}: {e}")
  209. # Remove from database
  210. cursor.execute('DELETE FROM files WHERE expiration_date < ?',
  211. (datetime.now().isoformat(),))
  212. conn.commit()
  213. conn.close()
  214. print(f"Cleaned up {len(expired_files)} expired files")
  215. def cleanup_worker():
  216. """Background worker to cleanup expired files every 10 minutes"""
  217. while True:
  218. time.sleep(600) # 10 minutes
  219. cleanup_expired_files()
  220. @app.route('/')
  221. def index():
  222. max_file_size = app.config['MAX_CONTENT_LENGTH'] // (1024 * 1024) # Convert bytes to MB
  223. return render_template('index.html', max_file_size=max_file_size)
  224. @app.route('/upload', methods=['POST'])
  225. def upload_file():
  226. if 'file' not in request.files:
  227. return jsonify({'error': 'No file provided'}), 400
  228. file = request.files['file']
  229. user_session = request.form.get('user_session')
  230. duration_hours = int(request.form.get('duration_hours', 24))
  231. if file.filename == '':
  232. return jsonify({'error': 'No file selected'}), 400
  233. if not user_session:
  234. return jsonify({'error': 'No user session provided'}), 400
  235. if file:
  236. # Generate file info
  237. file_id = str(uuid.uuid4())
  238. original_filename = file.filename
  239. sanitized_filename = sanitize_filename(original_filename)
  240. upload_date = datetime.now()
  241. expiration_date = upload_date + timedelta(hours=duration_hours)
  242. # Save file
  243. file_path = sanitized_filename
  244. full_path = os.path.join(app.config['UPLOAD_FOLDER'], file_path)
  245. file.save(full_path)
  246. # Get file info
  247. file_size = os.path.getsize(full_path)
  248. mime_type = file.content_type
  249. # Save to database
  250. conn = sqlite3.connect('files.db')
  251. cursor = conn.cursor()
  252. cursor.execute('''
  253. INSERT INTO files (id, original_filename, sanitized_filename, file_path,
  254. user_session, upload_date, expiration_date, duration_hours,
  255. file_size, mime_type)
  256. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  257. ''', (file_id, original_filename, sanitized_filename, file_path, user_session,
  258. upload_date.isoformat(), expiration_date.isoformat(), duration_hours,
  259. file_size, mime_type))
  260. conn.commit()
  261. conn.close()
  262. return jsonify({
  263. 'success': True,
  264. 'file_id': file_id,
  265. 'original_filename': original_filename,
  266. 'download_url': f'{app.config["BASE_URL"]}/download/{file_id}',
  267. 'expiration_date': expiration_date.isoformat(),
  268. 'file_size': file_size
  269. })
  270. @app.route('/files/<user_session>')
  271. def get_user_files(user_session):
  272. conn = sqlite3.connect('files.db')
  273. cursor = conn.cursor()
  274. cursor.execute('''
  275. SELECT id, original_filename, upload_date, expiration_date,
  276. duration_hours, file_size, mime_type
  277. FROM files
  278. WHERE user_session = ? AND expiration_date > ?
  279. ORDER BY upload_date DESC
  280. ''', (user_session, datetime.now().isoformat()))
  281. files = cursor.fetchall()
  282. conn.close()
  283. file_list = []
  284. for file_data in files:
  285. file_list.append({
  286. 'id': file_data[0],
  287. 'original_filename': file_data[1],
  288. 'upload_date': file_data[2],
  289. 'expiration_date': file_data[3],
  290. 'duration_hours': file_data[4],
  291. 'file_size': file_data[5],
  292. 'mime_type': file_data[6],
  293. 'download_url': f'{app.config["BASE_URL"]}/download/{file_data[0]}'
  294. })
  295. return jsonify(file_list)
  296. @app.route('/download/<file_id>')
  297. def download_page(file_id):
  298. conn = sqlite3.connect('files.db')
  299. cursor = conn.cursor()
  300. cursor.execute('''
  301. SELECT original_filename, file_path, expiration_date, file_size, mime_type
  302. FROM files
  303. WHERE id = ?
  304. ''', (file_id,))
  305. file_data = cursor.fetchone()
  306. conn.close()
  307. if not file_data:
  308. return make_response(render_template('file_not_found.html'), 404)
  309. original_filename, file_path, expiration_date, file_size, mime_type = file_data
  310. # Check if file has expired
  311. if datetime.fromisoformat(expiration_date) < datetime.now():
  312. return make_response(render_template('file_not_found.html'), 410)
  313. # Check if file exists
  314. full_path = os.path.join(app.config['UPLOAD_FOLDER'], file_path)
  315. if not os.path.exists(full_path):
  316. return make_response(render_template('file_not_found.html'), 404)
  317. # Check if it's a text file for preview
  318. is_text_file = mime_type and (mime_type.startswith('text/') or mime_type == 'application/json')
  319. file_content = None
  320. if is_text_file:
  321. try:
  322. with open(full_path, 'r', encoding='utf-8') as f:
  323. file_content = f.read()
  324. except (UnicodeDecodeError, Exception):
  325. # If can't read as text, treat as binary
  326. is_text_file = False
  327. # Check if it's an archive file for preview
  328. is_archive_file_flag = is_archive_file(full_path, mime_type, original_filename)
  329. archive_contents = None
  330. if is_archive_file_flag:
  331. archive_contents = get_archive_contents(full_path, mime_type)
  332. # Check if it's a media file for preview
  333. is_media_file_flag, media_type = is_media_file(mime_type, original_filename)
  334. return render_template('download.html',
  335. file_id=file_id,
  336. filename=original_filename,
  337. file_size=file_size,
  338. mime_type=mime_type,
  339. expiration_date=expiration_date,
  340. is_text_file=is_text_file,
  341. file_content=file_content,
  342. is_archive_file=is_archive_file_flag,
  343. archive_contents=archive_contents,
  344. is_media_file=is_media_file_flag,
  345. media_type=media_type)
  346. @app.route('/preview/<file_id>')
  347. def preview_file(file_id):
  348. """Serve media files for preview"""
  349. conn = sqlite3.connect('files.db')
  350. cursor = conn.cursor()
  351. cursor.execute('''
  352. SELECT original_filename, file_path, expiration_date, mime_type
  353. FROM files
  354. WHERE id = ?
  355. ''', (file_id,))
  356. file_data = cursor.fetchone()
  357. conn.close()
  358. if not file_data:
  359. abort(404)
  360. original_filename, file_path, expiration_date, mime_type = file_data
  361. # Check if file has expired
  362. if datetime.fromisoformat(expiration_date) < datetime.now():
  363. abort(410)
  364. # Check if file exists
  365. full_path = os.path.join(app.config['UPLOAD_FOLDER'], file_path)
  366. if not os.path.exists(full_path):
  367. abort(404)
  368. # Only allow media files for preview
  369. is_media_file_flag, media_type = is_media_file(mime_type, original_filename)
  370. if not is_media_file_flag:
  371. abort(403)
  372. return send_from_directory(app.config['UPLOAD_FOLDER'], file_path, mimetype=mime_type)
  373. @app.route('/download/<file_id>/direct')
  374. def download_file_direct(file_id):
  375. conn = sqlite3.connect('files.db')
  376. cursor = conn.cursor()
  377. cursor.execute('''
  378. SELECT original_filename, file_path, expiration_date
  379. FROM files
  380. WHERE id = ?
  381. ''', (file_id,))
  382. file_data = cursor.fetchone()
  383. conn.close()
  384. if not file_data:
  385. return make_response(render_template('file_not_found.html'), 404)
  386. original_filename, file_path, expiration_date = file_data
  387. # Check if file has expired
  388. if datetime.fromisoformat(expiration_date) < datetime.now():
  389. return make_response(render_template('file_not_found.html'), 410)
  390. # Check if file exists
  391. full_path = os.path.join(app.config['UPLOAD_FOLDER'], file_path)
  392. if not os.path.exists(full_path):
  393. return make_response(render_template('file_not_found.html'), 404)
  394. return send_from_directory(app.config['UPLOAD_FOLDER'], file_path,
  395. as_attachment=True, download_name=original_filename)
  396. @app.route('/delete/<file_id>', methods=['DELETE'])
  397. def delete_file(file_id):
  398. user_session = request.json.get('user_session')
  399. if not user_session:
  400. return jsonify({'error': 'No user session provided'}), 400
  401. conn = sqlite3.connect('files.db')
  402. cursor = conn.cursor()
  403. # Get file info and verify ownership
  404. cursor.execute('''
  405. SELECT file_path FROM files
  406. WHERE id = ? AND user_session = ?
  407. ''', (file_id, user_session))
  408. file_data = cursor.fetchone()
  409. if not file_data:
  410. conn.close()
  411. return jsonify({'error': 'File not found or unauthorized'}), 404
  412. file_path = file_data[0]
  413. # Delete from filesystem
  414. full_path = os.path.join(app.config['UPLOAD_FOLDER'], file_path)
  415. try:
  416. if os.path.exists(full_path):
  417. os.remove(full_path)
  418. except Exception as e:
  419. print(f"Error deleting file {file_path}: {e}")
  420. # Delete from database
  421. cursor.execute('DELETE FROM files WHERE id = ? AND user_session = ?',
  422. (file_id, user_session))
  423. conn.commit()
  424. conn.close()
  425. return jsonify({'success': True})
  426. if __name__ == '__main__':
  427. # Print configuration info
  428. print(f"🚀 Starting Flask Temporary File Upload Server")
  429. print(f"📁 Upload folder: {app.config['UPLOAD_FOLDER']}")
  430. print(f"🔑 Secret key: {app.config['SECRET_KEY'][:8]}... (32 chars)")
  431. print(f"🌐 Base URL: {app.config['BASE_URL']}")
  432. print(f"🖥️ Server: http://{HOST}:{args.port}")
  433. print(f"🔒 Max file size: {args.max_file_size}MB")
  434. print(f"⏰ Cleanup interval: 10 minutes")
  435. print(f"🐛 Debug mode: {'ON' if args.debug else 'OFF'}")
  436. print("-" * 50)
  437. init_db()
  438. # Start cleanup worker in background
  439. cleanup_thread = threading.Thread(target=cleanup_worker, daemon=True)
  440. cleanup_thread.start()
  441. # Initial cleanup
  442. cleanup_expired_files()
  443. app.run(debug=args.debug, host=HOST, port=args.port)