Recently Written · git

sbm-webpublish

Publish an sbm bookmark file as a list of links on a web site

git clone https://github.com/equwal/sbm-webpublish

Log | Files | Refs


commit f4991da6450c09fb3d7317c7b9ed6ace57999bfe
equwal <13551856+equwal@users.noreply.github.com>
2026-09-25 03:14:12 -0700

Add the command 'sbm-webpublish feed URL FEED'

It sets the feed of one link in a shell on the server, as the Save feed form of the admin page does. merge cannot change a feed that a link has already. The form and the command tell when no link has the URL.

 README.md      |  7 +++++--
 main.go        | 53 +++++++++++++++++++++++++++++++++++++++--------------
 server_test.go | 17 +++++++++++++++++
 3 files changed, 61 insertions(+), 16 deletions(-)
diff --git a/README.md b/README.md
index 98a3cd9..36122e9 100644
--- a/README.md
+++ b/README.md
@@ -80,8 +80,11 @@ import does:
 
     bm --list | ssh root@example.com 'LINKS_FILE=/var/lib/sbm-webpublish/links.sbm sbm-webpublish merge'
 
-It does not lock the file against the service, so do not use the admin
-page at the same time.
+`sbm-webpublish feed URL FEED` sets the feed of one link, as the admin
+page does. An empty FEED (`''`) removes the feed.
+
+The commands do not lock the file against the service, so do not use the
+admin page at the same time.
 
 ## Run
 
diff --git a/main.go b/main.go
index 65565b2..53b1259 100644
--- a/main.go
+++ b/main.go
@@ -203,20 +203,37 @@ func (s *server) add(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-// editFeed sets the feed of one link. An empty feed removes the feed.
-func (s *server) editFeed(w http.ResponseWriter, r *http.Request) {
-	feed := r.FormValue("feed")
+// saveFeed sets the feed of the link with the URL u, and tells what it did.
+// An empty feed removes the feed.
+func saveFeed(links *store, u, feed string) (string, error) {
 	if badFeed(feed) {
-		done(w, r, "Give an http or https address for the feed.")
-		return
+		return "Give an http or https address for the feed.", nil
 	}
-	k := norm(r.FormValue("url"))
-	if err := s.links.change(func(text string) string { return setFeed(text, k, feed) }); err != nil {
+	k, found := norm(u), false
+	err := links.change(func(text string) string {
+		for _, s := range lines(text) {
+			found = found || k != "" && key(s) == k
+		}
+		return setFeed(text, k, feed)
+	})
+	switch {
+	case err != nil:
+		return "", err
+	case !found:
+		return "No link has that URL.", nil
+	}
+	return "Saved the feed.", nil
+}
+
+// editFeed sets the feed of one link. An empty feed removes the feed.
+func (s *server) editFeed(w http.ResponseWriter, r *http.Request) {
+	message, err := saveFeed(s.links, r.FormValue("url"), r.FormValue("feed"))
+	if err != nil {
 		log.Print(err)
 		http.Error(w, "cannot write the links", http.StatusInternalServerError)
 		return
 	}
-	done(w, r, "Saved the feed.")
+	done(w, r, message)
 }
 
 func (s *server) delete(w http.ResponseWriter, r *http.Request) {
@@ -273,14 +290,22 @@ func env(name, fallback string) string {
 func main() {
 	links := &store{path: env("LINKS_FILE", "links.sbm")}
 	if len(os.Args) > 1 {
-		// "merge" adds the links of stdin, as bm --merge does. It does not
-		// lock the file against the service: do not use the admin page at
-		// the same time.
-		if os.Args[1] != "merge" || len(os.Args) > 2 {
-			fmt.Fprintln(os.Stderr, "usage: sbm-webpublish [merge < bookmarks]")
+		// The commands change the file as the admin page does, in a shell
+		// on the server. They do not lock the file against the service: do
+		// not use the admin page at the same time.
+		var message string
+		var err error
+		switch args := os.Args[1:]; {
+		case len(args) == 1 && args[0] == "merge":
+			// merge adds the links of stdin, as bm --merge does.
+			message, err = mergeFrom(links, os.Stdin, time.Now())
+		case len(args) == 3 && args[0] == "feed":
+			// feed URL FEED sets the feed of a link.
+			message, err = saveFeed(links, args[1], args[2])
+		default:
+			fmt.Fprintln(os.Stderr, "usage: sbm-webpublish [merge < bookmarks | feed URL FEED]")
 			os.Exit(2)
 		}
-		message, err := mergeFrom(links, os.Stdin, time.Now())
 		if err != nil {
 			log.Fatal(err)
 		}
diff --git a/server_test.go b/server_test.go
index d3769a1..2f4774a 100644
--- a/server_test.go
+++ b/server_test.go
@@ -330,6 +330,23 @@ func TestImport(t *testing.T) {
 	}
 }
 
+func TestFeedCommand(t *testing.T) {
+	s, _ := newServer(t)
+	write(t, s, "https://a.com\tA\tx\thttps://github.com/a/commits.atom\n")
+	for _, c := range []struct{ u, feed, message, file string }{
+		// A feed that the link has already changes.
+		{"a.com/", "https://a.com/Japanese.xml", "Saved the feed.", "https://a.com\tA\tx\thttps://a.com/Japanese.xml\n"},
+		{"https://b.com", "https://b.com/rss", "No link has that URL.", "https://a.com\tA\tx\thttps://a.com/Japanese.xml\n"},
+		{"https://a.com", "file:///x", "Give an http or https address for the feed.", "https://a.com\tA\tx\thttps://a.com/Japanese.xml\n"},
+	} {
+		m, err := saveFeed(s.links, c.u, c.feed)
+		b, _ := os.ReadFile(s.links.path)
+		if err != nil || m != c.message || string(b) != c.file {
+			t.Errorf("feed %s %s: %q %v, file %q", c.u, c.feed, m, err, b)
+		}
+	}
+}
+
 func TestMergeCommand(t *testing.T) {
 	s, _ := newServer(t)
 	write(t, s, "https://a.com\tA\t\n")