目录

wallabag数据恢复记录

说明:只恢复了所有保存的文章,关于文章标签、文章标注未能100%恢复

说明:个人数据一定要备份!一定要备份!一定要备份!

起因

1月30日cloudcone在洛杉矶的机房遭受了勒索攻击,因为他们一个内部使用的虚拟机管理系统被侵入导致大批虚拟机的引导磁盘都被写入了勒索信息。而且cloudcone修复了三天也没能救回一台虚拟机,最后只能发邮件告诉客户自己重装(问题是最后也没有说明这次事故是否有补偿👎🏻)。

我的wallabag服务在虚拟机上跑了两年,上面保存的文章大概100多个,这下重装等于所有的数据都没有了……而且我并没有备份服务端的数据。

没有备份?

服务器上的数据肯定是没希望了,想来想去是不是可以从移动端入手?好在我还有两个移动端的数据,因为我经常会在手机上看,所以同步的数据和服务端的差异应该不是很大。

移动端没有导出的功能,但是可以把整个数据库找出来。数据库文件默认保存在内部存储上,如果想要获取到,需要修改设置。之后就可以获取到app的sqlite数据库了。

https://dylanblog.oss-cn-beijing.aliyuncs.com/2026-02-06/Screenshot_2026-02-05-22-23-57-186_fr.gaulupeau.apps.InThePoche-edit.jpg

尝试恢复数据

之前安装的wallabag我使用的是默认的mysql数据库,现在手里已经有了移动端的sqlite数据库,所以这次服务端也用sqlite看看能不能直接恢复。

安装部署完成之后,先把data/db目录下的wallabag.sqlite替换成移动端的数据库,之后尝试启动,访问时系统报错找不到wallabag_user表,所以服务端和移动端的表结构不同,方法失败。

经过和ChatGPT交流之后,暂定一个方案:将移动端的数据转换成json格式,之后导入新的服务端。

https://dylanblog.oss-cn-beijing.aliyuncs.com/2026-02-06/Pasted%20image%2020260205223441.png

生成导入数据

我修改了GPT写的脚本,最终版如下:

 1
 2
 3
 4
 5
 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
31
32
33
34
35
36
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
import sqlite3
import json
from datetime import datetime

conn = sqlite3.connect("wallabag")
conn.row_factory = sqlite3.Row
cur = conn.cursor()

# 取文章 + 内容
cur.execute("""
SELECT A._id,
  A.ARTICLE_ID,
  A.URL,
  A.TITLE,
  A.ARCHIVE,
  A.FAVORITE,
  A.CREATION_DATE,
  C.CONTENT
FROM ARTICLE A
LEFT JOIN ARTICLE_CONTENT C ON A._id = C._id
WHERE A.URL IS NOT NULL
""")

articles = []

for row in cur.fetchall():
    created_at = None
    if row["CREATION_DATE"]:
        created_at = datetime.fromtimestamp(
            row["CREATION_DATE"] / 1000
        ).isoformat()

    articles.append({
        "id": row["ARTICLE_ID"] - 1, # ARTICLE_TAGS_JOIN记录的文章id是实际的文章id减1
        "url": row["URL"],
        "title": row["TITLE"],
        "content": row["CONTENT"],
        "tags": [],  # 下一步再补
        "is_archived": bool(row["ARCHIVE"]),
        "is_starred": bool(row["FAVORITE"]),
        "created_at": created_at
    })

# 取标签
cur.execute("""
SELECT
  AT.ARTICLE_ID,
  T.TAG_ID
FROM ARTICLE_TAGS_JOIN AT
JOIN TAG T ON AT.TAG_ID = T._id
""")

tag_map = {}
for r in cur.fetchall():
    tag_map.setdefault(r["ARTICLE_ID"], set()).add(r["TAG_ID"])

cur.execute("SELECT TAG_ID, LABEL FROM TAG")
tag_label = {r["TAG_ID"]: r["LABEL"] for r in cur.fetchall()}

# 合并标签
for a in articles:
    tag_ids = tag_map.get(a["id"], set())
    a["tags"] = [tag_label[t] for t in tag_ids if t in tag_label]

with open("wallabag_import_without_content.json", "w", encoding="utf-8") as f:
    json.dump(articles, f, ensure_ascii=False, indent=2)

print(f"Exported {len(articles)} articles.")

这个脚本可以快速将移动端sqlite数据库中的数据转换成json格式,其中可以包括文章的所有主要信息(标题、URL、内容、标签等)。

放开上传数据限制

现在已经生成了用于导入的json数据,在wallabag上使用导入功能导入即可。

但是由于我们的数据大小超过了限制(2M),所以还需要修改一些配置来放开上传限制。

Nginx

因为是使用Nginx做反向代理,所以第一道关卡就在这里(超出限制会返回HTTP 413),需要在配置文件里添加client_max_body_size来增加上传文件的大小限制。

When you want to import large files into wallabag, you need to add this line in your nginx configuration client_max_body_size XM; # allows file uploads up to X megabytes.

PHP

wallabag自身使用php-fpm运行,在php的默认配置里同样限制了可上传文件的大小(在wallabag导入页面提示限制大小为2M),所以可以修改这个配置。

/etc/php/8.2/fpm/conf.d/(8.2是我使用的版本)下新建一个配置20-wallabag.ini,写入upload_max_filesize = 5M,之后重启php8.2-fpm.service。就可以上传成功了。

没解决的问题

恢复之后,这里的标签对应关系并不完全正确,主要在于ARTICLE表的article_idARTICLE_TAGS_JOIN表的article_id有部分对应错误,所以文章的标签存在很多错位的。这是原始数据的问题,没法靠脚本来修正了(为什么移动端的数据会有错位也不清楚)。所以就后期靠手动修改吧。

https://dylanblog.oss-cn-beijing.aliyuncs.com/2026-02-06/Pasted%20image%2020260205180227.png

同时还有原来文章中的高亮标注,GPT并没有给帮我实现,而且我查看过数据表,大概率是和标签一样会混乱,所以也就放弃了,手动处理就好。

备份

经过教训之后,还是老老实实地备份。根据wallabag的文档,如果是使用sqlite数据库,简单地拷贝数据库文件进行备份就可以。

在nas上配置一条cron任务,每天0点自动从服务器上同步数据库文件。