GoRss2Webhook开发记录 #2 Store模块开发
编辑项目地址
https://github.com/Mystery00/GoRss2Webhook
Store模块设计
上一篇文章讲了GoRss2Webhook
的诞生,以及大概的模块设计,今天我们就先做Store
模块部分。
顾名思义,Store
模块就是为了存储配置而存在的,我们涉及到的存储的地方以下几个地方:
- RSS订阅信息:用来存储订阅了哪些
RSS
源; - RSS历史记录:用来存储自服务启动之后检测到的所有
RSS
条目,因为我们要做WebHook
,所以需要在定时拉取之后做一次去重,筛选出新的数据; - WebHook配置:这部分对应到服务的下游部分,也就是需要将
RSS
条目发送到哪里去; - 服务配置文件:服务运行所需的配置文件。
其中,服务配置文件是在整个模块设计之外的,所以不在Store
模块中统一处理。
模块类图
涉及到的这三个部分,都采用同样的类结构,也就是写一个接口方法,然后分别使用不同的实现类来完成逻辑,上层调用时调用接口方法,然后在项目启动时根据服务配置来实例化不同的实现类,最终完成逻辑。
RSS订阅信息存储
首先,我们先定义出一个结构体和接口,代码如下:
type FeedSubscriber struct {
FeedUrl string
UserAgent string
ProxyUrl string
Timeout time.Duration
}
type FeedStore interface {
// Subscribe 订阅信息
Subscribe(subscriber FeedSubscriber) error
// GetAll 获取订阅信息
GetAll() ([]FeedSubscriber, error)
// Unsubscribe 取消订阅信息
Unsubscribe(feedUrl string) error
}
结构体中存储了一个RSS
订阅源的订阅地址和配置信息,实际上我们最重要的只是FeedUrl
就行了,至于UA
、代理地址
、超时时间
是根据一些实际情况增加的配置项。
UA
可以用在某些要求较为严格的RSS
订阅,代理地址同理,超时时间则是为了保护我们的服务,不让某些订阅源的失效影响到我们服务的运行。
接口上有三个方法:
- 添加一个订阅
- 获取当前的所有订阅
- 移除一个订阅
然后我们来写一个实现类,最简单的就是内存了。
type memoryStore struct {
data []store.FeedSubscriber
}
func Init() store.FeedStore {
var rssStore store.FeedStore
rssStore = &memoryStore{
data: make([]store.FeedSubscriber, 0),
}
return rssStore
}
func (store *memoryStore) Subscribe(subscriber store.FeedSubscriber) error {
store.data = append(store.data, subscriber)
return nil
}
func (store *memoryStore) GetAll() ([]store.FeedSubscriber, error) {
return store.data, nil
}
func (store *memoryStore) Unsubscribe(feedUrl string) error {
for i, subscriber := range store.data {
if subscriber.FeedUrl == feedUrl {
store.data = append(store.data[:i], store.data[i+1:]...)
return nil
}
}
return nil
}
代码应该还是很好懂的,就是来初始化时,创建一个 []store.FeedSubscriber
用来存储订阅列表,需要的时候直接返回就行了。
然后继续写文件的存储。文件的存储稍微复杂一些,因为涉及到IO操作
,最简单就是写的时候写到文件,读的时候从文件读,因此我们写这样子一些工具方法
// 读取文件
func readFile(storePath, fileName string) ([]byte, error) {
filePath := storePath + "/" + fileName
if !exists(filePath) {
return nil, nil
}
bytes, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, err
}
return bytes, nil
}
// 写入文件
func writeFile(parent, fileName string, content []byte) error {
err := os.MkdirAll(parent, os.ModePerm)
if err != nil {
return err
}
filePath := parent + "/" + fileName
err = ioutil.WriteFile(filePath, content, 0644)
return err
}
func exists(path string) bool {
//获取文件信息
_, err := os.Stat(path)
if err != nil {
if os.IsExist(err) {
return true
}
return false
}
return true
}
通过os
包以及ioutil
,我们可以很方便的完成文件的读写操作。
然后在这些工具方法的基础上,我们来实现store
。
首先编写一个结构体:
type fileStore struct {
storePath string
fileName string
data []store.FeedSubscriber
}
在这里我加了一个缓存的 []store.FeedSubscriber
,因为我们读取的情况可能很多,所以在内存中放一个列表,这样子可以在多次调用的情况下提升查询的性能。
然后实现Store
的接口:
import (
"GoRss2Webhook/utils/file"
)
func Init(storePath, fileName string) store.FeedStore {
data := make([]store.FeedSubscriber, 0)
err := file.Read(storePath, fileName, &data)
if err != nil {
logrus.Warnf(`read file error: %s`, err.Error())
}
var rssStore store.FeedStore
fileStore := &fileStore{
storePath: storePath,
fileName: fileName,
data: data,
}
rssStore = fileStore
return rssStore
}
func (store *fileStore) Subscribe(subscriber store.FeedSubscriber) error {
store.data = append(store.data, subscriber)
return file.Write(store.storePath, store.fileName, store.data)
}
func (store *fileStore) GetAll() ([]store.FeedSubscriber, error) {
return store.data, nil
}
func (store *fileStore) Unsubscribe(feedUrl string) error {
for i, subscriber := range store.data {
if subscriber.FeedUrl == feedUrl {
store.data = append(store.data[:i], store.data[i+1:]...)
return nil
}
}
return file.Write(store.storePath, store.fileName, store.data)
}
这里我把导入的包也放进来了,file
包对应了上面文件操作的工具方法,中间增加了一层序列化,也就是json
的Marshal
和Unmarshal
。
当对内存中的列表进行写操作后,我们将内存的列表往文件中同步一份,用来做持久化,采用读写分离来完成功能。当然,文件存储方式在初始化的时候就要去文件读一次数据,初始化内存中的列表,这里还需要加上一些异常处理,比如文件存在,当时里面的数据无法读取等。
这个时候我们就可以写两个单元测试来测试这两个实现的正确性。
详细的单元测试可以查看下面的链接:
- https://github.com/Mystery00/GoRss2Webhook/blob/main/feed/store/memory/memory_test.go
- https://github.com/Mystery00/GoRss2Webhook/blob/main/feed/store/file/file_test.go
RSS历史记录和WebHook配置
剩下的两个地方和RSS
订阅基本相同,内存的基本上都是用slice
或者map
来实现,文件的方式用工具方法加上内存缓冲区来实现。
稍微有一点点不一样的是RSS历史记录,因为涉及到的数据有两层结构:RSS订阅
- RSS条目
,这两层数据是一对多的关系,所以我把RSS
订阅做成了一个目录,目录里面存放这个订阅的历史条目数据,这样子当我们需要删除一个RSS
订阅时,可以在文件系统中直接删除这个目录,就不需要一个个去遍历条目的信息了。
当然,有一个需要注意的点,内存的方式我们是直接放入数据,文件的话因为有系统对一些特殊符号的限制,所以需要对数据做一次处理再作为文件名或者目录名,最简单的散列就行了。
结语
这三个模块的代码分别在
-
https://github.com/Mystery00/GoRss2Webhook/tree/main/feed/store
-
https://github.com/Mystery00/GoRss2Webhook/tree/main/webhook/store
需要查看更多详情的可以查阅代码。
- 0
- 0
-
分享