This repository has been archived by the owner on May 2, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 14
/
persistence.go
320 lines (302 loc) · 9.75 KB
/
persistence.go
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
/**
* Functionality to persist information about feeds that
* have been subscribed to and any data we might want to
* keep about things like the number of items retrieved.
*/
package main
import (
"database/sql"
"fmt"
rss "github.com/jteeuwen/go-pkg-rss"
_ "github.com/mattn/go-sqlite3"
"time"
)
// SQLite table initialization statements
// Create the table that contains information about RSS/Atom feeds the reader follows
const createFeedsTable = `create table if not exists feeds(
id integer primary key,
url varchar(255) unique on conflict ignore,
type varchar(8),
charset varchar(64),
articles integer,
lastPublished varchar(64),
latest varchar(255),
title varchar(255),
direction varchar(64)
);`
// Create the table that contains information about items received from feeds
const createItemsTable = `create table if not exists items(
id integer primary key,
title varchar(255),
url varchar(255),
feed_url varchar(255),
authors text,
published varchar(64)
);`
// Create a table containing information about errors encountered trying to subscribe
// to feeds, handling articles, etc.
// These errors can be retrieved as a report later that can be used to diagnose problems
// with specific feeds
const createErrorsTable = `create table if not exists errors(
id integer primary key,
resource_types integer,
error_types integer,
message text
);`
// A list of the tables to try to initialize when the reader starts
var tableInitializers = []string{
createFeedsTable,
createItemsTable,
createErrorsTable,
}
/**
* Repeatedly test a condition until it passes, allowing a thread to block
* on the condition.
* @param {func() bool} condition - A closure that will test the condition
* @param {time.Duration} testRate - The frequency at which to invoke the condition function
* @return A channel from which the number of calls to the condition were made when it passes
*/
func WaitUntilPass(condition func() bool, testRate time.Duration) chan int {
reportAttempts := make(chan int, 1)
go func() {
attempts := 0
passed := false
for !passed {
attempts++
passed = condition()
<-time.After(testRate)
}
reportAttempts <- attempts
}()
return reportAttempts
}
/**
* Create a database connection to SQLite3 and initialize (if not already)
* the tables used to store information about RSS feeds being followed etc.
* @param {string} dbFileName - The name of the file to keep the database info in
*/
func InitDBConnection(dbFileName string) (*sql.DB, error) {
db, err := sql.Open("sqlite3", dbFileName)
if err != nil {
return nil, err
}
for _, initializer := range tableInitializers {
_, err = db.Exec(initializer)
if err != nil {
break
}
}
return db, err
}
/**
* Persist information about a feed to the storage medium.
* @param {*sql.DB} db - The database connection to use
* @param {Feed} feed - Information describing the new feed to save
* @return any error that occurs trying to run the query
*/
func SaveFeed(db *sql.DB, feed Feed) error {
tx, err1 := db.Begin()
if err1 != nil {
return err1
}
_, err2 := tx.Exec(`
insert into feeds(url, type, charset, articles, lastPublished, latest, title, direction)
values(?,?,?,?,?,?,?,?)`,
feed.Url, feed.Type, feed.Charset, feed.Articles, feed.LastPublished, feed.Latest, feed.Title, feed.Direction)
if err2 != nil {
return err2
}
tx.Commit()
return nil
}
/**
* Get a collection of all the feeds subscribed to.
* @param {*sql.DB} db - The database connection to use
* @return the collection of feeds retrieved from the database and any error that occurs
*/
func AllFeeds(db *sql.DB) ([]Feed, error) {
var feeds []Feed
rows, err2 := db.Query(`select id, url, type, charset, articles, lastPublished, latest, title, direction
from feeds`)
if err2 != nil {
return feeds, err2
}
for rows.Next() {
var url, _type, charset, lastPublished, latest, title, direction string
var id, articles int
rows.Scan(&id, &url, &_type, &charset, &articles, &lastPublished, &latest, &title, &direction)
fmt.Printf("Found feed %s with %d articles. Last published %s on %s.\n",
url, articles, latest, lastPublished)
feeds = append(feeds, Feed{id, url, _type, charset, articles, lastPublished, latest, title, direction})
}
rows.Close()
return feeds, nil
}
/**
* Get the basic information about a persisted feed from its URL.
* @param {*sql.DB} db - The database connection to use
* @param {string} url - The URL to search for
* @return the feed retrieved from the database and any error that occurs
*/
func GetFeed(db *sql.DB, url string) (Feed, error) {
var feed Feed
var id, articles int
var _type, charset, lastPublished, latest, title, direction string
log("reading feed info from database")
err2 := db.QueryRow(`select id, url, type, charset, articles, lastPublished, latest, title, direction from feeds where url=?`, url).Scan(&id, &url, &_type, &charset, &articles, &lastPublished, &latest, &title, &direction)
if err2 != nil {
log("Failed to retrieve feed info from the db")
return feed, err2
}
log("feed info: " + url + " " + title + " " + direction)
return Feed{id, url, _type, charset, articles, lastPublished, latest, title, direction}, nil
}
/**
* Delete a feed by referencing its URL.
* @param {*sql.DB} db - The database connection to use
* @param {string} url - The URL of the feed
* @return any error that occurs executing the delete statement
*/
func DeleteFeed(db *sql.DB, url string) error {
tx, err1 := db.Begin()
if err1 != nil {
return err1
}
_, err2 := tx.Exec("delete from feeds where url=?", url)
if err2 != nil {
return err2
}
tx.Commit()
return nil
}
/**
* Store information about an item in a channel from a particular feed
* @param {*sql.DB} db - the database connection to use
* @param {string} feedUrl - The URL of the RSS/Atom feed
* @param {*rss.Item} item - The item to store the content of
* @return any error that occurs saving the item
*/
func SaveItem(db *sql.DB, feedUrl string, item *rss.Item) error {
tx, err1 := db.Begin()
if err1 != nil {
return err1
}
url := item.Links[0].Href
authors := item.Author.Name
if item.Contributors != nil {
for _, contrib := range item.Contributors {
authors += " " + contrib
}
}
// Insert the item itself
_, err2 := tx.Exec(`
insert into items(title, url, feed_url, authors, published)
values(?, ?, ?, ?, ?)`,
item.Title, url, feedUrl, authors, item.PubDate)
if err2 != nil {
return err2
}
_, err3 := tx.Exec(`
update feeds
set articles=articles+1, lastPublished=?, latest=?
where url=?`,
item.PubDate, item.Title, feedUrl)
if err3 != nil {
return err3
}
tx.Commit()
return nil
}
/**
* Get the items stored for a particular feed in reference to its URL.
* @param {*sql.DB} db - the database connection to use
* @param {string} url - The URL of the feed to get items from
* @return the collection of items retrieved from the database and any error that occurs
*/
func GetItems(db *sql.DB, feedUrl string) ([]Item, error) {
var items []Item
rows, err2 := db.Query(`select id, title, url, authors, published from items where feed_url=?`,
feedUrl)
if err2 != nil {
return items, err2
}
for rows.Next() {
var id int
var title, authors, published, url string
rows.Scan(&id, &title, &url, &authors, &published)
items = append(items, Item{id, title, url, feedUrl, authors, published})
}
rows.Close()
return items, nil
}
/**
* Delete a particular item.
* @param {*sql.DB} db - The database connection to use
* @param {int} id - The identifier of the item to delete
* @return any error that occurs running the delete statement
*/
func DeleteItem(db *sql.DB, id int) error {
tx, err1 := db.Begin()
if err1 != nil {
return err1
}
_, err2 := tx.Exec("delete from items where id=?")
if err2 != nil {
return err2
}
tx.Commit()
return nil
}
/**
* Save a report of the incidence of an error having occurred.
* Note that converting between encoded resource types and error classifications
* occurs strictly in the CENO Reader codebase. We aren't going to bother doing it in SQL.
* @param {*sql.DB} db - The database connection to use
* @param {Errorreport} report - Information about the error that occurred
* @return any error that occurs saving the error report information
*/
func SaveError(db *sql.DB, report ErrorReport) error {
tx, err := db.Begin()
if err != nil {
return err
}
_, execErr := tx.Exec(`
insert into errors (resource_types, error_types, message) values(?, ?, ?)`,
report.ResourceTypes, report.ErrorTypes, report.ErrorMessage)
if execErr != nil {
return execErr
}
tx.Commit()
return nil
}
/**
* Get an array of ErrorReports corresponding to the kinds specified by the
* argument to this function. Once error reports are retrieved from the database,
* they are deleted since we want to avoid reporting errors twice.
* @param {*sql.DB} db - The database connection to use
* @return the collection of error reports retrieved from the database and any error that occurs
*/
func GetErrors(db *sql.DB) ([]ErrorReport, error) {
reports := make([]ErrorReport, 0)
// Get the relevant rows from the database.
// Note that error types and resource types are specified in the database as integers.
// That means we do the usual binary operations to find them.
rows, queryError := db.Query(`select id, resource_types, error_types, message from errors`)
if queryError != nil {
return reports, queryError
}
for rows.Next() {
var id, resourceTypes, errorTypes int
var message string
rows.Scan(&id, &resourceTypes, &errorTypes, &message)
reports = append(reports, ErrorReport{
id, Resource(resourceTypes), ErrorClass(errorTypes), message,
})
}
rows.Close()
_, execError := db.Exec(`delete from errors`)
if execError != nil {
return reports, execError
}
return reports, nil
}