app.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  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, pdf)"""
  114. if not mime_type:
  115. return False, None
  116. # Image files (including HEIC/HEIF)
  117. if mime_type.startswith('image/'):
  118. supported_images = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml', 'image/bmp', 'image/heic', 'image/heif']
  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. # PDF files
  132. if mime_type == 'application/pdf':
  133. return True, 'pdf'
  134. return False, None
  135. def parse_arguments():
  136. """Parse command line arguments"""
  137. parser = argparse.ArgumentParser(description='Flask Temporary File Upload Server')
  138. parser.add_argument('--host', default='127.0.0.1', help='Host to bind to (default: 127.0.0.1)')
  139. parser.add_argument('--port', type=int, default=5000, help='Port to bind to (default: 5000)')
  140. parser.add_argument('--debug', action='store_true', help='Enable debug mode')
  141. parser.add_argument('--base-url', default=None, help='Base URL for download links (e.g., https://mysite.com)')
  142. parser.add_argument('--network', action='store_true', help='Bind to all network interfaces (0.0.0.0)')
  143. parser.add_argument('--max-file-size', type=int, default=16, help='Maximum file size in MB (default: 16)')
  144. return parser.parse_args()
  145. # Parse command line arguments
  146. args = parse_arguments()
  147. # Set host based on arguments
  148. if args.network:
  149. HOST = '0.0.0.0'
  150. else:
  151. HOST = args.host
  152. # Set base URL for download links
  153. BASE_URL = args.base_url or os.environ.get('BASE_URL', f'http://{HOST}:{args.port}')
  154. # Validate max file size
  155. if args.max_file_size <= 0:
  156. print("❌ Error: Maximum file size must be a positive integer")
  157. exit(1)
  158. app = Flask(__name__)
  159. app.config['SECRET_KEY'] = generate_secret_key(32)
  160. app.config['UPLOAD_FOLDER'] = 'uploads'
  161. app.config['MAX_CONTENT_LENGTH'] = args.max_file_size * 1024 * 1024 # Convert MB to bytes
  162. app.config['BASE_URL'] = BASE_URL
  163. # Ensure upload directory exists
  164. os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
  165. # Database initialization
  166. def init_db():
  167. conn = sqlite3.connect('files.db')
  168. cursor = conn.cursor()
  169. cursor.execute('''
  170. CREATE TABLE IF NOT EXISTS files (
  171. id TEXT PRIMARY KEY,
  172. original_filename TEXT NOT NULL,
  173. sanitized_filename TEXT NOT NULL,
  174. file_path TEXT NOT NULL,
  175. user_session TEXT NOT NULL,
  176. upload_date TEXT NOT NULL,
  177. expiration_date TEXT NOT NULL,
  178. duration_hours INTEGER NOT NULL,
  179. file_size INTEGER NOT NULL,
  180. mime_type TEXT
  181. )
  182. ''')
  183. conn.commit()
  184. conn.close()
  185. def sanitize_filename(filename):
  186. """Sanitize filename for safe storage"""
  187. # Remove special characters and spaces
  188. name, ext = os.path.splitext(filename)
  189. sanitized = re.sub(r'[^a-zA-Z0-9_-]', '_', name)
  190. # Generate unique identifier
  191. unique_id = str(uuid.uuid4())[:8]
  192. return f"{sanitized}_{unique_id}{ext}"
  193. def cleanup_expired_files():
  194. """Remove expired files from database and filesystem"""
  195. conn = sqlite3.connect('files.db')
  196. cursor = conn.cursor()
  197. # Get expired files
  198. cursor.execute('''
  199. SELECT file_path FROM files
  200. WHERE expiration_date < ?
  201. ''', (datetime.now().isoformat(),))
  202. expired_files = cursor.fetchall()
  203. # Delete files from filesystem
  204. for (file_path,) in expired_files:
  205. full_path = os.path.join(app.config['UPLOAD_FOLDER'], file_path)
  206. try:
  207. if os.path.exists(full_path):
  208. os.remove(full_path)
  209. print(f"Deleted expired file: {file_path}")
  210. except Exception as e:
  211. print(f"Error deleting file {file_path}: {e}")
  212. # Remove from database
  213. cursor.execute('DELETE FROM files WHERE expiration_date < ?',
  214. (datetime.now().isoformat(),))
  215. conn.commit()
  216. conn.close()
  217. print(f"Cleaned up {len(expired_files)} expired files")
  218. def cleanup_worker():
  219. """Background worker to cleanup expired files every 10 minutes"""
  220. while True:
  221. time.sleep(600) # 10 minutes
  222. cleanup_expired_files()
  223. @app.route('/')
  224. def index():
  225. max_file_size = app.config['MAX_CONTENT_LENGTH'] // (1024 * 1024) # Convert bytes to MB
  226. return render_template('index.html', max_file_size=max_file_size)
  227. @app.route('/upload', methods=['POST'])
  228. def upload_file():
  229. if 'file' not in request.files:
  230. return jsonify({'error': 'No file provided'}), 400
  231. file = request.files['file']
  232. user_session = request.form.get('user_session')
  233. duration_hours = int(request.form.get('duration_hours', 24))
  234. if file.filename == '':
  235. return jsonify({'error': 'No file selected'}), 400
  236. if not user_session:
  237. return jsonify({'error': 'No user session provided'}), 400
  238. if file:
  239. # Generate file info
  240. file_id = str(uuid.uuid4())
  241. original_filename = file.filename
  242. sanitized_filename = sanitize_filename(original_filename)
  243. upload_date = datetime.now()
  244. expiration_date = upload_date + timedelta(hours=duration_hours)
  245. # Save file
  246. file_path = sanitized_filename
  247. full_path = os.path.join(app.config['UPLOAD_FOLDER'], file_path)
  248. file.save(full_path)
  249. # Get file info
  250. file_size = os.path.getsize(full_path)
  251. mime_type = file.content_type
  252. # Save to database
  253. conn = sqlite3.connect('files.db')
  254. cursor = conn.cursor()
  255. cursor.execute('''
  256. INSERT INTO files (id, original_filename, sanitized_filename, file_path,
  257. user_session, upload_date, expiration_date, duration_hours,
  258. file_size, mime_type)
  259. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  260. ''', (file_id, original_filename, sanitized_filename, file_path, user_session,
  261. upload_date.isoformat(), expiration_date.isoformat(), duration_hours,
  262. file_size, mime_type))
  263. conn.commit()
  264. conn.close()
  265. return jsonify({
  266. 'success': True,
  267. 'file_id': file_id,
  268. 'original_filename': original_filename,
  269. 'download_url': f'{app.config["BASE_URL"]}/download/{file_id}',
  270. 'expiration_date': expiration_date.isoformat(),
  271. 'file_size': file_size
  272. })
  273. @app.route('/files/<user_session>')
  274. def get_user_files(user_session):
  275. conn = sqlite3.connect('files.db')
  276. cursor = conn.cursor()
  277. cursor.execute('''
  278. SELECT id, original_filename, upload_date, expiration_date,
  279. duration_hours, file_size, mime_type
  280. FROM files
  281. WHERE user_session = ? AND expiration_date > ?
  282. ORDER BY upload_date DESC
  283. ''', (user_session, datetime.now().isoformat()))
  284. files = cursor.fetchall()
  285. conn.close()
  286. file_list = []
  287. for file_data in files:
  288. file_list.append({
  289. 'id': file_data[0],
  290. 'original_filename': file_data[1],
  291. 'upload_date': file_data[2],
  292. 'expiration_date': file_data[3],
  293. 'duration_hours': file_data[4],
  294. 'file_size': file_data[5],
  295. 'mime_type': file_data[6],
  296. 'download_url': f'{app.config["BASE_URL"]}/download/{file_data[0]}'
  297. })
  298. return jsonify(file_list)
  299. @app.route('/download/<file_id>')
  300. def download_page(file_id):
  301. conn = sqlite3.connect('files.db')
  302. cursor = conn.cursor()
  303. cursor.execute('''
  304. SELECT original_filename, file_path, expiration_date, file_size, mime_type
  305. FROM files
  306. WHERE id = ?
  307. ''', (file_id,))
  308. file_data = cursor.fetchone()
  309. conn.close()
  310. if not file_data:
  311. return make_response(render_template('file_not_found.html'), 404)
  312. original_filename, file_path, expiration_date, file_size, mime_type = file_data
  313. # Check if file has expired
  314. if datetime.fromisoformat(expiration_date) < datetime.now():
  315. return make_response(render_template('file_not_found.html'), 410)
  316. # Check if file exists
  317. full_path = os.path.join(app.config['UPLOAD_FOLDER'], file_path)
  318. if not os.path.exists(full_path):
  319. return make_response(render_template('file_not_found.html'), 404)
  320. # Check if it's a text file for preview
  321. is_text_file = mime_type and (mime_type.startswith('text/') or mime_type == 'application/json')
  322. file_content = None
  323. if is_text_file:
  324. try:
  325. with open(full_path, 'r', encoding='utf-8') as f:
  326. file_content = f.read()
  327. except (UnicodeDecodeError, Exception):
  328. # If can't read as text, treat as binary
  329. is_text_file = False
  330. # Check if it's an archive file for preview
  331. is_archive_file_flag = is_archive_file(full_path, mime_type, original_filename)
  332. archive_contents = None
  333. if is_archive_file_flag:
  334. archive_contents = get_archive_contents(full_path, mime_type)
  335. # Check if it's a media file for preview
  336. is_media_file_flag, media_type = is_media_file(mime_type, original_filename)
  337. return render_template('download.html',
  338. file_id=file_id,
  339. filename=original_filename,
  340. file_size=file_size,
  341. mime_type=mime_type,
  342. expiration_date=expiration_date,
  343. is_text_file=is_text_file,
  344. file_content=file_content,
  345. is_archive_file=is_archive_file_flag,
  346. archive_contents=archive_contents,
  347. is_media_file=is_media_file_flag,
  348. media_type=media_type)
  349. @app.route('/preview/<file_id>')
  350. def preview_file(file_id):
  351. """Serve media files for preview"""
  352. conn = sqlite3.connect('files.db')
  353. cursor = conn.cursor()
  354. cursor.execute('''
  355. SELECT original_filename, file_path, expiration_date, mime_type
  356. FROM files
  357. WHERE id = ?
  358. ''', (file_id,))
  359. file_data = cursor.fetchone()
  360. conn.close()
  361. if not file_data:
  362. abort(404)
  363. original_filename, file_path, expiration_date, mime_type = file_data
  364. # Check if file has expired
  365. if datetime.fromisoformat(expiration_date) < datetime.now():
  366. abort(410)
  367. # Check if file exists
  368. full_path = os.path.join(app.config['UPLOAD_FOLDER'], file_path)
  369. if not os.path.exists(full_path):
  370. abort(404)
  371. # Only allow media files for preview
  372. is_media_file_flag, media_type = is_media_file(mime_type, original_filename)
  373. if not is_media_file_flag:
  374. abort(403)
  375. return send_from_directory(app.config['UPLOAD_FOLDER'], file_path, mimetype=mime_type)
  376. @app.route('/download/<file_id>/direct')
  377. def download_file_direct(file_id):
  378. conn = sqlite3.connect('files.db')
  379. cursor = conn.cursor()
  380. cursor.execute('''
  381. SELECT original_filename, file_path, expiration_date
  382. FROM files
  383. WHERE id = ?
  384. ''', (file_id,))
  385. file_data = cursor.fetchone()
  386. conn.close()
  387. if not file_data:
  388. return make_response(render_template('file_not_found.html'), 404)
  389. original_filename, file_path, expiration_date = file_data
  390. # Check if file has expired
  391. if datetime.fromisoformat(expiration_date) < datetime.now():
  392. return make_response(render_template('file_not_found.html'), 410)
  393. # Check if file exists
  394. full_path = os.path.join(app.config['UPLOAD_FOLDER'], file_path)
  395. if not os.path.exists(full_path):
  396. return make_response(render_template('file_not_found.html'), 404)
  397. return send_from_directory(app.config['UPLOAD_FOLDER'], file_path,
  398. as_attachment=True, download_name=original_filename)
  399. @app.route('/delete/<file_id>', methods=['DELETE'])
  400. def delete_file(file_id):
  401. user_session = request.json.get('user_session')
  402. if not user_session:
  403. return jsonify({'error': 'No user session provided'}), 400
  404. conn = sqlite3.connect('files.db')
  405. cursor = conn.cursor()
  406. # Get file info and verify ownership
  407. cursor.execute('''
  408. SELECT file_path FROM files
  409. WHERE id = ? AND user_session = ?
  410. ''', (file_id, user_session))
  411. file_data = cursor.fetchone()
  412. if not file_data:
  413. conn.close()
  414. return jsonify({'error': 'File not found or unauthorized'}), 404
  415. file_path = file_data[0]
  416. # Delete from filesystem
  417. full_path = os.path.join(app.config['UPLOAD_FOLDER'], file_path)
  418. try:
  419. if os.path.exists(full_path):
  420. os.remove(full_path)
  421. except Exception as e:
  422. print(f"Error deleting file {file_path}: {e}")
  423. # Delete from database
  424. cursor.execute('DELETE FROM files WHERE id = ? AND user_session = ?',
  425. (file_id, user_session))
  426. conn.commit()
  427. conn.close()
  428. return jsonify({'success': True})
  429. if __name__ == '__main__':
  430. # Print configuration info
  431. print(f"🚀 Starting Flask Temporary File Upload Server")
  432. print(f"📁 Upload folder: {app.config['UPLOAD_FOLDER']}")
  433. print(f"🔑 Secret key: {app.config['SECRET_KEY'][:8]}... (32 chars)")
  434. print(f"🌐 Base URL: {app.config['BASE_URL']}")
  435. print(f"🖥️ Server: http://{HOST}:{args.port}")
  436. print(f"🔒 Max file size: {args.max_file_size}MB")
  437. print(f"⏰ Cleanup interval: 10 minutes")
  438. print(f"🐛 Debug mode: {'ON' if args.debug else 'OFF'}")
  439. print("-" * 50)
  440. init_db()
  441. # Start cleanup worker in background
  442. cleanup_thread = threading.Thread(target=cleanup_worker, daemon=True)
  443. cleanup_thread.start()
  444. # Initial cleanup
  445. cleanup_expired_files()
  446. app.run(debug=args.debug, host=HOST, port=args.port)