├── img ├── home.png ├── view.png └── wechat-github-ai.png ├── requirements.txt ├── run.py ├── .env ├── .env.example ├── app ├── extensions.py ├── utils │ ├── password.py │ ├── id_generator.py │ └── encryption.py ├── api │ ├── page_routes.py │ └── paste_routes.py ├── static │ ├── images │ │ ├── favicon.svg │ │ └── logo.svg │ ├── css │ │ └── style.css │ └── js │ │ └── main.js ├── config.py ├── __init__.py ├── views.py ├── templates │ ├── pages │ │ ├── about.html │ │ ├── help.html │ │ ├── terms.html │ │ ├── privacy.html │ │ └── contact.html │ ├── error.html │ ├── index.html │ ├── edit.html │ ├── password.html │ └── base.html └── services │ └── paste_service.py ├── Dockerfile ├── config.py ├── docker-compose.yml ├── LICENSE ├── nginx.conf └── README.md /img/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zxcv0221/netcut/HEAD/img/home.png -------------------------------------------------------------------------------- /img/view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zxcv0221/netcut/HEAD/img/view.png -------------------------------------------------------------------------------- /img/wechat-github-ai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zxcv0221/netcut/HEAD/img/wechat-github-ai.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask==3.0.0 2 | flask-cors==4.0.0 3 | flask-wtf==1.2.1 4 | redis==5.0.1 5 | cryptography==41.0.7 6 | python-dotenv==1.0.0 -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | from app import create_app 2 | 3 | app = create_app() 4 | 5 | if __name__ == '__main__': 6 | app.run(host='0.0.0.0', port=5000) -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | FLASK_APP=run.py 2 | FLASK_ENV=development 3 | SECRET_KEY=your-secret-key-here 4 | ENCRYPTION_KEY=AOulIzdWROww2CikVmp5lMQ-dc9BN8jeLSDoV1slNk8= -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Flask配置 2 | SECRET_KEY=your-secret-key-here 3 | FLASK_ENV=production 4 | 5 | # Redis配置 6 | REDIS_HOST=redis 7 | REDIS_PORT=6379 8 | REDIS_DB=0 9 | 10 | # 加密配置 11 | ENCRYPTION_KEY=your-encryption-key-here 12 | 13 | # 其他配置 14 | MAX_CONTENT_LENGTH=52428800 # 50MB in bytes -------------------------------------------------------------------------------- /app/extensions.py: -------------------------------------------------------------------------------- 1 | from flask_cors import CORS 2 | from flask_assets import Environment 3 | 4 | # 初始化CORS 5 | cors = CORS() 6 | 7 | # 初始化Assets 8 | assets = Environment() 9 | 10 | def init_extensions(app): 11 | """初始化所有Flask扩展""" 12 | cors.init_app(app) 13 | assets.init_app(app) -------------------------------------------------------------------------------- /app/utils/password.py: -------------------------------------------------------------------------------- 1 | import bcrypt 2 | 3 | def hash_password(password: str) -> str: 4 | """对密码进行哈希处理""" 5 | return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() 6 | 7 | def verify_password(password: str, password_hash: str) -> bool: 8 | """验证密码""" 9 | return bcrypt.checkpw(password.encode(), password_hash.encode()) -------------------------------------------------------------------------------- /app/utils/id_generator.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | def generate_id(length: int = 8) -> str: 5 | """生成随机ID 6 | 7 | Args: 8 | length: ID长度,默认8位 9 | 10 | Returns: 11 | str: 随机生成的ID 12 | """ 13 | chars = string.ascii_letters + string.digits 14 | return ''.join(random.choice(chars) for _ in range(length)) -------------------------------------------------------------------------------- /app/utils/encryption.py: -------------------------------------------------------------------------------- 1 | from cryptography.fernet import Fernet 2 | from flask import current_app 3 | 4 | def get_encryption_key(): 5 | """获取加密密钥""" 6 | return current_app.config['ENCRYPTION_KEY'].encode() 7 | 8 | def encrypt_content(content): 9 | """加密内容""" 10 | if not content: 11 | return None 12 | f = Fernet(get_encryption_key()) 13 | return f.encrypt(content.encode()).decode() 14 | 15 | def decrypt_content(encrypted_content): 16 | """解密内容""" 17 | if not encrypted_content: 18 | return None 19 | f = Fernet(get_encryption_key()) 20 | return f.decrypt(encrypted_content.encode()).decode() -------------------------------------------------------------------------------- /app/api/page_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template 2 | 3 | bp = Blueprint('pages', __name__, url_prefix='/pages') 4 | 5 | @bp.route('/about') 6 | def about(): 7 | """关于页面""" 8 | return render_template('pages/about.html') 9 | 10 | @bp.route('/help') 11 | def help(): 12 | """帮助页面""" 13 | return render_template('pages/help.html') 14 | 15 | @bp.route('/terms') 16 | def terms(): 17 | """服务条款页面""" 18 | return render_template('pages/terms.html') 19 | 20 | @bp.route('/privacy') 21 | def privacy(): 22 | """隐私政策页面""" 23 | return render_template('pages/privacy.html') 24 | 25 | @bp.route('/contact') 26 | def contact(): 27 | """联系我们页面""" 28 | return render_template('pages/contact.html') -------------------------------------------------------------------------------- /app/static/images/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 使用Python 3.9作为基础镜像 2 | FROM python:3.9-slim 3 | 4 | # 设置工作目录 5 | WORKDIR /app 6 | 7 | # 设置环境变量 8 | ENV PYTHONDONTWRITEBYTECODE=1 \ 9 | PYTHONUNBUFFERED=1 \ 10 | FLASK_APP=run.py \ 11 | FLASK_ENV=production 12 | 13 | # 安装系统依赖 14 | RUN apt-get update \ 15 | && apt-get install -y --no-install-recommends \ 16 | build-essential \ 17 | curl \ 18 | && rm -rf /var/lib/apt/lists/* 19 | 20 | # 复制项目文件 21 | COPY requirements.txt . 22 | COPY .env .env 23 | COPY app app/ 24 | COPY config.py . 25 | COPY run.py . 26 | 27 | # 安装Python依赖 28 | RUN pip install --no-cache-dir -r requirements.txt 29 | 30 | # 暴露端口 31 | EXPOSE 5000 32 | 33 | # 启动命令 34 | CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "run:app"] -------------------------------------------------------------------------------- /app/static/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import timedelta 3 | 4 | class Config: 5 | # 基本配置 6 | SECRET_KEY = os.environ.get('SECRET_KEY') or os.urandom(24) 7 | 8 | # Redis配置 9 | REDIS_HOST = os.environ.get('REDIS_HOST') or 'localhost' 10 | REDIS_PORT = int(os.environ.get('REDIS_PORT') or 6379) 11 | REDIS_DB = int(os.environ.get('REDIS_DB') or 0) 12 | 13 | # 加密配置 14 | ENCRYPTION_KEY = os.environ.get('ENCRYPTION_KEY') or 'your-encryption-key-here' 15 | 16 | # 剪贴板配置 17 | MAX_CONTENT_LENGTH = 50 * 1024 * 1024 # 50MB 18 | DEFAULT_EXPIRATION = int(timedelta(days=7).total_seconds()) 19 | MAX_EXPIRATION = int(timedelta(days=30).total_seconds()) 20 | 21 | # CSRF配置 22 | WTF_CSRF_ENABLED = True 23 | WTF_CSRF_CHECK_DEFAULT = False # 只在表单提交时检查CSRF -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import timedelta 3 | 4 | class Config: 5 | # 基本配置 6 | SECRET_KEY = os.environ.get('SECRET_KEY', 'dev') 7 | 8 | # CSRF配置 9 | WTF_CSRF_ENABLED = True 10 | WTF_CSRF_SECRET_KEY = os.environ.get('WTF_CSRF_SECRET_KEY', 'csrf-key') 11 | WTF_CSRF_TIME_LIMIT = 3600 # CSRF token有效期:1小时 12 | 13 | # 粘贴内容配置 14 | MAX_CONTENT_LENGTH = 50 * 1024 * 1024 # 最大50MB 15 | ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'doc', 'docx'} 16 | 17 | # 加密和安全配置 18 | ENCRYPTION_KEY = os.environ.get('ENCRYPTION_KEY', 'your-encryption-key') 19 | ENABLE_ENCRYPTION = True 20 | 21 | # 过期时间配置(秒) 22 | DEFAULT_EXPIRATION = 7 * 24 * 60 * 60 # 7天 23 | MAX_EXPIRATION = 30 * 24 * 60 * 60 # 30天 24 | 25 | # 访问控制 26 | MAX_VIEWS = 100 # 最大访问次数 27 | ENABLE_PASSWORD_PROTECTION = True 28 | 29 | class DevelopmentConfig(Config): 30 | DEBUG = True 31 | 32 | class ProductionConfig(Config): 33 | DEBUG = False -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | web: 5 | build: . 6 | container_name: netcut-web 7 | restart: always 8 | ports: 9 | - "5000:5000" 10 | environment: 11 | - REDIS_HOST=redis 12 | - FLASK_ENV=production 13 | depends_on: 14 | - redis 15 | networks: 16 | - netcut-network 17 | 18 | redis: 19 | image: redis:6-alpine 20 | container_name: netcut-redis 21 | restart: always 22 | command: redis-server --appendonly yes 23 | volumes: 24 | - redis-data:/data 25 | networks: 26 | - netcut-network 27 | 28 | nginx: 29 | image: nginx:alpine 30 | container_name: netcut-nginx 31 | restart: always 32 | ports: 33 | - "80:80" 34 | - "443:443" 35 | volumes: 36 | - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro 37 | - ./ssl:/etc/nginx/ssl:ro 38 | depends_on: 39 | - web 40 | networks: 41 | - netcut-network 42 | 43 | networks: 44 | netcut-network: 45 | driver: bridge 46 | 47 | volumes: 48 | redis-data: -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 m0re 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | 5 | # 重定向HTTP到HTTPS 6 | return 301 https://$host$request_uri; 7 | } 8 | 9 | server { 10 | listen 443 ssl; 11 | server_name localhost; 12 | 13 | # SSL配置 14 | ssl_certificate /etc/nginx/ssl/cert.pem; 15 | ssl_certificate_key /etc/nginx/ssl/key.pem; 16 | ssl_protocols TLSv1.2 TLSv1.3; 17 | ssl_ciphers HIGH:!aNULL:!MD5; 18 | 19 | # 安全headers 20 | add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; 21 | add_header X-Frame-Options "SAMEORIGIN" always; 22 | add_header X-XSS-Protection "1; mode=block" always; 23 | add_header X-Content-Type-Options "nosniff" always; 24 | add_header Referrer-Policy "strict-origin-when-cross-origin" always; 25 | 26 | # 客户端缓存设置 27 | location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg)$ { 28 | proxy_pass http://web:5000; 29 | expires 30d; 30 | add_header Cache-Control "public, no-transform"; 31 | } 32 | 33 | # 主应用代理 34 | location / { 35 | proxy_pass http://web:5000; 36 | proxy_set_header Host $host; 37 | proxy_set_header X-Real-IP $remote_addr; 38 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 39 | proxy_set_header X-Forwarded-Proto $scheme; 40 | 41 | # WebSocket支持 42 | proxy_http_version 1.1; 43 | proxy_set_header Upgrade $http_upgrade; 44 | proxy_set_header Connection "upgrade"; 45 | 46 | # 超时设置 47 | proxy_connect_timeout 60s; 48 | proxy_send_timeout 60s; 49 | proxy_read_timeout 60s; 50 | } 51 | 52 | # 错误页面 53 | error_page 404 /404.html; 54 | error_page 500 502 503 504 /50x.html; 55 | } -------------------------------------------------------------------------------- /app/static/css/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-color: #3498db; 3 | --secondary-color: #2ecc71; 4 | --background-color: #ffffff; 5 | --text-color: #2c3e50; 6 | --border-color: #e0e0e0; 7 | } 8 | 9 | /* 深色主题 */ 10 | [data-theme="dark"] { 11 | --primary-color: #3498db; 12 | --secondary-color: #2ecc71; 13 | --background-color: #1a1a1a; 14 | --text-color: #ecf0f1; 15 | --border-color: #2c3e50; 16 | } 17 | 18 | body { 19 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; 20 | line-height: 1.6; 21 | color: var(--text-color); 22 | background-color: var(--background-color); 23 | } 24 | 25 | .container { 26 | max-width: 1200px; 27 | margin: 0 auto; 28 | padding: 2rem; 29 | } 30 | 31 | /* 表单样式 */ 32 | .create-form { 33 | background: var(--background-color); 34 | border-radius: 8px; 35 | padding: 2rem; 36 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 37 | } 38 | 39 | textarea { 40 | width: 100%; 41 | min-height: 300px; 42 | padding: 1rem; 43 | border: 1px solid var(--border-color); 44 | border-radius: 4px; 45 | resize: vertical; 46 | } 47 | 48 | .form-options { 49 | display: grid; 50 | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 51 | gap: 1rem; 52 | margin: 1rem 0; 53 | } 54 | 55 | .option-group { 56 | margin-bottom: 1rem; 57 | } 58 | 59 | /* 按钮样式 */ 60 | .btn-primary { 61 | background-color: var(--primary-color); 62 | color: white; 63 | border: none; 64 | padding: 0.8rem 1.5rem; 65 | border-radius: 4px; 66 | cursor: pointer; 67 | transition: background-color 0.3s; 68 | } 69 | 70 | .btn-primary:hover { 71 | background-color: #2980b9; 72 | } 73 | 74 | /* 查看页面样式 */ 75 | .paste-header { 76 | display: flex; 77 | justify-content: space-between; 78 | align-items: center; 79 | margin-bottom: 1rem; 80 | padding-bottom: 1rem; 81 | border-bottom: 1px solid var(--border-color); 82 | } 83 | 84 | .paste-content { 85 | background: var(--background-color); 86 | padding: 1rem; 87 | border-radius: 4px; 88 | border: 1px solid var(--border-color); 89 | overflow-x: auto; 90 | } 91 | 92 | .burn-notice { 93 | margin-top: 1rem; 94 | padding: 1rem; 95 | background-color: #ff7675; 96 | color: white; 97 | border-radius: 4px; 98 | text-align: center; 99 | } -------------------------------------------------------------------------------- /app/static/js/main.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function() { 2 | // 主题切换 3 | const themeToggle = document.querySelector('.theme-toggle'); 4 | if (themeToggle) { 5 | themeToggle.addEventListener('click', function(e) { 6 | e.preventDefault(); 7 | const currentTheme = document.documentElement.getAttribute('data-theme'); 8 | const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; 9 | document.documentElement.setAttribute('data-theme', newTheme); 10 | localStorage.setItem('theme', newTheme); 11 | }); 12 | } 13 | 14 | // 表单提交 15 | const pasteForm = document.getElementById('pasteForm'); 16 | if (pasteForm) { 17 | pasteForm.addEventListener('submit', async function(e) { 18 | e.preventDefault(); 19 | 20 | const formData = { 21 | content: document.getElementById('content').value, 22 | expiration: parseInt(document.getElementById('expiration').value), 23 | syntax: document.getElementById('syntax').value, 24 | password: document.getElementById('password').value, 25 | burn_after_read: document.getElementById('burnAfterRead').checked 26 | }; 27 | 28 | try { 29 | const response = await fetch('/api/paste', { 30 | method: 'POST', 31 | headers: { 32 | 'Content-Type': 'application/json' 33 | }, 34 | body: JSON.stringify(formData) 35 | }); 36 | 37 | const data = await response.json(); 38 | 39 | if (response.ok) { 40 | // 跳转到查看页面 41 | window.location.href = `/paste/${data.id}`; 42 | } else { 43 | alert(data.error || '创建失败,请重试'); 44 | } 45 | } catch (error) { 46 | console.error('Error:', error); 47 | alert('发生错误,请重试'); 48 | } 49 | }); 50 | } 51 | 52 | // 复制按钮 53 | const copyButton = document.querySelector('.btn-copy'); 54 | if (copyButton) { 55 | copyButton.addEventListener('click', function() { 56 | const content = document.querySelector('.paste-content pre').textContent; 57 | navigator.clipboard.writeText(content).then(function() { 58 | copyButton.textContent = '已复制!'; 59 | setTimeout(() => { 60 | copyButton.textContent = '复制内容'; 61 | }, 2000); 62 | }); 63 | }); 64 | } 65 | }); -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, jsonify 2 | from flask_cors import CORS 3 | from flask_wtf.csrf import CSRFProtect 4 | from config import Config 5 | from .api.paste_routes import bp as paste_bp 6 | from .api.page_routes import bp as page_bp 7 | 8 | # 初始化 CSRF 保护 9 | csrf = CSRFProtect() 10 | 11 | def create_app(config_class=Config): 12 | """应用工厂函数""" 13 | app = Flask(__name__) 14 | CORS(app) 15 | 16 | # 加载配置 17 | app.config.from_object(config_class) 18 | 19 | # 开启调试模式 20 | app.config['DEBUG'] = True 21 | 22 | # 初始化 CSRF 保护 23 | csrf.init_app(app) 24 | 25 | # 初始化 Redis 或使用内存存储 26 | try: 27 | import redis 28 | app.db = redis.Redis( 29 | host=app.config.get('REDIS_HOST', 'localhost'), 30 | port=app.config.get('REDIS_PORT', 6379), 31 | db=app.config.get('REDIS_DB', 0), 32 | decode_responses=True, 33 | socket_connect_timeout=1, 34 | socket_timeout=1 35 | ) 36 | app.db.ping() 37 | print("成功连接到 Redis 服务器") 38 | except Exception as e: 39 | print(f"无法连接到 Redis({str(e)}),使用内存存储作为临时数据库") 40 | class DictDB: 41 | def __init__(self): 42 | self.data = {} 43 | print("初始化内存数据库") 44 | 45 | def get(self, key): 46 | value = self.data.get(key) 47 | print(f"获取键 {key}: {value}") 48 | return value 49 | 50 | def set(self, key, value, ex=None): 51 | print(f"设置键 {key}: {value}") 52 | self.data[key] = value 53 | 54 | def delete(self, key): 55 | print(f"删除键 {key}") 56 | self.data.pop(key, None) 57 | 58 | def expire(self, key, time): 59 | print(f"设置过期时间 {key}: {time}") 60 | pass 61 | 62 | def ping(self): 63 | return True 64 | app.db = DictDB() 65 | 66 | # 注册蓝图 67 | app.register_blueprint(paste_bp) 68 | app.register_blueprint(page_bp) # 注册页面路由蓝图 69 | 70 | # 注册错误处理 71 | @app.errorhandler(404) 72 | def not_found_error(error): 73 | return render_template('error.html', error='页面未找到'), 404 74 | 75 | @app.errorhandler(500) 76 | def internal_error(error): 77 | import traceback 78 | error_info = traceback.format_exc() 79 | print(f"500错误: {error_info}") 80 | return jsonify({ 81 | 'error': '服务器内部错误', 82 | 'details': str(error), 83 | 'traceback': error_info 84 | }), 500 85 | 86 | return app -------------------------------------------------------------------------------- /app/views.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, abort, request, jsonify, redirect, url_for 2 | from .services.paste_service import PasteService 3 | 4 | bp = Blueprint('main', __name__) 5 | paste_service = PasteService() 6 | 7 | @bp.route('/') 8 | def index(): 9 | """渲染首页""" 10 | return render_template('index.html') 11 | 12 | @bp.route('/pages/') 13 | def static_page(page_name): 14 | """处理静态页面""" 15 | try: 16 | return render_template(f'pages/{page_name}.html') 17 | except: 18 | abort(404) 19 | 20 | @bp.route('/', methods=['GET', 'POST']) 21 | def paste(paste_name): 22 | """处理剪贴板的创建和访问""" 23 | # 验证剪贴板名称 24 | if not paste_name.replace('-', '').replace('_', '').isalnum(): 25 | abort(404) 26 | 27 | if request.method == 'POST': 28 | # 处理保存请求 29 | data = request.get_json() 30 | try: 31 | result = paste_service.create_paste( 32 | paste_name=paste_name, 33 | content=data.get('content'), 34 | expiration=data.get('expiration'), 35 | password=data.get('password'), 36 | burn_after_read=data.get('burn_after_read', False), 37 | syntax=data.get('syntax') 38 | ) 39 | return jsonify(result) 40 | except ValueError as e: 41 | return jsonify({'error': str(e)}), 400 42 | else: 43 | # 处理获取请求 44 | try: 45 | paste = paste_service.get_paste(paste_name) 46 | if paste is None: 47 | # 如果剪贴板不存在,返回编辑页面 48 | return render_template('edit.html', paste_name=paste_name) 49 | 50 | # 如果需要密码 51 | if paste.get('has_password') and not paste.get('content'): 52 | return render_template('password.html', paste_name=paste_name) 53 | 54 | # 直接显示编辑页面 55 | return render_template('edit.html', paste_name=paste_name, paste=paste) 56 | 57 | except ValueError as e: 58 | return jsonify({'error': str(e)}), 401 59 | 60 | @bp.route('//raw') 61 | def raw(paste_name): 62 | """获取原始内容""" 63 | try: 64 | paste = paste_service.get_paste(paste_name) 65 | if paste is None: 66 | abort(404) 67 | 68 | if paste.get('has_password'): 69 | password = request.args.get('password') 70 | if not password: 71 | return jsonify({'error': 'Password required'}), 401 72 | 73 | return paste.get('content', ''), 200, { 74 | 'Content-Type': 'text/plain; charset=utf-8' 75 | } 76 | except ValueError as e: 77 | return jsonify({'error': str(e)}), 401 -------------------------------------------------------------------------------- /app/templates/pages/about.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}关于我们 - 在线剪贴板{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |

关于在线剪贴板

9 | 10 |
11 |

我们的使命

12 |

为用户提供一个简单、安全、高效的文本分享平台。无论是代码片段、笔记还是临时文本,都可以轻松地进行分享和存储。

13 |
14 | 15 |
16 |

主要特点

17 |
    18 |
  • 🔒 支持密码保护
  • 19 |
  • ⏰ 自定义过期时间
  • 20 |
  • 🔥 阅后即焚功能
  • 21 |
  • 📱 移动端适配
  • 22 |
  • 🎨 美观的界面设计
  • 23 |
  • ⚡ 快速的响应速度
  • 24 |
25 |
26 | 27 |
28 |

技术栈

29 |
    30 |
  • 后端:Python + Flask
  • 31 |
  • 数据库:Redis
  • 32 |
  • 前端:HTML5 + CSS3 + JavaScript
  • 33 |
  • 部署:Docker + Nginx
  • 34 |
35 |
36 | 37 |
38 |

联系我们

39 |

如果您有任何问题、建议或合作意向,欢迎联系我们:

40 |
    41 |
  • 📧 Email:contact@example.com
  • 42 |
  • 💬 GitHub:项目仓库
  • 43 |
44 |
45 |
46 |
47 | {% endblock %} 48 | 49 | {% block extra_css %} 50 | 133 | {% endblock %} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 在线剪贴板 2 | 3 | > 🎯 本项目由 [Cursor](https://cursor.sh/) - 基于 AI 的新一代 IDE 开发。Cursor 是一个强大的 AI 编程助手,它让编程变得更加智能和高效。 4 | 5 | 一个简单、安全、高效的在线文本分享平台。支持密码保护、阅后即焚、自定义过期时间等功能。 6 | 7 | ![home](/img/home.png) 8 | 9 | ![view](/img/view.png) 10 | 11 | 12 | ## 功能特点 13 | 14 | - 🔒 密码保护:设置访问密码,保护敏感内容 15 | - ⏰ 过期时间:支持1小时、1天、7天、30天的过期设置 16 | - 🔥 阅后即焚:查看一次后自动销毁内容 17 | - 📱 响应式设计:完美支持移动端访问 18 | - ⚡ 快速响应:毫秒级的访问速度 19 | - 🎨 美观界面:现代化的UI设计 20 | 21 | ## 技术栈 22 | 23 | - 后端:Python + Flask 24 | - 数据库:Redis 25 | - 前端:HTML5 + CSS3 + JavaScript 26 | - 部署:Docker + Nginx 27 | 28 | ## 项目架构 29 | ``` 30 | netcut/ 31 | ├── app/ # 应用主目录 32 | │ ├── api/ # API路由 33 | │ │ ├── page_routes.py # 页面路由 34 | │ │ └── paste_routes.py # 剪贴板相关路由 35 | │ ├── services/ # 业务逻辑层 36 | │ │ └── paste_service.py # 剪贴板服务 37 | │ ├── static/ # 静态资源 38 | │ │ ├── images/ # 图片资源 39 | │ │ │ ├── favicon.svg # 网站图标 40 | │ │ │ └── logo.svg # 网站Logo 41 | │ ├── templates/ # 模板文件 42 | │ │ ├── pages/ # 静态页面模板 43 | │ │ │ ├── about.html # 关于页面 44 | │ │ │ ├── help.html # 帮助页面 45 | │ │ │ ├── terms.html # 服务条款 46 | │ │ │ └── privacy.html # 隐私政策 47 | │ │ ├── base.html # 基础模板 48 | │ │ ├── edit.html # 编辑页面 49 | │ │ ├── error.html # 错误页面 50 | │ │ ├── index.html # 首页 51 | │ │ └── password.html # 密码验证页面 52 | │ ├── utils/ # 工具函数 53 | │ │ ├── encryption.py # 加密工具 54 | │ │ └── password.py # 密码处理 55 | │ ├── extensions.py # Flask扩展 56 | │ └── __init__.py # 应用初始化 57 | ├── docker/ # Docker相关文件 58 | ├── ssl/ # SSL证书目录 59 | ├── .env # 环境变量 60 | ├── .env.example # 环境变量示例 61 | ├── config.py # 配置文件 62 | ├── docker-compose.yml # Docker编排配置 63 | ├── Dockerfile # Docker构建文件 64 | ├── nginx.conf # Nginx配置 65 | ├── requirements.txt # Python依赖 66 | ├── run.py # 应用入口 67 | └── README.md # 项目说明 68 | ``` 69 | 70 | ## 快速开始 71 | 72 | ### 环境要求 73 | 74 | - Docker 75 | - Docker Compose 76 | - Redis 6.0+ (使用 Docker 部署时会自动安装) 77 | - Python 3.9+ (本地开发) 78 | 79 | > 💡 注意:如果使用 Docker 部署,Redis 会自动在容器中安装。如果是本地开发,需要自行安装 Redis 服务器。 80 | 81 | ### 部署步骤(推荐使用Docker) 82 | 83 | 1. 克隆项目 84 | ```bash 85 | git clone https://github.com/your-username/netcut.git 86 | cd netcut 87 | ``` 88 | 89 | 2. 配置环境变量 90 | ```bash 91 | # 复制环境变量示例文件 92 | cp .env.example .env 93 | 94 | # 编辑.env文件,修改必要的配置项 95 | # 特别是 SECRET_KEY 和 ENCRYPTION_KEY 96 | ``` 97 | 98 | 3. 配置SSL证书 99 | ```bash 100 | # 创建ssl目录 101 | mkdir ssl 102 | 103 | # 将SSL证书和私钥放入ssl目录 104 | # - cert.pem:SSL证书 105 | # - key.pem:私钥 106 | ``` 107 | 108 | 4. 启动服务 109 | ```bash 110 | # 构建并启动所有服务 111 | docker-compose up -d 112 | 113 | # 查看服务状态 114 | docker-compose ps 115 | 116 | # 查看日志 117 | docker-compose logs -f 118 | ``` 119 | 120 | ### 本地开发 121 | 122 | 1. 安装 Redis 123 | ```bash 124 | # Ubuntu/Debian 125 | sudo apt-get update 126 | sudo apt-get install redis-server 127 | 128 | # CentOS/RHEL 129 | sudo yum install redis 130 | 131 | # macOS 132 | brew install redis 133 | 134 | # Windows 135 | # 从 https://github.com/microsoftarchive/redis/releases 下载安装包 136 | ``` 137 | 138 | 2. 创建虚拟环境 139 | ```bash 140 | python -m venv venv 141 | source venv/bin/activate # Linux/Mac 142 | venv\Scripts\activate # Windows 143 | ``` 144 | 145 | 3. 安装依赖 146 | ```bash 147 | pip install -r requirements.txt 148 | ``` 149 | 150 | 4. 配置 Redis 151 | ```bash 152 | # 启动 Redis 服务 153 | # Linux/macOS 154 | sudo service redis start # 或 sudo systemctl start redis 155 | 156 | # Windows 157 | redis-server 158 | 159 | # 验证 Redis 是否正常运行 160 | redis-cli ping # 应该返回 PONG 161 | ``` 162 | 163 | 5. 启动开发服务器 164 | ```bash 165 | flask run 166 | ``` 167 | 168 | ## 使用说明 169 | 170 | 1. 创建剪贴板 171 | - 访问首页 172 | - 输入或粘贴文本内容 173 | - 可选择设置密码保护、过期时间、阅后即焚 174 | - 点击"保存内容"按钮 175 | 176 | 2. 分享内容 177 | - 复制生成的链接 178 | - 如果设置了密码,需要同时分享密码 179 | - 接收者通过链接访问内容 180 | 181 | 3. 安全提示 182 | - 重要内容建议使用密码保护 183 | - 敏感信息推荐使用阅后即焚 184 | - 定期清理不再需要的内容 185 | 186 | ## 配置说明 187 | 188 | ### 环境变量 189 | 190 | - `SECRET_KEY`:Flask密钥 191 | - `FLASK_ENV`:运行环境(development/production) 192 | - `REDIS_HOST`:Redis服务器地址 193 | - `REDIS_PORT`:Redis端口 194 | - `ENCRYPTION_KEY`:内容加密密钥 195 | 196 | ### Docker配置 197 | 198 | - Web服务:运行Flask应用,端口5000 199 | - Redis:数据存储,持久化配置 200 | - Nginx:反向代理,SSL终端 201 | 202 | ## 常见问题 203 | 204 | 1. Q: 内容会永久保存吗? 205 | A: 不会,所有内容都有过期时间,最长30天。 206 | 207 | 2. Q: 忘记密码怎么办? 208 | A: 密码无法找回,建议重新创建剪贴板。 209 | 210 | 3. Q: 支持哪些内容格式? 211 | A: 支持纯文本内容,包括代码、链接等。 212 | 213 | ## 关于 214 | 关于开发大致过程,记录在我的公众号中 215 | [我用AI全栈开发了一个漂亮的在线剪贴板](https://mp.weixin.qq.com/s/VCZ9JnePDKWwgnUMdVn9Mw) 216 | 欢迎关注我的公众号哦 217 | 218 | ![wechat](./img/wechat-github-ai.png) 219 | -------------------------------------------------------------------------------- /app/templates/pages/help.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}使用帮助 - 在线剪贴板{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |

使用帮助

9 | 10 |
11 |

基本使用

12 |
13 |

1. 创建剪贴板

14 |
    15 |
  • 访问首页,直接在文本框中输入或粘贴内容
  • 16 |
  • 点击"保存内容"按钮创建剪贴板
  • 17 |
  • 系统会自动生成一个唯一的链接
  • 18 |
19 |
20 | 21 |
22 |

2. 分享链接

23 |
    24 |
  • 复制生成的链接分享给他人
  • 25 |
  • 接收者可以通过链接直接访问内容
  • 26 |
27 |
28 |
29 | 30 |
31 |

高级功能

32 |
33 |

1. 密码保护

34 |
    35 |
  • 创建剪贴板时设置访问密码
  • 36 |
  • 分享链接时需要同时告知密码
  • 37 |
  • 访问者需要输入正确密码才能查看内容
  • 38 |
39 |
40 | 41 |
42 |

2. 过期时间

43 |
    44 |
  • 可选择内容的自动过期时间
  • 45 |
  • 支持1小时、1天、7天、30天
  • 46 |
  • 过期后内容将自动删除
  • 47 |
48 |
49 | 50 |
51 |

3. 阅后即焚

52 |
    53 |
  • 创建时勾选"阅后即焚"选项
  • 54 |
  • 内容被查看后将立即删除
  • 55 |
  • 适合一次性的敏感信息传递
  • 56 |
57 |
58 |
59 | 60 |
61 |

常见问题

62 |
63 |

Q: 内容会永久保存吗?

64 |

A: 不会。所有内容都有过期时间,最长30天。建议及时保存重要内容。

65 |
66 | 67 |
68 |

Q: 忘记密码怎么办?

69 |

A: 密码无法找回。建议重新创建剪贴板并妥善保管密码。

70 |
71 | 72 |
73 |

Q: 支持哪些内容格式?

74 |

A: 支持纯文本内容,包括代码、链接等。暂不支持富文本格式。

75 |
76 |
77 | 78 |
79 |

联系支持

80 |

如果您遇到其他问题或需要帮助,请通过以下方式联系我们:

81 |
    82 |
  • 📧 发送邮件至:support@example.com
  • 83 |
  • 💬 在 GitHub 上提交 Issue
  • 84 |
  • 📝 查看我们的服务条款隐私政策
  • 85 |
86 |
87 |
88 |
89 | {% endblock %} 90 | 91 | {% block extra_css %} 92 | 202 | {% endblock %} -------------------------------------------------------------------------------- /app/api/paste_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, request, jsonify, abort, current_app, session 2 | from ..services.paste_service import PasteService 3 | 4 | bp = Blueprint('paste', __name__) 5 | 6 | @bp.route('/') 7 | def index(): 8 | """渲染首页""" 9 | return render_template('index.html') 10 | 11 | @bp.route('/', methods=['GET', 'POST']) 12 | def paste(paste_name): 13 | """处理剪贴板的创建和访问""" 14 | try: 15 | # 验证剪贴板名称 16 | if not paste_name.replace('-', '').replace('_', '').isalnum(): 17 | abort(404) 18 | 19 | paste_service = PasteService() 20 | 21 | if request.method == 'POST': 22 | # 处理保存请求 23 | data = request.get_json() 24 | if not data: 25 | return jsonify({'error': '无效的请求数据'}), 400 26 | 27 | try: 28 | print(f"创建剪贴板: {paste_name}") 29 | print(f"请求数据: {data}") 30 | result = paste_service.create_paste( 31 | paste_name=paste_name, 32 | content=data.get('content'), 33 | expiration=data.get('expiration'), 34 | password=data.get('password'), 35 | burn_after_read=data.get('burn_after_read', False) 36 | ) 37 | print(f"创建结果: {result}") 38 | return jsonify(result) 39 | except ValueError as e: 40 | print(f"创建失败: {str(e)}") 41 | return jsonify({'error': str(e)}), 400 42 | else: 43 | # 处理获取请求 44 | try: 45 | print(f"获取剪贴板: {paste_name}") 46 | # 检查是否已通过密码验证 47 | is_authenticated = session.get(f'paste_auth_{paste_name}', False) 48 | 49 | # 尝试获取剪贴板内容 50 | paste = paste_service.get_paste(paste_name, None if not is_authenticated else '') 51 | 52 | if paste is None: 53 | print("剪贴板不存在,返回编辑页面") 54 | return render_template('edit.html', paste_name=paste_name) 55 | 56 | if paste.get('has_password'): 57 | if not is_authenticated: 58 | print("需要密码访问") 59 | return render_template('password.html', paste_name=paste_name) 60 | else: 61 | # 从会话中获取已解密的内容 62 | paste['content'] = session.get(f'paste_content_{paste_name}') 63 | 64 | print("返回编辑页面") 65 | return render_template('edit.html', paste_name=paste_name, paste=paste) 66 | 67 | except ValueError as e: 68 | print(f"获取失败: {str(e)}") 69 | return jsonify({'error': str(e)}), 401 70 | except Exception as e: 71 | import traceback 72 | error_info = traceback.format_exc() 73 | print(f"处理请求时出错: {error_info}") 74 | return jsonify({ 75 | 'error': '服务器内部错误', 76 | 'details': str(e), 77 | 'traceback': error_info 78 | }), 500 79 | 80 | @bp.route('//raw') 81 | def raw(paste_name): 82 | """获取原始内容""" 83 | paste_service = PasteService() 84 | try: 85 | paste = paste_service.get_paste(paste_name) 86 | if paste is None: 87 | abort(404) 88 | 89 | if paste.get('has_password'): 90 | password = request.args.get('password') 91 | if not password: 92 | return jsonify({'error': 'Password required'}), 401 93 | 94 | return paste.get('content', ''), 200, { 95 | 'Content-Type': 'text/plain; charset=utf-8' 96 | } 97 | except ValueError as e: 98 | return jsonify({'error': str(e)}), 401 99 | 100 | @bp.route('//verify', methods=['POST']) 101 | def verify_password(paste_name): 102 | """验证剪贴板密码""" 103 | try: 104 | print(f"收到密码验证请求: {paste_name}") 105 | data = request.get_json() 106 | print(f"请求数据: {data}") 107 | 108 | if not data or 'password' not in data: 109 | print("未提供密码") 110 | return jsonify({'error': '请提供密码'}), 400 111 | 112 | paste_service = PasteService() 113 | print("验证密码...") 114 | 115 | # 先验证密码 116 | if not paste_service.verify_paste_password(paste_name, data['password']): 117 | print("密码验证失败") 118 | return jsonify({'error': '密码错误'}), 401 119 | 120 | print("密码验证成功") 121 | # 获取完整的剪贴板内容 122 | paste = paste_service.get_paste(paste_name, data['password']) 123 | if not paste: 124 | return jsonify({'error': '剪贴板不存在'}), 404 125 | 126 | # 设置会话标记,表示已通过密码验证 127 | session[f'paste_auth_{paste_name}'] = True 128 | session[f'paste_content_{paste_name}'] = paste.get('content') 129 | 130 | return jsonify({ 131 | 'status': 'success', 132 | 'content': paste.get('content'), 133 | 'name': paste.get('name'), 134 | 'created_at': paste.get('created_at'), 135 | 'expires_at': paste.get('expires_at'), 136 | 'burn_after_read': paste.get('burn_after_read'), 137 | 'views': paste.get('views') 138 | }) 139 | 140 | except Exception as e: 141 | print(f"验证过程出错: {str(e)}") 142 | return jsonify({'error': str(e)}), 500 -------------------------------------------------------------------------------- /app/templates/pages/terms.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}服务条款 - 在线剪贴板{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |

服务条款

10 |

使用我们的服务即表示您同意以下条款

11 |

最后更新:2025年1月6日

12 |
13 |
14 | 15 |
16 |
17 |
18 |

1. 服务说明

19 |

在线剪贴板提供文本分享服务,允许用户创建、分享和访问文本内容。我们保留随时修改、暂停或终止服务的权利。

20 |
21 | 22 |
23 |

2. 用户责任

24 |

使用本服务时,您同意:

25 |
    26 |
  • 不发布任何违法、有害或侵犯他人权利的内容
  • 27 |
  • 不使用服务进行垃圾信息传播或骚扰行为
  • 28 |
  • 不尝试破坏或干扰服务的正常运行
  • 29 |
  • 对您创建和分享的内容负责
  • 30 |
31 |
32 | 33 |
34 |

3. 内容政策

35 |

禁止发布以下类型的内容:

36 |
    37 |
  • 违反法律法规的内容
  • 38 |
  • 侵犯知识产权的内容
  • 39 |
  • 含有恶意代码的内容
  • 40 |
  • 垃圾广告或营销信息
  • 41 |
42 |
43 | 44 |
45 |

4. 服务限制

46 |

我们保留以下权利:

47 |
    48 |
  • 限制单个用户的使用频率
  • 49 |
  • 删除违规内容
  • 50 |
  • 封禁违规用户
  • 51 |
  • 修改服务功能和限制
  • 52 |
53 |
54 | 55 |
56 |

5. 免责声明

57 |

我们不对以下情况负责:

58 |
    59 |
  • 用户发布的内容
  • 60 |
  • 服务中断或数据丢失
  • 61 |
  • 第三方链接或内容
  • 62 |
  • 因使用服务造成的损失
  • 63 |
64 |
65 | 66 |
67 |

6. 知识产权

68 |

服务相关的所有知识产权归我们所有。用户保留其发布内容的权利,但授予我们使用和展示的许可。

69 |
70 | 71 |
72 |

7. 条款变更

73 |

我们可能随时修改这些条款。重大变更将通过网站公告通知用户。继续使用服务即表示接受新条款。

74 |
75 | 76 |
77 |

8. 联系我们

78 |

如果您对这些条款有任何疑问,请联系我们:

79 |
80 |

📧 邮箱:xxxxx@gmail.com

81 |

📞 电话:400-xxx-xxxx

82 |
83 |
84 |
85 |
86 |
87 | {% endblock %} 88 | 89 | {% block extra_css %} 90 | 210 | {% endblock %} -------------------------------------------------------------------------------- /app/services/paste_service.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Dict, Any 2 | from datetime import datetime, timedelta 3 | from ..utils.encryption import encrypt_content, decrypt_content 4 | from ..utils.password import hash_password, verify_password 5 | from flask import current_app 6 | import json 7 | 8 | class PasteService: 9 | @property 10 | def db(self): 11 | """延迟获取数据库实例""" 12 | return current_app.db 13 | 14 | def create_paste(self, paste_name, content, expiration=604800, password=None, burn_after_read=False): 15 | """创建或更新剪贴板""" 16 | # 验证输入 17 | if not content: 18 | raise ValueError('内容不能为空') 19 | 20 | if not paste_name: 21 | raise ValueError('剪贴板名称不能为空') 22 | 23 | # 计算过期时间 24 | expires_at = datetime.now() + timedelta(seconds=expiration) 25 | 26 | try: 27 | # 加密内容 28 | encrypted_content = encrypt_content(content) 29 | if encrypted_content is None: 30 | raise ValueError('内容加密失败') 31 | 32 | # 如果有密码,记录状态并存储哈希密码 33 | has_password = bool(password) 34 | password_hash = hash_password(password) if password else None 35 | 36 | # 准备数据 37 | paste_data = { 38 | 'name': paste_name, 39 | 'content': None, # 明文内容始终为空 40 | 'encrypted_content': encrypted_content, 41 | 'has_password': has_password, 42 | 'password_hash': password_hash, # 存储密码哈希 43 | 'burn_after_read': burn_after_read, 44 | 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 45 | 'expires_at': expires_at.strftime('%Y-%m-%d %H:%M:%S'), 46 | 'views': 0 47 | } 48 | 49 | # 保存到数据库 50 | self.db.set(f'paste:{paste_name}', json.dumps(paste_data)) 51 | if expiration: 52 | self.db.expire(f'paste:{paste_name}', expiration) 53 | 54 | return {'status': 'success'} 55 | 56 | except Exception as e: 57 | raise ValueError(f'创建剪贴板失败: {str(e)}') 58 | 59 | def verify_paste_password(self, paste_name: str, password: str) -> bool: 60 | """验证剪贴板密码""" 61 | try: 62 | paste_data = self.db.get(f'paste:{paste_name}') 63 | if not paste_data: 64 | return False 65 | 66 | paste = json.loads(paste_data) 67 | if not paste.get('has_password'): 68 | return True 69 | 70 | stored_password_hash = paste.get('password_hash') 71 | return bool(stored_password_hash and verify_password(password, stored_password_hash)) 72 | 73 | except Exception: 74 | return False 75 | 76 | def get_paste(self, paste_name, password=None): 77 | """获取剪贴板内容""" 78 | try: 79 | # 从数据库获取数据 80 | paste_data = self.db.get(f'paste:{paste_name}') 81 | if not paste_data: 82 | return None 83 | 84 | paste = json.loads(paste_data) 85 | 86 | # 处理阅后即焚 87 | if paste.get('burn_after_read'): 88 | self.db.delete(f'paste:{paste_name}') 89 | 90 | # 更新访问次数 91 | paste['views'] += 1 92 | self.db.set(f'paste:{paste_name}', json.dumps(paste)) 93 | 94 | # 解密内容 95 | encrypted_content = paste.get('encrypted_content') 96 | if encrypted_content: 97 | try: 98 | # 如果有密码保护,先验证密码 99 | if paste.get('has_password'): 100 | if not password: 101 | # 如果需要密码但没有提供,返回提示 102 | return { 103 | 'name': paste_name, 104 | 'has_password': True, 105 | 'created_at': paste.get('created_at'), 106 | 'expires_at': paste.get('expires_at'), 107 | 'views': paste.get('views', 0) 108 | } 109 | 110 | # 验证密码 111 | if not self.verify_paste_password(paste_name, password): 112 | raise ValueError('密码错误') 113 | 114 | # 解密内容 115 | content = decrypt_content(encrypted_content) 116 | if content is None: 117 | raise ValueError('内容解密失败') 118 | 119 | paste['content'] = content 120 | 121 | except Exception as e: 122 | raise ValueError(f'解密失败: {str(e)}') 123 | 124 | return paste 125 | 126 | except Exception as e: 127 | raise ValueError(f'获取剪贴板失败: {str(e)}') 128 | 129 | def _format_paste_response(self, paste: Dict[str, Any]) -> Dict[str, Any]: 130 | """格式化响应数据""" 131 | return { 132 | 'name': paste['name'], 133 | 'created_at': paste['created_at'], 134 | 'expires_at': paste['expires_at'], 135 | 'burn_after_read': paste['burn_after_read'], 136 | 'views': paste['views'], 137 | 'has_password': paste.get('has_password', False), 138 | 'content': paste.get('content') 139 | } -------------------------------------------------------------------------------- /app/templates/error.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ error }} - 在线剪贴板{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 | {% if error == '页面未找到' %} 10 |
11 |
4
12 |
13 |
👨‍🚀
14 |
15 |
4
16 |
17 | {% else %} 18 |
19 |
⚙️
20 |
500
21 |
22 | {% endif %} 23 |
24 | 25 |
26 |

{{ error }}

27 | 28 | {% if error == '页面未找到' %} 29 |

抱歉,您访问的页面似乎漂流到了外太空 🚀

30 |
31 |

您可以尝试:

32 |
    33 |
  • 检查输入的网址是否正确
  • 34 |
  • 返回首页创建新的剪贴板
  • 35 |
  • 联系我们寻求帮助
  • 36 |
37 |
38 | {% elif error == '服务器内部错误' %} 39 |

抱歉,服务器遇到了一些小问题 🔧

40 |
41 |

请稍后再试,或者您可以:

42 |
    43 |
  • 刷新页面重试
  • 44 |
  • 返回首页
  • 45 |
  • 联系我们报告问题
  • 46 |
47 |
48 | {% else %} 49 |

发生了一些错误,请重试 🤔

50 | {% endif %} 51 | 52 |
53 | 返回首页 54 | 联系我们 55 |
56 |
57 |
58 |
59 | {% endblock %} 60 | 61 | {% block extra_css %} 62 | 216 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/pages/privacy.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}隐私政策 - 在线剪贴板{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |

隐私政策

10 |

我们如何保护您的隐私和数据安全

11 |

最后更新:2025年1月6日

12 |
13 |
14 | 15 |
16 |
17 |
18 |

概述

19 |

在线剪贴板(以下简称"我们")非常重视用户的隐私和个人信息保护。本隐私政策旨在帮助您了解我们如何收集、使用和保护您的个人信息。使用我们的服务即表示您同意本隐私政策的内容。

20 |
21 | 22 |
23 |

信息收集

24 |

我们收集的信息

25 |
    26 |
  • 27 | 用户提供的内容 28 |

    您在使用我们的服务时主动提供的文本内容。

    29 |
  • 30 |
  • 31 | 访问日志 32 |

    包括IP地址、浏览器类型、访问时间等基本访问信息。

    33 |
  • 34 |
  • 35 | Cookie信息 36 |

    用于改善用户体验的必要Cookie信息。

    37 |
  • 38 |
39 |
40 | 41 |
42 |

信息使用

43 |

我们如何使用您的信息

44 |
    45 |
  • 46 | 提供服务 47 |

    使用您的内容提供文本分享服务。

    48 |
  • 49 |
  • 50 | 改善服务 51 |

    分析使用数据以优化和改进我们的服务。

    52 |
  • 53 |
  • 54 | 安全保护 55 |

    防止滥用和确保服务安全。

    56 |
  • 57 |
58 |
59 | 60 |
61 |

数据安全

62 |

我们如何保护您的数据

63 |
    64 |
  • 65 | 加密存储 66 |

    所有用户内容都经过加密存储。

    67 |
  • 68 |
  • 69 | 安全传输 70 |

    使用HTTPS协议确保数据传输安全。

    71 |
  • 72 |
  • 73 | 访问控制 74 |

    严格的内部访问控制机制。

    75 |
  • 76 |
77 |
78 | 79 |
80 |

数据保留

81 |

内容保留政策

82 |
    83 |
  • 84 | 永久内容 85 |

    默认情况下永久保存,除非您设置了过期时间。

    86 |
  • 87 |
  • 88 | 临时内容 89 |

    根据您设置的过期时间自动删除。

    90 |
  • 91 |
  • 92 | 阅后即焚 93 |

    查看后立即删除的内容不会保留。

    94 |
  • 95 |
96 |
97 | 98 |
99 |

用户权利

100 |

您的权利

101 |
    102 |
  • 103 | 访问权 104 |

    您有权访问您创建的所有内容。

    105 |
  • 106 |
  • 107 | 修改权 108 |

    您可以修改您创建的内容的设置。

    109 |
  • 110 |
  • 111 | 删除权 112 |

    您可以随时删除您创建的内容。

    113 |
  • 114 |
115 |
116 | 117 |
118 |

Cookie使用

119 |

Cookie政策

120 |
    121 |
  • 122 | 必要Cookie 123 |

    用于维持基本功能的Cookie。

    124 |
  • 125 |
  • 126 | 功能Cookie 127 |

    用于改善用户体验的Cookie。

    128 |
  • 129 |
  • 130 | 统计Cookie 131 |

    用于分析服务使用情况的Cookie。

    132 |
  • 133 |
134 |
135 | 136 |
137 |

政策更新

138 |

我们可能会不时更新本隐私政策。更新后的政策将在本页面上发布,并注明更新日期。重大变更将通过网站公告通知用户。

139 |
140 | 141 |
142 |

联系我们

143 |

如果您对本隐私政策有任何疑问或建议,请通过以下方式联系我们:

144 |
145 |

📧 邮箱:xxxx@gmail.com

146 |

📞 电话:400-xxx-xxxx

147 |
148 |
149 |
150 |
151 |
152 | {% endblock %} 153 | 154 | {% block extra_css %} 155 | 282 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}在线剪贴板 - 简单、安全的文本分享工具{% endblock %} 4 | 5 | {% block extra_css %} 6 | 155 | {% endblock %} 156 | 157 | {% block content %} 158 |
159 |
160 |

简单、安全的文本分享

161 |

快速创建和分享文本内容,支持设置密码保护和阅后即焚,让分享更加安全可靠。

162 | 163 |
164 | 170 | 171 |
172 | 173 |
174 |
175 |
🔒
176 |

密码保护

177 |

为重要内容设置访问密码,确保信息安全。

178 |
179 |
180 |
🔥
181 |

阅后即焚

182 |

内容阅读一次后自动销毁,保护隐私。

183 |
184 |
185 |
186 |

自动过期

187 |

设置内容的有效期,过期后自动清除。

188 |
189 |
190 |
191 |
192 | {% endblock %} 193 | 194 | {% block extra_js %} 195 | 281 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/edit.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ paste.name if paste else paste_name }} - 在线剪贴板{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 | 首页 10 | | 11 | 12 | 剪贴板地址: 13 | {{ paste.name if paste else paste_name }} 14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 |
24 | 27 |
28 | 29 | 60 |
61 | 62 | 63 | {% endblock %} 64 | 65 | {% block extra_css %} 66 | 223 | {% endblock %} 224 | 225 | {% block extra_js %} 226 | 314 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/password.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}需要密码 - {{ paste_name }} - 在线剪贴板{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |
10 |
🔒
11 |
12 |
13 |
14 | 15 |
16 |

此内容已加密

17 |
18 | 剪贴板地址: 19 | {{ paste_name }} 20 |
21 | 22 |

请输入密码以访问此剪贴板内容

23 | 24 |
25 |
26 | 31 | 39 |
40 | 41 | 44 |
45 | 46 |
47 |

🔒 此剪贴板已启用密码保护

48 |

⚠️ 密码错误3次后将被临时锁定

49 |
50 |
51 |
52 |
53 | {% endblock %} 54 | 55 | {% block extra_css %} 56 | 237 | {% endblock %} 238 | 239 | {% block extra_js %} 240 | 318 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/pages/contact.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}联系我们 - 在线剪贴板{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |

联系我们

10 |

我们随时欢迎您的反馈和建议

11 |
12 |
13 | 14 |
15 |
16 |
17 |
18 |

联系方式

19 |
20 |
21 |
📧
22 |

电子邮件

23 |

xxxxx@gmail.com

24 |

工作日24小时内回复

25 |
26 |
27 |
📞
28 |

客服电话

29 |

400-xxx-xxxx

30 |

周一至周五 9:00-18:00

31 |
32 |
33 |
💬
34 |

在线客服

35 |

点击右下角图标

36 |

实时解答您的问题

37 |
38 |
39 |
40 | 41 | 58 |
59 | 60 |
61 |

发送消息

62 |
63 |
64 | 65 | 66 |
67 | 68 |
69 | 70 | 71 |
72 | 73 |
74 | 75 | 82 |
83 | 84 |
85 | 86 | 87 |
88 | 89 | 90 |
91 |
92 |
93 | 94 |
95 |

常见问题

96 |
97 |
98 |

如何获取技术支持?

99 |

您可以通过在线客服、电子邮件或电话获取技术支持。我们的支持团队会在工作时间内为您提供帮助。

100 |
101 |
102 |

响应时间是多久?

103 |

我们通常会在24小时内回复电子邮件。对于紧急问题,建议使用在线客服或电话联系我们。

104 |
105 |
106 |

如何报告问题?

107 |

您可以通过联系表单或发送电子邮件报告问题。请尽可能详细地描述问题,包括复现步骤和错误信息。

108 |
109 |
110 |
111 |
112 |
113 | {% endblock %} 114 | 115 | {% block extra_css %} 116 | 346 | {% endblock %} 347 | 348 | {% block extra_js %} 349 | 362 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {% block title %}在线剪贴板{% endblock %} 8 | 9 | 281 | {% block extra_css %}{% endblock %} 282 | 283 | 284 | 298 | 299 |
300 | {% block content %}{% endblock %} 301 |
302 | 303 | 316 | 317 | 345 | {% block extra_js %}{% endblock %} 346 | 347 | --------------------------------------------------------------------------------