Mirrors were generated once and never again and caches never cleaned. The module holds node-package-manager (hq ADR 0207), declares pacman itself, owns /etc/pacman.conf whole — [options] cannot take an appended block — with the union of the enabled repositories and improved options, proven by pacman-conf in its test, and enables reflector.timer (its config owned) and paccache.timer. Fifteen tools from a Go bundle; transactions run as transient units so a call's timeout never kills pacman mid-transaction (to-be 42).
111 lines
3.1 KiB
Go
111 lines
3.1 KiB
Go
package main
|
|
|
|
// The distribution's news since the last upgrade (novox/hq research 026/05: "an upgrade with the
|
|
// news first"). Arch posts what an upgrade needs a person to do — a manual intervention, a replaced
|
|
// package — in its news feed, and an upgrade that ignores it is how a machine breaks. Fetched over
|
|
// https; no network is an answer, never a failure.
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// NewsFeed is the distribution's news, as RSS.
|
|
const NewsFeed = "https://archlinux.org/feeds/news/"
|
|
|
|
// fetch is how the feed is read; a test replaces it.
|
|
var fetch = func(url string) ([]byte, error) {
|
|
client := http.Client{Timeout: 10 * time.Second}
|
|
res, err := client.Get(url)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("%s answered %s", url, res.Status)
|
|
}
|
|
return io.ReadAll(io.LimitReader(res.Body, 4<<20))
|
|
}
|
|
|
|
// NewsItem is one post.
|
|
type NewsItem struct {
|
|
Title string `json:"title"`
|
|
Link string `json:"link"`
|
|
Published string `json:"published"`
|
|
Summary string `json:"summary"`
|
|
}
|
|
|
|
type rss struct {
|
|
Items []struct {
|
|
Title string `xml:"title"`
|
|
Link string `xml:"link"`
|
|
PubDate string `xml:"pubDate"`
|
|
Description string `xml:"description"`
|
|
} `xml:"channel>item"`
|
|
}
|
|
|
|
var tags = regexp.MustCompile(`<[^>]*>`)
|
|
|
|
// ParseNews reads the feed's posts published after a time, newest first as the feed has them.
|
|
func ParseNews(body []byte, since time.Time) ([]NewsItem, error) {
|
|
var feed rss
|
|
if err := xml.Unmarshal(body, &feed); err != nil {
|
|
return nil, err
|
|
}
|
|
items := []NewsItem{}
|
|
for _, it := range feed.Items {
|
|
t, err := time.Parse(time.RFC1123Z, strings.TrimSpace(it.PubDate))
|
|
if err != nil {
|
|
t, err = time.Parse(time.RFC1123, strings.TrimSpace(it.PubDate))
|
|
}
|
|
if err != nil || !t.After(since) {
|
|
continue
|
|
}
|
|
summary := strings.Join(strings.Fields(tags.ReplaceAllString(it.Description, " ")), " ")
|
|
if len(summary) > 600 {
|
|
summary = summary[:600] + "…"
|
|
}
|
|
items = append(items, NewsItem{Title: it.Title, Link: it.Link, Published: t.Format(time.RFC3339), Summary: summary})
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
// News is the posts since a day (YYYY-MM-DD), or since the last full upgrade the log records.
|
|
func (m *Machine) News(since string) map[string]any {
|
|
out := map[string]any{"feed": NewsFeed, "items": []NewsItem{}}
|
|
var from time.Time
|
|
if since != "" {
|
|
t, err := time.ParseInLocation("2006-01-02", since, time.Local)
|
|
if err != nil {
|
|
out["error"] = fmt.Sprintf("since %q is not a day as YYYY-MM-DD", since)
|
|
return out
|
|
}
|
|
from = t
|
|
} else if last, err := m.LastUpgrade(); err == nil && !last.IsZero() {
|
|
from = last
|
|
out["since_last_full_upgrade"] = true
|
|
} else {
|
|
from = m.Now().AddDate(0, 0, -90)
|
|
}
|
|
out["since"] = from.Format(time.RFC3339)
|
|
body, err := fetch(NewsFeed)
|
|
if err != nil {
|
|
out["reachable"] = false
|
|
out["error"] = err.Error()
|
|
return out
|
|
}
|
|
out["reachable"] = true
|
|
items, err := ParseNews(body, from)
|
|
if err != nil {
|
|
out["error"] = "the feed could not be read: " + err.Error()
|
|
return out
|
|
}
|
|
out["items"] = items
|
|
return out
|
|
}
|