app.py 16 KB

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