1 // Package hn provides a filesystem interface to items on Hacker News.
19 const APIRoot = "https://hacker-news.firebaseio.com/v0"
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 }
43 func CacheDirFS(name string) *FS {
44 return &FS{cache: os.DirFS(name)}
47 func (fsys *FS) Open(name string) (fs.File, error) {
48 if !fs.ValidPath(name) {
49 return nil, &fs.PathError{"open", name, fs.ErrInvalid}
51 name = path.Clean(name)
54 return nil, fmt.Errorf("TODO")
56 if _, err := strconv.Atoi(name); err != nil {
57 return nil, &fs.PathError{"open", name, fs.ErrNotExist}
60 if fsys.cache != nil {
61 if f, err := fsys.cache.Open(name); err == nil {
66 u := fmt.Sprintf("%s/item/%s.json", APIRoot, name)
67 resp, err := http.Get(u)
71 if resp.StatusCode != http.StatusOK {
74 return &file{rc: resp.Body}, nil
83 func (f *file) Read(p []byte) (int, error) {
86 if err := json.NewDecoder(f.rc).Decode(&f.item); err != nil {
87 return n, fmt.Errorf("decode item: %v", err)
91 f.msg = toMessage(f.item)
96 func (f *file) Stat() (fs.FileInfo, error) { return f.item, nil }
98 func (f *file) Close() error {
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)
116 fmt.Fprintln(buf, item.URL)
119 fmt.Fprintln(buf, strings.ReplaceAll(html.UnescapeString(item.Text), "<p>", "\n\n"))
121 return bytes.NewReader(buf.Bytes())