Blob


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