背景:极简记事本的唯一缺憾
我用的是 GitHub 上的 minimalist-web-notepad(在 /srv/www/文本笔记/),只有一个 index.php(几千行),没有数据库,所有笔记存成 .txt 文件在 _tmp/ 目录里。
它的优点是:
- 打开即写,内容自动保存到 URL 路径对应的 txt 文件
- 极简,没有账号、没有分类、只有「地址栏区分不同笔记」这一条规则
缺点也很明显:只能写文字,不能插图。
笔记站是我日常用得最多的服务:服务器命令、配置笔记、甚至买菜清单——全用它。纯文字一直够用,直到上个月我想把「Nginx 反代配置示意图」的截图存下来,文字描述不清:
"proxy_pass 到 127.0.0.1:5244,记得加 WebSocket 升级头,不然后台白屏"
看文字谁也想象不出来。必须有图。
需求拆解
我想实现的效果:
在记事本的编辑界面里,直接按 Ctrl+V 粘贴截图(QQ/微信截图后),或者拖拽图片进去 → 自动上传到服务器 → 笔记正文里自动插入 Markdown 图片链接 。
约束条件:
- 不能装 MySQL/Redis(笔记站本来就没数据库)
- 不能改底层逻辑太多(要保持一个 php 文件的极简风格)
- 图片访问地址要漂亮,不暴露真实磁盘路径
- 必须做文件大小 / 类型 / 总量限制(否则哪天贴了一张 20MB 的手机原图)
方案:30 行上传脚本 + 15 行 JS 粘贴监听
1. 文件结构
/srv/www/文本笔记/
├── index.php ← 原本的记事本(不碰核心逻辑)
├── upload.php ← 【新增】30 行的上传脚本
├── _tmp/ ← 笔记 txt 文件的存储目录
└── uploads/ ← 【新增】图片存储目录
└── 2026/
└── 08/
├── 01-img1.png ← 按日期归档,防止单目录文件过多
└── 01-img2.jpg
2. upload.php(后端核心)
<?php
/**
* 极简记事本图片上传脚本
* - 只允许 png/jpg/jpeg/gif/webp
* - 单文件 <= 2MB
* - 每天最多 100 张(防滥用)
* - 按 年/月/日-时间戳-4位随机 命名,绝不覆盖
*/
$ALLOW_TYPES = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
$MAX_SIZE = 2 * 1024 * 1024; // 2MB
$DAILY_LIMIT = 100;
$UPLOAD_ROOT = __DIR__ . '/uploads';
$URL_PREFIX = '/uploads';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405); die('Method Not Allowed');
}
if (!isset($_FILES['image']) || $_FILES['image']['error'] !== UPLOAD_ERR_OK) {
http_response_code(400); die(json_encode(['ok' => 0, 'msg' => '上传失败']));
}
$file = $_FILES['image'];
if ($file['size'] > $MAX_SIZE) {
http_response_code(413); die(json_encode(['ok' => 0, 'msg' => '图片大于 2MB,请压缩后上传']));
}
if (!in_array($file['type'], $ALLOW_TYPES, true)) {
http_response_code(415); die(json_encode(['ok' => 0, 'msg' => '只支持 png/jpg/gif/webp']));
}
// 按日期目录归档:/uploads/2026/08/01_165902_abcd.png
[$y, $m, $d] = [date('Y'), date('m'), date('d')];
$dir = "$UPLOAD_ROOT/$y/$m";
if (!is_dir($dir)) mkdir($dir, 0755, true);
// 每日限制数:统计当天文件数
$today_files = glob("$dir/{$d}_*");
if ($today_files && count($today_files) >= $DAILY_LIMIT) {
http_response_code(429); die(json_encode(['ok' => 0, 'msg' => '今日上传次数上限,请明天再试']));
}
// 安全的文件名
$ext = (new finfo(FILEINFO_EXTENSION))->file($file['tmp_name'])
?: strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$ext = explode('/', $ext)[0] ?? $ext;
$filename = sprintf("%s_%s_%s.%s", $d, date('His'), substr(md5(uniqid()), 0, 4), $ext);
$dst = "$dir/$filename";
if (!move_uploaded_file($file['tmp_name'], $dst)) {
http_response_code(500); die(json_encode(['ok' => 0, 'msg' => '写入失败,检查 uploads 目录权限']));
}
header('Content-Type: application/json');
echo json_encode([
'ok' => 1,
'url' => "$URL_PREFIX/$y/$m/$filename",
'name' => $filename
], JSON_UNESCAPED_SLASHES);
3. 前端:改记事本的 editor 代码(JS 15 行)
把这段 JS 加到原本的 editor 初始化代码末尾(在 index.php 的 <script> 里插进去就行):
/* 粘贴 / 拖拽上传图片到笔记 */
const editor = document.querySelector('#editor') || document.querySelector('textarea');
function insertAtCursor(txt) {
const s = editor.selectionStart, e = editor.selectionEnd;
editor.value = editor.value.slice(0, s) + txt + editor.value.slice(e);
editor.selectionStart = editor.selectionEnd = s + txt.length;
}
async function uploadFile(file) {
const fd = new FormData(); fd.append('image', file);
const r = await fetch('/upload.php', { method: 'POST', body: fd });
const j = await r.json();
if (!j.ok) throw new Error(j.msg || '上传失败');
insertAtCursor(`\n\n`);
}
editor.addEventListener('paste', async e => {
const items = e.clipboardData?.items || [];
for (const it of items) {
if (it.kind === 'file' && it.type.startsWith('image/')) {
uploadFile(it.getAsFile()).catch(err => alert(err.message));
}
}
});
editor.addEventListener('drop', e => {
e.preventDefault();
for (const f of e.dataTransfer.files) {
if (f.type.startsWith('image/')) uploadFile(f).catch(err => alert(err.message));
}
});
4. Nginx 加的一条规则(防盗链 + 大小限制)
# 笔记站 server 块里
location /uploads/ {
valid_referers none blocked note.mccok.cn *.mccok.cn;
if ($invalid_referer) { return 403; } # 防盗链
client_max_body_size 2m; # 单请求不超 2M,同 PHP
expires 60d; # 图片强缓存
}
location ~* ^/upload\.php$ {
client_max_body_size 2m;
}
效果
现在在笔记里:
- 微信
Win+A截图一张 Nginx 配置 →Ctrl+C→ 切回笔记页Ctrl+V - 页面右下角小字提示 "正在上传..." → 0.5 秒后正文出现
 - 保存后刷新笔记,图片就嵌在文字中间了
完全不用去图床,不用走第三方,图片就在我自己服务器上。
还没做完的 3 个小优化
- 图片自动压缩——超过 500KB 自动用 GD 库缩到 1920 像素宽度
- 上传大小限制可配置——把 2MB 写到 index.php 顶部常量里
- 垃圾清理——半年没被引用的图片自动列出来人工确认删除
等我写好了补第二篇。
(完)