|
package main |
|
|
|
import ( |
|
"fmt" |
|
"encoding/json" |
|
"os" |
|
"time" |
|
"strings" |
|
"strconv" |
|
"path/filepath" |
|
"database/sql" |
|
|
|
"miniflux.app/config" |
|
"miniflux.app/logger" |
|
"miniflux.app/model" |
|
"miniflux.app/crypto" |
|
"miniflux.app/database" |
|
"miniflux.app/storage" |
|
|
|
"github.com/lib/pq" |
|
) |
|
|
|
type jsonFeedInfo struct { |
|
ID string `json:"id"` |
|
FeedType string `json:"feedType"` |
|
Title string `json:"title"` |
|
Categories []struct { |
|
ID string `json:"id"` |
|
Label string `json:"label"` |
|
} `json:"categories"` |
|
SortID string `json:"sortid"` |
|
FirstItemMsec int64 `json:"firstitemmsec"` |
|
URL string `json:"url"` |
|
HTMLURL string `json:"htmlUrl"` |
|
IconURL string `json:"iconUrl"` |
|
} |
|
|
|
|
|
type jsonFeed struct { |
|
Direction string `json:"direction"` |
|
ID string `json:"id"` |
|
Title string `json:"title"` |
|
Description string `json:"description"` |
|
Self struct { |
|
Href string `json:"href"` |
|
} `json:"self"` |
|
Updated int64 `json:"updated"` |
|
UpdatedUsec string `json:"updatedUsec"` |
|
Items []jsonItem `json:"items"` |
|
} |
|
|
|
|
|
|
|
type jsonItem struct { |
|
CrawlTimeMsec string `json:"crawlTimeMsec"` |
|
TimestampUsec string `json:"timestampUsec"` |
|
ID string `json:"id"` |
|
Categories []string `json:"categories"` |
|
Title string `json:"title"` |
|
Published int64 `json:"published"` |
|
Updated int64 `json:"updated"` |
|
Canonical []struct { |
|
Href string `json:"href"` |
|
} `json:"canonical"` |
|
Alternate []struct { |
|
Href string `json:"href"` |
|
Type string `json:"type"` |
|
} `json:"alternate"` |
|
Summary struct { |
|
Direction string `json:"direction"` |
|
Content string `json:"content"` |
|
} `json:"summary"` |
|
Author string `json:"author"` |
|
LikingUsers []struct{} `json:"likingUsers"` |
|
Comments []struct{} `json:"comments"` |
|
CommentsNum int `json:"commentsNum"` |
|
Annotations []struct { |
|
ID int64 `json:"id"` |
|
Start int `json:"start"` |
|
End int `json:"end"` |
|
AddedOn int64 `json:"added_on"` |
|
Text string `json:"text"` |
|
Note string `json:"note"` |
|
UserID int `json:"user_id"` |
|
UserName string `json:"user_name"` |
|
UserProfilePic string `json:"user_profile_picture"` |
|
} `json:"annotations"` |
|
Origin struct { |
|
StreamID string `json:"streamId"` |
|
Title string `json:"title"` |
|
HTMLUrl string `json:"htmlUrl"` |
|
} `json:"origin"` |
|
} |
|
|
|
func (j *jsonItem) GetCreateAt() time.Time { |
|
timestampUsec, _ := strconv.ParseInt(j.TimestampUsec, 10, 64) |
|
sec := timestampUsec / 1e6 |
|
usec := timestampUsec % 1e6 |
|
return time.Unix(sec, usec*1000) |
|
} |
|
|
|
|
|
func (j *jsonItem) GetHash() string { |
|
for _, value := range []string{j.ID} { |
|
if value != "" { |
|
return crypto.Hash(value) |
|
} |
|
} |
|
|
|
return "" |
|
} |
|
|
|
func (j *jsonItem) GetContent() string { |
|
return j.Summary.Content |
|
} |
|
|
|
|
|
func (j *jsonItem) Transform() *model.Entry { |
|
entry := new(model.Entry) |
|
entry.URL = j.Canonical[0].Href |
|
entry.Date = time.Unix(j.Published, 0) |
|
entry.CreatedAt = j.GetCreateAt() |
|
entry.Author = j.Author |
|
entry.Hash = j.GetHash() |
|
entry.Content = j.GetContent() |
|
entry.Title = strings.TrimSpace(j.Title) |
|
// entry.Tags = j.Categories |
|
return entry |
|
} |
|
|
|
func findByFeedURL(feeds model.Feeds, url string) *model.Feed{ |
|
for i := range feeds { |
|
if feeds[i].FeedURL == url { |
|
return feeds[i] |
|
} |
|
} |
|
return nil |
|
} |
|
|
|
func restoreBackup(feeds model.Feeds, s *storage.Storage, db *sql.DB, root string) { |
|
|
|
entries, err := os.ReadDir(root) |
|
if err != nil { |
|
fmt.Println("Error:", err) |
|
return |
|
} |
|
for _, entry := range entries { |
|
if entry.IsDir() { |
|
fmt.Println("Directory:", entry.Name()) |
|
path := filepath.Join(root, entry.Name(), "info.json") |
|
file, err := os.Open(path) |
|
if err != nil { |
|
fmt.Println("打开文件失败:", err) |
|
return |
|
} |
|
defer file.Close() |
|
feedInfo := new(jsonFeedInfo) |
|
decoder := json.NewDecoder(file) |
|
err = decoder.Decode(&feedInfo) |
|
if err != nil { |
|
fmt.Println("解码 JSON 数据失败:", err) |
|
} else { |
|
fmt.Println("Title: " + feedInfo.Title) |
|
fmt.Println("URL: " + feedInfo.URL) |
|
feed := findByFeedURL(feeds, feedInfo.URL) |
|
if feed != nil { |
|
fmt.Println("Feed ID: "+ strconv.Itoa(int(feed.ID))) |
|
restoreEntries(feed, filepath.Join(root, entry.Name()), s, db) |
|
}else{ |
|
fmt.Println("Ignore: feed 不存在") |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
func restoreEntries(feed *model.Feed, root string, s *storage.Storage, db *sql.DB){ |
|
files, err := filepath.Glob((filepath.Join(root, "content_*.json"))) |
|
if err != nil { |
|
fmt.Println("打开文件失败:", err) |
|
return |
|
} |
|
|
|
for _, file := range files{ |
|
content, err := os.Open(file) |
|
if err != nil { |
|
fmt.Println("打开文件失败:", err) |
|
return |
|
} |
|
defer content.Close() |
|
|
|
decoder := json.NewDecoder(content) |
|
jsonfeed := new(jsonFeed) |
|
err = decoder.Decode(&jsonfeed) |
|
if err != nil { |
|
fmt.Println("解码 JSON 数据失败:", err) |
|
} else { |
|
fmt.Println("Restore: " + file) |
|
var entries model.Entries = []*model.Entry{} |
|
urls := []string{} |
|
for _, item := range jsonfeed.Items{ |
|
entry := item.Transform() |
|
entries = append(entries, entry) |
|
urls = append(urls, entry.URL) |
|
} |
|
|
|
filter, existAll := entriesFilter(feed.ID, urls, db) |
|
if !existAll { |
|
tx, err := db.Begin() |
|
if err != nil { |
|
logger.Fatal(`store: unable to start transaction: %v`, err) |
|
return |
|
} |
|
for _, entry := range entries{ |
|
if !filter[entry.URL]{ |
|
fmt.Println("Insert: " + entry.Title) |
|
entry.FeedID = feed.ID |
|
entry.UserID = feed.UserID |
|
err := createEntry(s, tx, entry) |
|
if err != nil { |
|
tx.Rollback() |
|
logger.Fatal("%v", err) |
|
return |
|
} |
|
}else{ |
|
fmt.Println("Exist: " +entry.Title) |
|
} |
|
} |
|
if err := tx.Commit(); err != nil { |
|
logger.Fatal(`store: unable to commit transaction: %v`, err) |
|
return |
|
} |
|
}else{ |
|
fmt.Println("Ingore: All Exist") |
|
} |
|
} |
|
} |
|
} |
|
|
|
func entriesFilter(feedID int64, urls []string, db *sql.DB) (map[string]bool, bool){ |
|
query := "SELECT url, (SELECT COUNT(*) FROM entries WHERE feed_id = $1 AND url = urls.url) > 0 AS exists FROM (SELECT unnest($2::text[]) AS url) AS urls" |
|
|
|
type Exist struct{ |
|
url string |
|
b bool |
|
} |
|
|
|
exists := []Exist{} |
|
rows, err := db.Query(query, feedID, pq.Array(urls)) |
|
if err != nil { |
|
panic(err) |
|
} |
|
defer rows.Close() |
|
for rows.Next() { |
|
var e Exist |
|
if err := rows.Scan(&e.url, &e.b); err != nil { |
|
panic(err) |
|
} |
|
exists = append(exists, e) |
|
} |
|
if err := rows.Err(); err != nil { |
|
panic(err) |
|
} |
|
|
|
existMap := make(map[string]bool) |
|
existAll := true |
|
for _, e := range exists { |
|
existMap[e.url] = e.b |
|
existAll = existAll && e.b |
|
} |
|
|
|
return existMap, existAll |
|
|
|
} |
|
// createEntry add a new entry. |
|
func createEntry(s *storage.Storage, tx *sql.Tx, entry *model.Entry) error { |
|
query := ` |
|
INSERT INTO entries |
|
( |
|
title, |
|
hash, |
|
url, |
|
comments_url, |
|
published_at, |
|
content, |
|
author, |
|
user_id, |
|
feed_id, |
|
reading_time, |
|
changed_at, |
|
document_vectors, |
|
tags |
|
) |
|
VALUES |
|
( |
|
$1, |
|
$2, |
|
$3, |
|
$4, |
|
$5, |
|
$6, |
|
$7, |
|
$8, |
|
$9, |
|
$10, |
|
now(), |
|
setweight(to_tsvector(left(coalesce($1, ''), 500000)), 'A') || setweight(to_tsvector(left(coalesce($6, ''), 500000)), 'B'), |
|
$11 |
|
) |
|
RETURNING |
|
id, status |
|
` |
|
err := tx.QueryRow( |
|
query, |
|
entry.Title, |
|
entry.Hash, |
|
entry.URL, |
|
entry.CommentsURL, |
|
entry.Date, |
|
entry.Content, |
|
entry.Author, |
|
entry.UserID, |
|
entry.FeedID, |
|
entry.ReadingTime, |
|
pq.Array(entry.Tags), |
|
).Scan(&entry.ID, &entry.Status) |
|
|
|
if err != nil { |
|
return fmt.Errorf(`store: unable to create entry %q (feed #%d): %v`, entry.URL, entry.FeedID, err) |
|
} |
|
|
|
return nil |
|
} |
|
|
|
|
|
|
|
func initFeeds(s *storage.Storage, username string) model.Feeds{ |
|
fmt.Println(username) |
|
user, err := s.UserByUsername(username) |
|
if err != nil { |
|
logger.Fatal("%v", err) |
|
} |
|
fmt.Println(user) |
|
feeds, err := s.Feeds(user.ID) |
|
if err != nil { |
|
logger.Fatal("%v", err) |
|
} |
|
return feeds |
|
} |
|
|
|
func main() { |
|
flagConfigFile := "/etc/miniflux.conf" |
|
var err error |
|
cfg := config.NewParser() |
|
config.Opts, err = cfg.ParseFile(flagConfigFile) |
|
if err != nil { |
|
logger.Fatal("%v", err) |
|
} |
|
config.Opts, err = cfg.ParseEnvironmentVariables() |
|
|
|
db, err := database.NewConnectionPool( |
|
config.Opts.DatabaseURL(), |
|
config.Opts.DatabaseMinConns(), |
|
config.Opts.DatabaseMaxConns(), |
|
config.Opts.DatabaseConnectionLifetime(), |
|
) |
|
if err != nil { |
|
logger.Fatal("Unable to initialize database connection pool: %v", err) |
|
} |
|
defer db.Close() |
|
|
|
store := storage.NewStorage(db) |
|
|
|
if err := store.Ping(); err != nil { |
|
logger.Fatal("Unable to connect to the database: %v", err) |
|
} |
|
|
|
|
|
username := os.Args[len(os.Args)-1] |
|
root := os.Args[len(os.Args)-2] |
|
feeds := initFeeds(store, username) |
|
restoreBackup(feeds, store, db, root) |
|
|
|
|
|
} |