Mystery0の小站

Mystery0の小站

GoRss2Webhook开发记录 #2 Store模块开发

2022-05-25
GoRss2Webhook开发记录 #2 Store模块开发

项目地址

https://github.com/Mystery00/GoRss2Webhook

Store模块设计

上一篇文章讲了GoRss2Webhook的诞生,以及大概的模块设计,今天我们就先做Store模块部分。

顾名思义,Store模块就是为了存储配置而存在的,我们涉及到的存储的地方以下几个地方:

  • RSS订阅信息:用来存储订阅了哪些RSS源;
  • RSS历史记录:用来存储自服务启动之后检测到的所有RSS条目,因为我们要做WebHook,所以需要在定时拉取之后做一次去重,筛选出新的数据;
  • WebHook配置:这部分对应到服务的下游部分,也就是需要将RSS条目发送到哪里去;
  • 服务配置文件:服务运行所需的配置文件。

其中,服务配置文件是在整个模块设计之外的,所以不在Store模块中统一处理。

模块类图

图片-1653458747245

涉及到的这三个部分,都采用同样的类结构,也就是写一个接口方法,然后分别使用不同的实现类来完成逻辑,上层调用时调用接口方法,然后在项目启动时根据服务配置来实例化不同的实现类,最终完成逻辑。

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包对应了上面文件操作的工具方法,中间增加了一层序列化,也就是jsonMarshalUnmarshal

当对内存中的列表进行写操作后,我们将内存的列表往文件中同步一份,用来做持久化,采用读写分离来完成功能。当然,文件存储方式在初始化的时候就要去文件读一次数据,初始化内存中的列表,这里还需要加上一些异常处理,比如文件存在,当时里面的数据无法读取等。

这个时候我们就可以写两个单元测试来测试这两个实现的正确性。

详细的单元测试可以查看下面的链接:

RSS历史记录和WebHook配置

剩下的两个地方和RSS订阅基本相同,内存的基本上都是用slice或者map来实现,文件的方式用工具方法加上内存缓冲区来实现。

稍微有一点点不一样的是RSS历史记录,因为涉及到的数据有两层结构:RSS订阅 - RSS条目,这两层数据是一对多的关系,所以我把RSS订阅做成了一个目录,目录里面存放这个订阅的历史条目数据,这样子当我们需要删除一个RSS订阅时,可以在文件系统中直接删除这个目录,就不需要一个个去遍历条目的信息了。

当然,有一个需要注意的点,内存的方式我们是直接放入数据,文件的话因为有系统对一些特殊符号的限制,所以需要对数据做一次处理再作为文件名或者目录名,最简单的散列就行了。

结语

这三个模块的代码分别在

需要查看更多详情的可以查阅代码。