Skip to content

Commit af7ceb5

Browse files
committed
add support for resolving links locally
Specify a command line argument to resolve a link locally and exit. Also add a new flag, -resolve-from-backup, which loads a snapshot into an in-memory database and resolves the specified link.
1 parent 9d1e45f commit af7ceb5

File tree

3 files changed

+103
-5
lines changed

3 files changed

+103
-5
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,7 @@ Only links that don't already exist in the database will be added.
120120
golink -snapshot links.json
121121

122122
[JSON lines]: https://jsonlines.org/
123+
124+
You can also resolve links locally using a snapshot file:
125+
126+
golink -resolve-from-backup links.json go/link

golink.go

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,12 @@ import (
3636
const defaultHostname = "go"
3737

3838
var (
39-
verbose = flag.Bool("verbose", false, "be verbose")
40-
sqlitefile = flag.String("sqlitedb", "", "path of SQLite database to store links")
41-
dev = flag.String("dev-listen", "", "if non-empty, listen on this addr and run in dev mode; auto-set sqlitedb if empty and don't use tsnet")
42-
snapshot = flag.String("snapshot", "", "file path of snapshot file")
43-
hostname = flag.String("hostname", defaultHostname, "service name")
39+
verbose = flag.Bool("verbose", false, "be verbose")
40+
sqlitefile = flag.String("sqlitedb", "", "path of SQLite database to store links")
41+
dev = flag.String("dev-listen", "", "if non-empty, listen on this addr and run in dev mode; auto-set sqlitedb if empty and don't use tsnet")
42+
snapshot = flag.String("snapshot", "", "file path of snapshot file")
43+
hostname = flag.String("hostname", defaultHostname, "service name")
44+
resolveFromBackup = flag.String("resolve-from-backup", "", "resolve a link from snapshot file and exit")
4445
)
4546

4647
var stats struct {
@@ -66,6 +67,16 @@ var localClient *tailscale.LocalClient
6667
func Run() error {
6768
flag.Parse()
6869

70+
// if resolving from backup, set sqlitefile and snapshot flags to
71+
// restore links into an in-memory sqlite database.
72+
if *resolveFromBackup != "" {
73+
*sqlitefile = ":memory:"
74+
snapshot = resolveFromBackup
75+
if flag.NArg() != 1 {
76+
log.Fatal("--resolve-from-backup also requires a link to be resolved")
77+
}
78+
}
79+
6980
if *sqlitefile == "" {
7081
if devMode() {
7182
tmpdir, err := ioutil.TempDir("", "golink_dev_*")
@@ -102,6 +113,16 @@ func Run() error {
102113
log.Printf("initializing stats: %v", err)
103114
}
104115

116+
// if link specified on command line, resolve and exit
117+
if flag.NArg() > 0 {
118+
destination, err := resolveLink(flag.Arg(0))
119+
if err != nil {
120+
log.Fatal(err)
121+
}
122+
fmt.Println(destination)
123+
os.Exit(0)
124+
}
125+
105126
// flush stats periodically
106127
go flushStatsLoop()
107128

@@ -588,3 +609,19 @@ func restoreLastSnapshot() error {
588609
}
589610
return bs.Err()
590611
}
612+
613+
func resolveLink(link string) (string, error) {
614+
// if link specified as "go/name", trim "go" prefix.
615+
// Remainder will parse as URL with no scheme or host
616+
link = strings.TrimPrefix(link, *hostname)
617+
u, err := url.Parse(link)
618+
if err != nil {
619+
return "", err
620+
}
621+
short, remainder, _ := strings.Cut(strings.TrimPrefix(u.RequestURI(), "/"), "/")
622+
l, err := db.Load(short)
623+
if err != nil {
624+
return "", err
625+
}
626+
return expandLink(l.Long, expandEnv{Now: time.Now().UTC(), Path: remainder})
627+
}

golink_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,60 @@ func TestExpandLink(t *testing.T) {
8787
})
8888
}
8989
}
90+
91+
func TestResolveLink(t *testing.T) {
92+
var err error
93+
db, err = NewSQLiteDB(":memory:")
94+
if err != nil {
95+
t.Fatal(err)
96+
}
97+
db.Save(&Link{Short: "meet", Long: "https://meet.google.com/lookup/"})
98+
db.Save(&Link{Short: "cs", Long: "http://codesearch/{{with .Path}}search?q={{.}}{{end}}"})
99+
100+
tests := []struct {
101+
link string
102+
want string
103+
}{
104+
{
105+
link: "meet",
106+
want: "https://meet.google.com/lookup/",
107+
},
108+
{
109+
link: "meet/foo",
110+
want: "https://meet.google.com/lookup/foo",
111+
},
112+
{
113+
link: "go/meet/foo",
114+
want: "https://meet.google.com/lookup/foo",
115+
},
116+
{
117+
link: "http://go/meet/foo",
118+
want: "https://meet.google.com/lookup/foo",
119+
},
120+
{
121+
// if absolute URL provided, host doesn't actually matter
122+
link: "http://mygo/meet/foo",
123+
want: "https://meet.google.com/lookup/foo",
124+
},
125+
{
126+
link: "cs",
127+
want: "http://codesearch/",
128+
},
129+
{
130+
link: "cs/term",
131+
want: "http://codesearch/search?q=term",
132+
},
133+
}
134+
for _, tt := range tests {
135+
name := "golink " + tt.link
136+
t.Run(name, func(t *testing.T) {
137+
got, err := resolveLink(tt.link)
138+
if err != nil {
139+
t.Error(err)
140+
}
141+
if got != tt.want {
142+
t.Errorf("ResolveLink(%q) = %q; want %q", tt.link, got, tt.want)
143+
}
144+
})
145+
}
146+
}

0 commit comments

Comments
 (0)