Loading

Python 极简在线记事本:一个基于 Flask 的轻量级笔记服务

# Python 实现的极简网页记事本

这是一个基于 Flask 和 Waitress 实现的轻量级在线记事本,灵感来自 [minimalist-web-notepad](https://github.com/pereorga/minimalist-web-notepad)。

## 特点

- 极简设计,无需登录
- 自动保存
- 支持开机自启
- 基于文件系统存储
- 自动清理过期笔记

## 技术栈

- Python 3.x
- Flask (Web框架)
- Waitress (WSGI服务器)
- JavaScript (前端自动保存)

## 核心代码

### 1. 主应用 (app.py)
```python
from flask import Flask, request, render_template, redirect, url_for, abort
import os
import string
import random
from datetime import datetime, timedelta
import logging
from logging.handlers import RotatingFileHandler
from config import Config
import re

app = Flask(__name__)
app.config.from_object(Config)

# 配置日志
if not os.path.exists('logs'):
    os.mkdir('logs')
file_handler = RotatingFileHandler('logs/minitex.log', maxBytes=10240, backupCount=10)
file_handler.setFormatter(logging.Formatter(
    '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
))
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)
app.logger.info('Minitex startup')

def generate_id(length=5):
    """生成随机记事本ID"""
    chars = string.ascii_lowercase + string.digits
    return ''.join(random.choice(chars) for _ in range(length))

def get_note_path(note_id):
    """获取记事本文件路径"""
    if not re.match(r'^[a-z0-9]+$', note_id):
        abort(400)
    return os.path.join(app.config['DATA_DIR'], note_id + '.txt')

def cleanup_old_notes():
    """清理过期笔记"""
    try:
        cutoff_date = datetime.now() - timedelta(days=app.config['NOTE_LIFETIME_DAYS'])
        for filename in os.listdir(app.config['DATA_DIR']):
            filepath = os.path.join(app.config['DATA_DIR'], filename)
            if os.path.getmtime(filepath) < cutoff_date.timestamp():
                os.remove(filepath)
                app.logger.info(f'Deleted old note: {filename}')
    except Exception as e:
        app.logger.error(f'Error during cleanup: {str(e)}')

@app.route('/')
def index():
    """主页 - 创建新记事本"""
    try:
        note_id = generate_id(app.config['NOTE_ID_LENGTH'])
        while os.path.exists(get_note_path(note_id)):
            note_id = generate_id(app.config['NOTE_ID_LENGTH'])
        os.makedirs(app.config['DATA_DIR'], exist_ok=True)
        return redirect(url_for('note', note_id=note_id))
    except Exception as e:
        app.logger.error(f'Error creating new note: {str(e)}')
        abort(500)

@app.route('/<note_id>')
def note(note_id):
    """显示记事本页面"""
    try:
        note_path = get_note_path(note_id)
        content = ''
        if os.path.exists(note_path):
            with open(note_path, 'r', encoding='utf-8') as f:
                content = f.read()
        return render_template('note.html', content=content)
    except Exception as e:
        app.logger.error(f'Error accessing note {note_id}: {str(e)}')
        abort(500)

@app.route('/<note_id>', methods=['POST'])
def save_note(note_id):
    """保存记事本内容"""
    try:
        content = request.form.get('content', '')
        if len(content.encode('utf-8')) > app.config['MAX_NOTE_SIZE']:
            abort(413)
        
        note_path = get_note_path(note_id)
        os.makedirs(app.config['DATA_DIR'], exist_ok=True)
        
        with open(note_path, 'w', encoding='utf-8') as f:
            f.write(content)
        
        cleanup_old_notes()
        return 'OK'
    except Exception as e:
        app.logger.error(f'Error saving note {note_id}: {str(e)}')
        abort(500)

@app.errorhandler(404)
def not_found_error(error):
    return render_template('error.html', error='页面未找到'), 404

@app.errorhandler(500)
def internal_error(error):
    return render_template('error.html', error='服务器内部错误'), 500

@app.errorhandler(413)
def request_entity_too_large(error):
    return render_template('error.html', error='内容超出大小限制'), 413

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8082)

2. 配置文件 (config.py)

import os

class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard-to-guess-string'
    DATA_DIR = 'data'
    MAX_CONTENT_LENGTH = 1 * 1024 * 1024
    NOTE_ID_LENGTH = 5
    MAX_NOTE_SIZE = 500 * 1024
    NOTE_LIFETIME_DAYS = 30

3. 记事本模板 (templates/note.html)

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Minimalist Web Notepad</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            height: 100vh;
        }
        textarea {
            width: 100%;
            height: 100vh;
            border: none;
            padding: 20px;
            font-family: monospace;
            font-size: 14px;
            box-sizing: border-box;
            resize: none;
            outline: none;
        }
    </style>
</head>
<body>
    <textarea id="content" autofocus>{{ content }}</textarea>
    <script>
        let textarea = document.getElementById('content');
        let timeoutId;
        
        textarea.addEventListener('input', function() {
            clearTimeout(timeoutId);
            timeoutId = setTimeout(saveContent, 1000);
        });

        function saveContent() {
            fetch(window.location.pathname, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                },
                body: 'content=' + encodeURIComponent(textarea.value)
            });
        }
    </script>
</body>
</html>

4. 错误页面模板 (templates/error.html)

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>错误 - Minimalist Web Notepad</title>
    <style>
        body {
            margin: 40px auto;
            max-width: 650px;
            line-height: 1.6;
            font-size: 18px;
            color: #444;
            padding: 0 10px;
            font-family: sans-serif;
        }
        h1 {
            line-height: 1.2;
        }
    </style>
</head>
<body>
    <h1>出错了</h1>
    <p>{{ error }}</p>
    <p><a href="/">返回首页</a></p>
</body>
</html>

5. WSGI入口 (wsgi.py)

from app import app

if __name__ == "__main__":
    app.run()

6. 启动脚本 (start.bat)

@echo off
set FLASK_ENV=production
python -m waitress --host=0.0.0.0 --port=8082 wsgi:app

7. 开机自启动脚本 (startup.bat)

@echo off
cd /d %~dp0
start /min cmd /c start.bat

部署步骤

  1. 安装依赖:
pip install flask waitress
  1. 创建必要的目录:
mkdir data logs
  1. 配置开机自启:
$WshShell = New-Object -comObject WScript.Shell
$Shortcut = $WshShell.CreateShortcut("$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup\MinitexNotepad.lnk")
$Shortcut.TargetPath = "$(Get-Location)\startup.bat"
$Shortcut.WorkingDirectory = "$(Get-Location)"
$Shortcut.WindowStyle = 7
$Shortcut.Save()
  1. 启动服务:
.\start.bat

使用说明

  1. 访问 http://localhost:8082 自动创建新记事本
  2. 直接在页面上编辑内容,会自动保存
  3. 通过URL分享笔记
  4. 笔记在30天后自动清理

注意事项

  1. 确保端口 8082 未被占用
  2. 需要 Python 3.x 环境
  3. 建议在生产环境中修改配置文件中的密钥
  4. 可以根据需要调整笔记的过期时间

改进空间

  1. 添加密码保护功能
  2. 实现笔记加密存储
  3. 添加富文本编辑功能
  4. 支持多语言界面
  5. 添加笔记分享限制
posted @ 2025-04-01 13:36  夷某蓁  阅读(187)  评论(0)    收藏  举报