Blame


1 fd79aa76 2024-03-28 o // Package hn provides a filesystem interface to items on Hacker News.
2 fd79aa76 2024-03-28 o package hn
3 fd79aa76 2024-03-28 o
4 fd79aa76 2024-03-28 o import (
5 fd79aa76 2024-03-28 o "bytes"
6 fd79aa76 2024-03-28 o "encoding/json"
7 fd79aa76 2024-03-28 o "fmt"
8 fd79aa76 2024-03-28 o "html"
9 fd79aa76 2024-03-28 o "io"
10 fd79aa76 2024-03-28 o "io/fs"
11 fd79aa76 2024-03-28 o "net/http"
12 fd79aa76 2024-03-28 o "os"
13 fd79aa76 2024-03-28 o "path"
14 fd79aa76 2024-03-28 o "strconv"
15 fd79aa76 2024-03-28 o "strings"
16 fd79aa76 2024-03-28 o "time"
17 fd79aa76 2024-03-28 o )
18 fd79aa76 2024-03-28 o
19 fd79aa76 2024-03-28 o const APIRoot = "https://hacker-news.firebaseio.com/v0"
20 fd79aa76 2024-03-28 o
21 fd79aa76 2024-03-28 o type Item struct {
22 fd79aa76 2024-03-28 o ID int
23 fd79aa76 2024-03-28 o Type string
24 fd79aa76 2024-03-28 o By string
25 fd79aa76 2024-03-28 o Time int
26 fd79aa76 2024-03-28 o Text string
27 fd79aa76 2024-03-28 o Parent int
28 fd79aa76 2024-03-28 o URL string
29 fd79aa76 2024-03-28 o Title string
30 fd79aa76 2024-03-28 o }
31 fd79aa76 2024-03-28 o
32 fd79aa76 2024-03-28 o func (it *Item) Name() string { return strconv.Itoa(it.ID) }
33 fd79aa76 2024-03-28 o func (it *Item) Size() int64 { r := toMessage(it); return r.Size() }
34 fd79aa76 2024-03-28 o func (it *Item) Mode() fs.FileMode { return 0o444 }
35 fd79aa76 2024-03-28 o func (it *Item) ModTime() time.Time { return time.Unix(int64(it.Time), 0) }
36 fd79aa76 2024-03-28 o func (it *Item) IsDir() bool { return false }
37 fd79aa76 2024-03-28 o func (it *Item) Sys() any { return nil }
38 fd79aa76 2024-03-28 o
39 fd79aa76 2024-03-28 o type FS struct {
40 fd79aa76 2024-03-28 o cache fs.FS
41 fd79aa76 2024-03-28 o }
42 fd79aa76 2024-03-28 o
43 fd79aa76 2024-03-28 o func CacheDirFS(name string) *FS {
44 fd79aa76 2024-03-28 o return &FS{cache: os.DirFS(name)}
45 fd79aa76 2024-03-28 o }
46 fd79aa76 2024-03-28 o
47 fd79aa76 2024-03-28 o func (fsys *FS) Open(name string) (fs.File, error) {
48 fd79aa76 2024-03-28 o if !fs.ValidPath(name) {
49 fd79aa76 2024-03-28 o return nil, &fs.PathError{"open", name, fs.ErrInvalid}
50 fd79aa76 2024-03-28 o }
51 fd79aa76 2024-03-28 o name = path.Clean(name)
52 fd79aa76 2024-03-28 o switch name {
53 fd79aa76 2024-03-28 o case ".":
54 fd79aa76 2024-03-28 o return nil, fmt.Errorf("TODO")
55 fd79aa76 2024-03-28 o default:
56 fd79aa76 2024-03-28 o if _, err := strconv.Atoi(name); err != nil {
57 fd79aa76 2024-03-28 o return nil, &fs.PathError{"open", name, fs.ErrNotExist}
58 fd79aa76 2024-03-28 o }
59 fd79aa76 2024-03-28 o }
60 fd79aa76 2024-03-28 o if fsys.cache != nil {
61 fd79aa76 2024-03-28 o if f, err := fsys.cache.Open(name); err == nil {
62 fd79aa76 2024-03-28 o return f, nil
63 fd79aa76 2024-03-28 o }
64 fd79aa76 2024-03-28 o }
65 fd79aa76 2024-03-28 o
66 fd79aa76 2024-03-28 o u := fmt.Sprintf("%s/item/%s.json", APIRoot, name)
67 fd79aa76 2024-03-28 o resp, err := http.Get(u)
68 fd79aa76 2024-03-28 o if err != nil {
69 fd79aa76 2024-03-28 o return nil, err
70 fd79aa76 2024-03-28 o }
71 fd79aa76 2024-03-28 o if resp.StatusCode != http.StatusOK {
72 fd79aa76 2024-03-28 o return nil, err
73 fd79aa76 2024-03-28 o }
74 fd79aa76 2024-03-28 o return &file{rc: resp.Body}, nil
75 fd79aa76 2024-03-28 o }
76 fd79aa76 2024-03-28 o
77 fd79aa76 2024-03-28 o type file struct {
78 fd79aa76 2024-03-28 o rc io.ReadCloser
79 fd79aa76 2024-03-28 o item *Item
80 fd79aa76 2024-03-28 o msg *bytes.Reader
81 fd79aa76 2024-03-28 o }
82 fd79aa76 2024-03-28 o
83 fd79aa76 2024-03-28 o func (f *file) Read(p []byte) (int, error) {
84 fd79aa76 2024-03-28 o var n int
85 fd79aa76 2024-03-28 o if f.item == nil {
86 fd79aa76 2024-03-28 o if err := json.NewDecoder(f.rc).Decode(&f.item); err != nil {
87 fd79aa76 2024-03-28 o return n, fmt.Errorf("decode item: %v", err)
88 fd79aa76 2024-03-28 o }
89 fd79aa76 2024-03-28 o }
90 fd79aa76 2024-03-28 o if f.msg == nil {
91 fd79aa76 2024-03-28 o f.msg = toMessage(f.item)
92 fd79aa76 2024-03-28 o }
93 fd79aa76 2024-03-28 o return f.msg.Read(p)
94 fd79aa76 2024-03-28 o }
95 fd79aa76 2024-03-28 o
96 fd79aa76 2024-03-28 o func (f *file) Stat() (fs.FileInfo, error) { return f.item, nil }
97 fd79aa76 2024-03-28 o
98 fd79aa76 2024-03-28 o func (f *file) Close() error {
99 fd79aa76 2024-03-28 o f.msg = nil
100 fd79aa76 2024-03-28 o return f.rc.Close()
101 fd79aa76 2024-03-28 o }
102 fd79aa76 2024-03-28 o
103 fd79aa76 2024-03-28 o func toMessage(item *Item) *bytes.Reader {
104 fd79aa76 2024-03-28 o buf := &bytes.Buffer{}
105 fd79aa76 2024-03-28 o fmt.Fprintf(buf, "From: %s\n", item.By)
106 fd79aa76 2024-03-28 o fmt.Fprintf(buf, "Message-ID: <%d@news.ycombinator.com>\n", item.ID)
107 fd79aa76 2024-03-28 o fmt.Fprintf(buf, "Date: %s\n", time.Unix(int64(item.Time), 0).Format(time.RFC1123Z))
108 fd79aa76 2024-03-28 o if item.Parent != 0 {
109 fd79aa76 2024-03-28 o fmt.Fprintf(buf, "References: <%d@news.ycombinator.com>\n", item.Parent)
110 fd79aa76 2024-03-28 o }
111 fd79aa76 2024-03-28 o if item.Title != "" {
112 fd79aa76 2024-03-28 o fmt.Fprintf(buf, "Subject: %s\n", item.Title)
113 fd79aa76 2024-03-28 o }
114 fd79aa76 2024-03-28 o fmt.Fprintln(buf)
115 fd79aa76 2024-03-28 o if item.URL != "" {
116 fd79aa76 2024-03-28 o fmt.Fprintln(buf, item.URL)
117 fd79aa76 2024-03-28 o }
118 fd79aa76 2024-03-28 o if item.Text != "" {
119 fd79aa76 2024-03-28 o fmt.Fprintln(buf, strings.ReplaceAll(html.UnescapeString(item.Text), "<p>", "\n\n"))
120 fd79aa76 2024-03-28 o }
121 fd79aa76 2024-03-28 o return bytes.NewReader(buf.Bytes())
122 fd79aa76 2024-03-28 o }