用Go创建一个iCal feed的方法

265 阅读5分钟

这篇文章将展示一个简短的例子,说明如何从Go网络服务器上创建一个.ics feed。数据源将是一个用mocky创建的假REST端点。

数据也将被缓存在内存中,因此我们不必一直调用API,并能够提供快速的响应时间。

为了创建feed本身,将使用goics库,这有助于编码。

服务器上将有两个端点。

  • POST /feedURL - 创建一个新的、随机生成的feedURL,并初始化token的feed。
  • GET /feed/{token} - 返回给定token的feed,如果缓存过期,则重新创建。

说了这么多,让我们开始吧!

实施

首先,让我们看一下我们的数据源。我们将使用一个来自mocky 的假JSON响应,JSON看起来像这样。

[
    {
        "dateStart": "2018-02-01T13:30:01+00:00",
        "dateEnd": "2018-02-01T12:30:01+00:00",
        "description": "dentist"
    },
    {
        "dateStart": "2018-02-02T13:21:01+00:00",
        "dateEnd": "2018-02-02T15:51:01+00:00",
        "description": "gym"
    },
    {
        "dateStart": "2018-02-09T13:21:01+00:00",
        "dateEnd": "2018-02-09T14:21:01+00:00",
        "description": "meeting"
    },
    {
        "dateStart": "2018-02-09T15:21:01+00:00",
        "dateEnd": "2018-02-09T17:41:01+00:00",
        "description": "cooking class"
    },
    {
        "dateStart": "2018-02-11T13:21:01+00:00",
        "dateEnd": "2018-02-11T18:21:01+00:00",
        "description": "gym"
    },
    {
        "dateStart": "2018-02-12T13:21:01+00:00",
        "dateEnd": "2018-02-12T15:21:01+00:00",
        "description": "shopping"
    },
    {
        "dateStart": "2018-02-14T19:00:01+00:00",
        "dateEnd": "2018-02-14T21:00:01+00:00",
        "description": "valentines"
    }
]

为了简单起见,数据已经形成,我们的目的是创建以description 为标题的日历条目。

有了这些数据,我们可以创建我们的数据模型。一个用于解析JSON的Entry 结构和一个作为缓存创建的feed的容器的Feed 结构应该足够了。

// Feed is an iCal feed
type Feed struct {
    Content   string
    ExpiresAt time.Time
}

// Entry is a time entry
type Entry struct {
    DateStart   time.Time `json:"dateStart"`
    DateEnd     time.Time `json:"dateEnd"`
    Description string    `json:"description"`
}

// Entries is a collection of entries
type Entries []*Entry

还有一个Entries 集合类型,我们将需要它来进行.ics 编码,但我们将在后面处理这个问题。

下一步是用上述的路由创建一个简单的Go网络服务器。

const feedPrefix = "/feed/"
const expirationTime = 20 * time.Minute // caching time

func main() {
    cache := make(map[string]*Feed)

    mux := http.NewServeMux()
    mux.HandleFunc("/feedURL", feedURL(cache))
    mux.HandleFunc(feedPrefix, feed(cache))

    log.Print("Server started on localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

我们创建一个简单的 "缓存",在我们的例子中,它只是一个map[string]*Feed ,并为两个路由注册处理函数。

让我们先看一下feedURL

func feedURL(cache map[string]*Feed) http.HandlerFunc {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := randomToken(20)
        _, err := createFeedForToken(token, cache)
        if err != nil {
            writeError(http.StatusInternalServerError, "Could not create feed", w, err)
            return
        }
        writeSuccess(fmt.Sprintf("FeedToken: %s", token), w)
    })
}

这个处理程序使用一个简单的辅助函数crypto/rand ,创建一个随机的token,调用createFeedForToken ,我们将在后面看一下,为给定的token创建一个新的feed,并将token返回给用户。

第二个处理程序,/feed/{token} ,涉及的内容更多。

func feed(cache map[string]*Feed) http.HandlerFunc {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-type", "text/calendar")
        w.Header().Set("charset", "utf-8")
        w.Header().Set("Content-Disposition", "inline")
        w.Header().Set("filename", "calendar.ics")

        var result string
        token := parseToken(r.URL.Path)

        feed, ok := cache[token]
        if !ok || feed == nil {
            writeError(http.StatusNotFound, "No Feed for this Token", w, errors.New("No Feed for this Token"))
            return
        }

        result = feed.Content
        if feed.ExpiresAt.Before(time.Now()) {
            newFeed, err := createFeedForToken(token, cache)
            if err != nil {
                writeError(http.StatusInternalServerError, "Could not create feed", w, err)
                return
            }
            result = newFeed.Content
        }

        writeSuccess(result, w)
    })
}

在为.ics 响应设置标准头信息并解析所提供的标记后,我们检查是否有一个给定标记的缓存条目。如果没有,我们会返回一个404 错误。

如果有一个token的feed,但它已经过期,我们使用createFeedForToken 重新创建feed,并返回新创建的feed。

如果有一个条目并且仍然有效,我们就从缓存中返回feed。

现在,让我们看一下createFeedForToken 函数。

func createFeedForToken(token string, cache map[string]*Feed) (*Feed, error) {
    res, err := fetchData()
    if err != nil {
        return nil, err 
    }

    b := bytes.Buffer{}
    goics.NewICalEncode(&b).Encode(res)

    feed := &Feed{
        Content: b.String(),
        ExpiresAt: time.Now().Add(expirationTime)
    }
    cache[token] = feed

    return feed, nil
}

fetchData 函数没有做什么花哨的事情。它调用mocky URL,并将生成的JSON解密到Entry 结构的列表中,处理所有可能的错误。

然后,我们使用goics.NewICalEncode().Encode() 创建实际的.ics feed,用给定的token将其放入缓存并返回feed。

为了能够使用我们的Entry 结构列表作为goics.NewICalEncode(&b).Encode() 的有效输入,Entries 类型需要实现ICalEmitter 接口。

// EmitICal implements the interface for goics
func (e Entries) EmitICal() goics.Componenter {
    c := goics.NewComponent()
    c.SetType("VCALENDAR")
    c.AddProperty("CALSCAL", "GREGORIAN")

    for _, entry := range e {
        s := goics.NewComponent()
        s.SetType("VEVENT")

        k, v := goics.FormatDateTimeField("DTSTART", entry.DateStart)
        s.AddProperty(k, v)
        k, v = goics.FormatDateTimeField("DTEND", entry.DateEnd)
        s.AddProperty(k, v)

        s.AddProperty("SUMMARY", entry.Description)
        c.AddComponent(s)
    }
    return c
}

该方法使用goics 帮助器来创建.ics 输出,如图书馆文档所示。

现在,如果我们调用/feedURL ,并将生成的令牌作为/feed/{token} 的输入,我们应该得到一个有效的calendar.ics 文件,它可以被导入到iCalendar、Google Calendar等应用程序中。

还请注意,向/feedURL 发出的第一个请求(初始化Feed)需要大约100毫秒,而向/feed/ 发出的后续请求(返回缓存的结果)则非常快。

就这样了。你可以在这里找到完整的代码。

总结

这是在Go中的另一个简短的例子,它是网络应用中经常实现的一个基本功能。我没有找到很多用于iCal 的 Go 库,但是我使用的这个库工作得很好,对于这个使用案例来说是足够的。

更广泛地说,我相信这个例子说明了 Go 标准库是多么强大,以及像这样的简单功能可以用它来实现。

资源