import os import uuid import json import fcntl from datetime import datetime, timedelta, timezone from flask import Flask, request, redirect, url_for, render_template, send_file, abort from werkzeug.utils import secure_filename app = Flask(__name__) app.config['MAX_CONTENT_LENGTH'] = 20 * 1024 * 1024 app.config['UPLOAD_FOLDER'] = os.environ.get('UPLOAD_FOLDER', os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')) app.config['STORE_FOLDER'] = os.environ.get('STORE_FOLDER', os.path.join(os.path.dirname(os.path.abspath(__file__)), 'store')) app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'paste-bin-secret') EXPIRY_OPTIONS = [ ('1h', '1 hour'), ('1d', '1 day'), ('1w', '1 week'), ('1m', '1 month'), ('forever', 'Never'), ] ALLOWED_IMAGE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg', 'tiff'} ALLOWED_TEXT_EXTENSIONS = {'txt', 'py', 'js', 'ts', 'c', 'cpp', 'h', 'java', 'rb', 'go', 'rs', 'md', 'json', 'xml', 'yaml', 'yml', 'html', 'css', 'sh', 'log', 'csv', 'sql', 'ini', 'cfg', 'toml', 'lua', 'php', 'swift', 'kt', 'scala', 'r', 'pl', 'hs', 'zig', 'nix'} def ensure_dirs(): os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) os.makedirs(app.config['STORE_FOLDER'], exist_ok=True) def generate_id(): while True: pid = uuid.uuid4().hex[:8] if not os.path.exists(os.path.join(app.config['STORE_FOLDER'], pid)): return pid def parse_expiry(expiry_key): if expiry_key == 'forever': return None now = datetime.now(timezone.utc) if expiry_key == '1h': return (now + timedelta(hours=1)).isoformat() elif expiry_key == '1d': return (now + timedelta(days=1)).isoformat() elif expiry_key == '1w': return (now + timedelta(weeks=1)).isoformat() elif expiry_key == '1m': return (now + timedelta(days=30)).isoformat() return None def store_paste(paste_id, paste_data): store_path = os.path.join(app.config['STORE_FOLDER'], paste_id) with open(store_path, 'w') as f: fcntl.flock(f, fcntl.LOCK_EX) json.dump(paste_data, f) fcntl.flock(f, fcntl.LOCK_UN) def load_paste(paste_id): store_path = os.path.join(app.config['STORE_FOLDER'], paste_id) if not os.path.exists(store_path): return None with open(store_path, 'r') as f: fcntl.flock(f, fcntl.LOCK_SH) data = json.load(f) fcntl.flock(f, fcntl.LOCK_UN) return data def delete_paste(paste_id): store_path = os.path.join(app.config['STORE_FOLDER'], paste_id) paste = load_paste(paste_id) if paste and paste['type'] in ('image', 'file') and paste.get('filepath'): filepath = paste['filepath'] if os.path.exists(filepath): os.remove(filepath) if os.path.exists(store_path): os.remove(store_path) def is_expired(paste): if not paste.get('expires_at'): return False expires_at = datetime.fromisoformat(paste['expires_at']) return expires_at < datetime.now(timezone.utc) def cleanup_expired(): now = datetime.now(timezone.utc) for filename in os.listdir(app.config['STORE_FOLDER']): filepath = os.path.join(app.config['STORE_FOLDER'], filename) try: with open(filepath, 'r') as f: fcntl.flock(f, fcntl.LOCK_SH) paste = json.load(f) fcntl.flock(f, fcntl.LOCK_UN) except (json.JSONDecodeError, IOError): continue if is_expired(paste): delete_paste(filename) def is_image(filename): ext = filename.rsplit('.', 1)[-1].lower() if '.' in filename else '' return ext in ALLOWED_IMAGE_EXTENSIONS def is_text_file(filename): ext = filename.rsplit('.', 1)[-1].lower() if '.' in filename else '' return ext in ALLOWED_TEXT_EXTENSIONS def get_text_content(paste_id): content_path = os.path.join(app.config['STORE_FOLDER'], paste_id + '.txt') if os.path.exists(content_path): with open(content_path, 'r', encoding='utf-8') as f: return f.read() return None def save_text_content(paste_id, content): content_path = os.path.join(app.config['STORE_FOLDER'], paste_id + '.txt') with open(content_path, 'w', encoding='utf-8') as f: f.write(content) @app.before_request def before_request(): ensure_dirs() @app.route('/', methods=['GET']) def index(): return render_template('index.html', expiry_options=EXPIRY_OPTIONS) @app.route('/paste', methods=['POST']) def create_paste(): paste_type = request.form.get('paste_type', 'text') title = request.form.get('title', '').strip() expiry_key = request.form.get('expiry', '1d') expires_at = parse_expiry(expiry_key) created_at = datetime.now(timezone.utc).isoformat() if paste_type == 'text': content = request.form.get('content', '').strip() if not content: return render_template('index.html', expiry_options=EXPIRY_OPTIONS, error='Content cannot be empty'), 400 paste_id = generate_id() save_text_content(paste_id, content) store_paste(paste_id, { 'type': 'text', 'title': title, 'expires_at': expires_at, 'created_at': created_at, }) return redirect(url_for('view_paste', paste_id=paste_id)) elif paste_type == 'image': file = request.files.get('file') if not file or file.filename == '': return render_template('index.html', expiry_options=EXPIRY_OPTIONS, error='No file selected'), 400 filename = secure_filename(file.filename) if not is_image(filename): return render_template('index.html', expiry_options=EXPIRY_OPTIONS, error='Invalid image format. Allowed: ' + ', '.join(sorted(ALLOWED_IMAGE_EXTENSIONS))), 400 paste_id = generate_id() ext = filename.rsplit('.', 1)[-1] saved_name = f"{paste_id}.{ext}" filepath = os.path.join(app.config['UPLOAD_FOLDER'], saved_name) file.save(filepath) store_paste(paste_id, { 'type': 'image', 'title': title or filename, 'expires_at': expires_at, 'created_at': created_at, 'filepath': filepath, 'filename': filename, 'mimetype': file.mimetype or f'image/{ext}', }) return redirect(url_for('view_paste', paste_id=paste_id)) elif paste_type == 'file': file = request.files.get('file') if not file or file.filename == '': return render_template('index.html', expiry_options=EXPIRY_OPTIONS, error='No file selected'), 400 filename = secure_filename(file.filename) paste_id = generate_id() if is_text_file(filename): content = file.read().decode('utf-8', errors='replace') save_text_content(paste_id, content) store_paste(paste_id, { 'type': 'text', 'title': title or filename, 'expires_at': expires_at, 'created_at': created_at, 'filename': filename, }) else: ext = filename.rsplit('.', 1)[-1] if '.' in filename else 'bin' saved_name = f"{paste_id}.{ext}" filepath = os.path.join(app.config['UPLOAD_FOLDER'], saved_name) file.save(filepath) store_paste(paste_id, { 'type': 'file', 'title': title or filename, 'expires_at': expires_at, 'created_at': created_at, 'filepath': filepath, 'filename': filename, 'mimetype': file.mimetype or 'application/octet-stream', }) return redirect(url_for('view_paste', paste_id=paste_id)) else: return render_template('index.html', expiry_options=EXPIRY_OPTIONS, error='Invalid paste type'), 400 @app.route('/', methods=['GET']) def view_paste(paste_id): paste = load_paste(paste_id) if not paste: abort(404) if is_expired(paste): delete_paste(paste_id) abort(404) if paste['type'] == 'text': paste['data'] = get_text_content(paste_id) or '' return render_template('view_text.html', paste=paste, paste_id=paste_id) elif paste['type'] == 'image': return render_template('view_image.html', paste=paste, paste_id=paste_id) elif paste['type'] == 'file': return render_template('view_file.html', paste=paste, paste_id=paste_id) @app.route('//download', methods=['GET']) def download_file(paste_id): paste = load_paste(paste_id) if not paste: abort(404) if is_expired(paste): delete_paste(paste_id) abort(404) if paste['type'] == 'text': from io import BytesIO content = (get_text_content(paste_id) or '').encode('utf-8') return send_file(BytesIO(content), download_name=paste.get('filename', 'paste.txt'), as_attachment=True) elif paste['type'] in ('image', 'file'): filepath = paste.get('filepath') if not filepath or not os.path.exists(filepath): abort(404) return send_file(filepath, download_name=paste['filename'], as_attachment=True, mimetype=paste.get('mimetype', 'application/octet-stream')) @app.route('//raw', methods=['GET']) def raw_paste(paste_id): paste = load_paste(paste_id) if not paste: abort(404) if is_expired(paste): delete_paste(paste_id) abort(404) if paste['type'] == 'text': content = get_text_content(paste_id) or '' return content, 200, {'Content-Type': 'text/plain; charset=utf-8'} elif paste['type'] == 'image': filepath = paste.get('filepath') if not filepath or not os.path.exists(filepath): abort(404) return send_file(filepath, mimetype=paste.get('mimetype', 'image/png')) @app.errorhandler(413) def request_entity_too_large(e): return render_template('index.html', expiry_options=EXPIRY_OPTIONS, error='File too large. Maximum size is 20 MB.'), 413 if __name__ == '__main__': ensure_dirs() app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 8080)))