Implement basic Client which supports the gemini protocol
This commit is contained in:
parent
8007f090f2
commit
e03e4037d2
140
client.go
Normal file
140
client.go
Normal file
@ -0,0 +1,140 @@
|
||||
package deadlinks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"git.sr.ht/~adnano/go-gemini"
|
||||
)
|
||||
|
||||
// Client is a thread-safe type which fetches a resource at the given URL,
|
||||
// returning its MIME type and body. If the MIME type is not known then empty
|
||||
// string should be returned.
|
||||
type Client interface {
|
||||
Get(context.Context, URL) (string, io.ReadCloser, error)
|
||||
}
|
||||
|
||||
// ClientOpts are optional fields which can be provided to NewClient. A nil
|
||||
// ClientOpts is equivalent to an empty one.
|
||||
type ClientOpts struct {
|
||||
// GeminiClient will be used for retrieving resources via the gemini
|
||||
// protocol.
|
||||
//
|
||||
// Defaults to `new(gemini.Client)`.
|
||||
GeminiClient interface {
|
||||
Do(context.Context, *gemini.Request) (*gemini.Response, error)
|
||||
}
|
||||
|
||||
// MaxRedirects indicates the maximum number of redirects which will be
|
||||
// allowed when resolving a resource. A negative value indicates no
|
||||
// redirects are allowed.
|
||||
//
|
||||
// Default: 10.
|
||||
MaxRedirects int
|
||||
}
|
||||
|
||||
func (o *ClientOpts) withDefaults() *ClientOpts {
|
||||
if o == nil {
|
||||
o = new(ClientOpts)
|
||||
}
|
||||
|
||||
if o.GeminiClient == nil {
|
||||
o.GeminiClient = new(gemini.Client)
|
||||
}
|
||||
|
||||
if o.MaxRedirects == 0 {
|
||||
o.MaxRedirects = 10
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
type client struct {
|
||||
opts ClientOpts
|
||||
}
|
||||
|
||||
// NewClient initializes and returns a Client which supports commonly used
|
||||
// transport protocols. The returned Client will error when it encounters an
|
||||
// unfamiliar protocol.
|
||||
//
|
||||
// Supported URL schemas:
|
||||
// - gemini
|
||||
// - http/https (TODO)
|
||||
func NewClient(opts *ClientOpts) Client {
|
||||
return &client{*opts.withDefaults()}
|
||||
}
|
||||
|
||||
func (c *client) getGemini(
|
||||
ctx context.Context, url URL, redirectDepth int,
|
||||
) (
|
||||
string, io.ReadCloser, error,
|
||||
) {
|
||||
req, err := gemini.NewRequest(string(url))
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("building request: %w", err)
|
||||
}
|
||||
|
||||
// TODO allow specifying client cert
|
||||
|
||||
res, err := c.opts.GeminiClient.Do(ctx, req)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("performing request: %w", err)
|
||||
}
|
||||
|
||||
// all status numbers are grouped by their first digit, and actions taken
|
||||
// can be entirely based on that.
|
||||
switch res.Status / 10 {
|
||||
|
||||
case 1: // input required
|
||||
// Assume that input required is fine, even though we don't know the
|
||||
// MIME type.
|
||||
return "", res.Body, nil
|
||||
|
||||
case 2: // success
|
||||
return res.Meta, res.Body, nil
|
||||
|
||||
case 3: // redirect
|
||||
defer res.Body.Close()
|
||||
if redirectDepth >= c.opts.MaxRedirects {
|
||||
return "", nil, errors.New("too many redirects")
|
||||
}
|
||||
|
||||
newURL, err := url.ResolveReference(res.Meta)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("resolving redirect URL %q: %w", res.Meta, err)
|
||||
}
|
||||
|
||||
return c.get(ctx, newURL, redirectDepth+1)
|
||||
|
||||
default:
|
||||
defer res.Body.Close()
|
||||
return "", nil, fmt.Errorf(
|
||||
"response code %d (%v): %q", res.Status, res.Status, res.Meta,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) get(
|
||||
ctx context.Context, url URL, redirectDepth int,
|
||||
) (
|
||||
string, io.ReadCloser, error,
|
||||
) {
|
||||
|
||||
scheme := url.toStd().Scheme
|
||||
switch scheme {
|
||||
case "gemini":
|
||||
return c.getGemini(ctx, url, redirectDepth)
|
||||
default:
|
||||
return "", nil, fmt.Errorf("unsupported scheme %q", scheme)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) Get(
|
||||
ctx context.Context, url URL,
|
||||
) (
|
||||
string, io.ReadCloser, error,
|
||||
) {
|
||||
return c.get(ctx, url, 0)
|
||||
}
|
17
deadlinks.go
17
deadlinks.go
@ -50,6 +50,23 @@ func parseURLs(urlStrs []string) ([]URL, error) {
|
||||
return res, errors.Join(errs...)
|
||||
}
|
||||
|
||||
func (u URL) toStd() *url.URL {
|
||||
uu, err := url.Parse(string(u))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("parsing URL %q: %v", u, err))
|
||||
}
|
||||
return uu
|
||||
}
|
||||
|
||||
// ResolveReference is equivalend to the method of the same name in `net/url`.
|
||||
func (u URL) ResolveReference(u2Str string) (URL, error) {
|
||||
u2, err := url.Parse(u2Str)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return URL(u.toStd().ResolveReference(u2).String()), nil
|
||||
}
|
||||
|
||||
// ResourceStatus describes what state a particular Resource is in.
|
||||
type ResourceStatus int
|
||||
|
||||
|
3
go.mod
3
go.mod
@ -4,6 +4,7 @@ go 1.20
|
||||
|
||||
require (
|
||||
code.betamike.com/mediocregopher/mediocre-go-lib v0.0.0-20231226160338-0b5bdf3dfb03 // indirect
|
||||
git.sr.ht/~adnano/go-gemini v0.2.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.19 // indirect
|
||||
@ -11,5 +12,7 @@ require (
|
||||
github.com/rubenv/sql-migrate v1.6.0 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/stretchr/testify v1.8.4 // indirect
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
|
||||
golang.org/x/text v0.3.3 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
9
go.sum
9
go.sum
@ -2,6 +2,8 @@ code.betamike.com/mediocregopher/mediocre-go-lib v0.0.0-20231226155808-e8376ef26
|
||||
code.betamike.com/mediocregopher/mediocre-go-lib v0.0.0-20231226155808-e8376ef263a7/go.mod h1:GJhpoMNnN/OT6O9NmeQBV02yq9kQP8zPyY1IvsslHak=
|
||||
code.betamike.com/mediocregopher/mediocre-go-lib v0.0.0-20231226160338-0b5bdf3dfb03 h1:wJ6X1vc289RpHVGClD1P33yijPoNIdgCXbTn7DjVWYs=
|
||||
code.betamike.com/mediocregopher/mediocre-go-lib v0.0.0-20231226160338-0b5bdf3dfb03/go.mod h1:GJhpoMNnN/OT6O9NmeQBV02yq9kQP8zPyY1IvsslHak=
|
||||
git.sr.ht/~adnano/go-gemini v0.2.3 h1:oJ+Y0/mheZ4Vg0ABjtf5dlmvq1yoONStiaQvmWWkofc=
|
||||
git.sr.ht/~adnano/go-gemini v0.2.3/go.mod h1:hQ75Y0i5jSFL+FQ7AzWVAYr5LQsaFC7v3ZviNyj46dY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@ -21,6 +23,13 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
4
store.go
4
store.go
@ -80,8 +80,8 @@ var migrations = &migrate.MemoryMigrationSource{Migrations: []*migrate.Migration
|
||||
},
|
||||
}}
|
||||
|
||||
// SQLiteSQLiteStoreOpts are optional fields which can be provided to NewSQLiteStore.
|
||||
// A nil SQLiteSQLiteStoreOpts is equivalent to an empty one.
|
||||
// SQLiteSQLiteStoreOpts are optional fields which can be provided to
|
||||
// NewSQLiteStore. A nil SQLiteSQLiteStoreOpts is equivalent to an empty one.
|
||||
type SQLiteStoreOpts struct {
|
||||
// Path to the database file to use.
|
||||
//
|
||||
|
Loading…
Reference in New Issue
Block a user